/**********************************************************************/
/* INSTMDW.SQL                                                        */
/*                                                                    */
/* Installs the tables and stored procedures necessary for            */
/* supporting the Management Data Warehouse in Data Collector         */
/*                                                                    */
/* MDW database is created by Setup and this script is run in the     */
/* context of the created MDW database                                */
/*                                                                    */
/* To run this script manually on a MDW database:                     */
/* "sqlcmd -d MDW -i instmdw.sql"                                     */
/*                                                                    */
/*                                                                    */
/* Copyright (c) Microsoft Corporation                                */
/* All Rights Reserved.                                               */
/*                                                                    */
/**********************************************************************/


RAISERROR('', 0, 1)  WITH NOWAIT;
RAISERROR('----------------------------------', 0, 1)  WITH NOWAIT;
RAISERROR('Starting execution of INSTMDW.SQL ', 0, 1)  WITH NOWAIT;
RAISERROR('----------------------------------', 0, 1)  WITH NOWAIT;
GO

SET ANSI_NULLS ON
SET ANSI_NULL_DFLT_ON ON
SET ANSI_PADDING ON
SET ANSI_WARNINGS ON
SET ARITHABORT ON
SET CONCAT_NULL_YIELDS_NULL ON
SET NUMERIC_ROUNDABORT OFF
SET QUOTED_IDENTIFIER ON
GO

-- This version of instmdw should be executed only against 10.0 servers or higher
IF (@@microsoftversion / 0x01000000) < 10
BEGIN
    RAISERROR('Management Data Warehouse database can only be installed on an instance of SQL Server 2008 or higher.', 21, 127) WITH LOG 
END
GO

-- SQL Server Express Edition does not support a management data warehouse.
DECLARE @retval sql_variant
SELECT @retval = (SELECT SERVERPROPERTY('EngineEdition'))
IF (@retval = 4) -- 4: Express
BEGIN
    RAISERROR(14713, 20, -1) WITH LOG 
END
GO

-- Take the database to single mode during the installation
RAISERROR('', 0, 1)  WITH NOWAIT;
RAISERROR('Taking database to single user mode', 0, 1)  WITH NOWAIT;

DECLARE @dbname sysname
SELECT @dbname = QUOTENAME(DB_NAME())


-- Check if someone has turned async statistics autoupdate on
DECLARE @async_auto_stat bit
SELECT @async_auto_stat = is_auto_update_stats_async_on
FROM sys.databases
WHERE database_id = DB_ID()

CREATE TABLE #tmp_auto_mode (auto_mode bit)

IF (@async_auto_stat = 1)       -- if yes, turn it off while install
BEGIN
    RAISERROR('Disabling asynchronous auto statistics while database in single user mode ...', 0, 1)  WITH NOWAIT;
    DECLARE @sql_async_autostat_off nvarchar(256)
    SET @sql_async_autostat_off = 'ALTER DATABASE ' + @dbname + 
                                            ' SET AUTO_UPDATE_STATISTICS_ASYNC OFF'

    EXEC sp_executesql @sql_async_autostat_off

    -- check for all the currently running background statistics jobs
    -- and kill them
    DECLARE @stats_job_id nvarchar(10)
    DECLARE @sql_kill_stats_job nvarchar(256)

    DECLARE stats_jobs_id_cursor CURSOR READ_ONLY FOR
    SELECT CONVERT(nvarchar(10), job_id)
    FROM sys.dm_exec_background_job_queue
    WHERE database_id = DB_ID()

    OPEN stats_jobs_id_cursor
    FETCH NEXT FROM stats_jobs_id_cursor INTO @stats_job_id
    WHILE (@@FETCH_STATUS = 0)
    BEGIN
        SET @sql_kill_stats_job = 'KILL STATS JOB ' + @stats_job_id
        EXEC sp_executesql @sql_kill_stats_job
    END

    CLOSE stats_jobs_id_cursor
    DEALLOCATE stats_jobs_id_cursor

    -- save the fact that async_auto_stats was on
    INSERT INTO #tmp_auto_mode
    VALUES(1)
END

-- Now, put the database in single user mode
DECLARE @sql_query nvarchar(256)
SET @sql_query = 'ALTER DATABASE ' + @dbname +
                 '   SET SINGLE_USER WITH ROLLBACK IMMEDIATE'
EXEC sp_executesql @sql_query;

-- Allow the use of snapshot transaction isolation level 
SET @sql_query = 'ALTER DATABASE ' + @dbname + ' SET ALLOW_SNAPSHOT_ISOLATION ON';
EXEC sp_executesql @sql_query;

-- Turn on the read_committed_snapshot database option
SET @sql_query = 'ALTER DATABASE ' + @dbname + ' SET READ_COMMITTED_SNAPSHOT ON';
EXEC sp_executesql @sql_query;
GO


-- Set the right options
-- These are needed for the correct behavior of check constraint
SET ANSI_NULLS ON
SET ANSI_PADDING ON
SET ANSI_WARNINGS ON
SET ARITHABORT ON
SET CONCAT_NULL_YIELDS_NULL ON
SET NUMERIC_ROUNDABORT OFF
SET QUOTED_IDENTIFIER ON 
GO

/**********************************************************************/
/* Add an extended database property to identify database as a data   */
/* warehouse for the data collector.                                  */
/*                                                                    */
/* If a version already exists, check that we are not down grading    */
/* to a lower version.                                                */
/**********************************************************************/

--
-- This stored procedure compares two build numbers
-- It tokenizes the numbers by '.' and compares the portions from left to right
--
IF (OBJECT_ID(N'tempdb..#sp_compare_builds', 'P') IS NOT NULL)
BEGIN
    DROP PROCEDURE [tempdb]..[#sp_compare_builds]
END
GO 

CREATE PROCEDURE #sp_compare_builds
    @old            nvarchar(100),
    @new            nvarchar(100),
    @order          int OUTPUT      -- -1 if @old<@new, 0 if @old=@new, 1 if @old>@new
AS
BEGIN
    DECLARE @retVal int
    DECLARE @old_portion nvarchar(100)
    DECLARE @new_portion nvarchar(100)
    DECLARE @old_number int
    DECLARE @new_number int
    
    SET @old = NULLIF(@old+'.', '.')
    SET @new = NULLIF(@new+'.', '.')    
    SET @order = 0
    BEGIN TRY
        SET @retVal = 0
        WHILE ((@old IS NOT NULL) AND (@new IS NOT NULL))
        BEGIN
            --SELECT @old as old, @new as new
            DECLARE @old_token_mark int
            DECLARE @new_token_mark int
            SET @old_token_mark = CHARINDEX('.', @old)
            SET @new_token_mark = CHARINDEX('.', @new)
            
            -- get the first number in the version from the left
            SET @old_portion = LEFT(@old, @old_token_mark-1)
            SET @new_portion = LEFT(@new, @new_token_mark-1)
            
            -- trim the number from the left
            SET @old = NULLIF(SUBSTRING(@old, @old_token_mark+1, LEN(@old)-@old_token_mark), '')
            SET @new = NULLIF(SUBSTRING(@new, @new_token_mark+1, LEN(@new)-@new_token_mark), '')
            
            -- compare the portions you have
            SET @old_number = CONVERT(int, @old_portion)
            SET @new_number = CONVERT(int, @new_portion)
            
            IF (@old_number = @new_number)
            BEGIN
                CONTINUE
            END
            ELSE
            BEGIN -- We have a resolution, figure out who comes first and get out
                IF (@old_number < @new_number)
                BEGIN
                    SET @order = -1
                END
                ELSE
                BEGIN
                    SET @order = 1
                END
                
                BREAK
            END
        END
        
        RETURN (0)
    END TRY
    BEGIN CATCH
        DECLARE @ErrorMessage   NVARCHAR(4000);
        DECLARE @ErrorSeverity  INT;
        DECLARE @ErrorState     INT;
        DECLARE @ErrorNumber    INT;
        DECLARE @ErrorLine      INT;
        DECLARE @ErrorProcedure NVARCHAR(200);
        SELECT @ErrorLine = ERROR_LINE(),
               @ErrorSeverity = ERROR_SEVERITY(),
               @ErrorState = ERROR_STATE(),
               @ErrorNumber = ERROR_NUMBER(),
               @ErrorMessage = ERROR_MESSAGE(),
               @ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-');

        RAISERROR (14684, @ErrorSeverity, -1 , @ErrorNumber, @ErrorSeverity, @ErrorState, @ErrorProcedure, @ErrorLine, @ErrorMessage);

        RETURN (1)
    END CATCH    
END
GO

DECLARE @prop_name sysname
DECLARE @new_value sql_variant
DECLARE @old_value sql_variant
SET @prop_name = 'Microsoft_DataCollector_MDW_Version'
SET @new_value = '12.0.2000.8'     -- This will be replaced at build time with the sql build number

SELECT @old_value = value
FROM fn_listextendedproperty(@prop_name, NULL, NULL, NULL, NULL, NULL, NULL)

IF (@old_value IS NOT NULL)
BEGIN
    DECLARE @order int
    DECLARE @old_value_char nvarchar(100)
    DECLARE @new_value_char nvarchar(100)
    SET @old_value_char = CONVERT(nvarchar(100), @old_value)
    SET @new_value_char = CONVERT(nvarchar(100), @new_value)

    -- check that we are not downgrading the database
    IF (@old_value_char <> '12.0.2000.8')   -- value only used during development
    BEGIN
        EXEC #sp_compare_builds @old_value_char, @new_value_char, @order OUTPUT
        
        IF (@order > 0) -- the old build version is older than the new, abort the script and kill the connection
        BEGIN
            -- Put the database back into multi user mode
            RAISERROR('', 0, 1)  WITH NOWAIT;
            RAISERROR('Restoring database to multi user mode before aborting the script', 0, 1)  WITH NOWAIT;
            DECLARE @dbname sysname
            SET @dbname = QUOTENAME(DB_NAME())

            DECLARE @sql_db_multi_mode nvarchar(256)
            SET @sql_db_multi_mode = 'ALTER DATABASE ' + @dbname +
                                                ' SET MULTI_USER WITH ROLLBACK IMMEDIATE'
            EXEC sp_executesql @sql_db_multi_mode

            RAISERROR(14714, 21, 1, @old_value_char, @new_value_char) WITH LOG
        END
    END
    EXEC sp_dropextendedproperty @name = @prop_name
END            

EXEC sp_addextendedproperty
        @name = @prop_name,
        @value = @new_value
GO

DROP PROCEDURE #sp_compare_builds


/*
Procedure [#create_or_alter_primary_key_or_index]

Helper proc to create/update primary key constraints and nonclustered/clustered indexes. 
Avoids the need to duplicate object definition in separate creation and upgrade sections. 
Prevents cluttering up the script with repetitive logic to repeat these tasks every time 
we need to define an index or a primary key: 
  - Check for an existing object and skip the create if appropriate
  - Compare the definition of the existing object (clustered vs. nonclustered, number and 
    order of index key columns, ignore_dup_key bit, included vs. key columns, asc/desc 
    sort direction) to the desired index or constraint definition
  - Drop the existing constraint/index if it is malformed or out-of-date 
  - Drop nonclustered indexes before dropping clustered indexes
  - Drop referencing foreign keys before dropping a primary key
 
Parameters: 
    @table_schema - e.g. "snapshots"
    @table_name - e.g. "query_stats"
    @object_type - either "PRIMARY KEY" or "INDEX"
    @constraint_or_index_name - the PK or index name
    @ignore_dup_key - 1 to enable the IGNORE_DUP_KEY index option (default 0)
    @clustered - 1 to create a clustered index/PK (default 1)

The columns in the table are passed to this proc via this temp table: 
    CREATE TABLE #index_key_columns (
        key_ordinal         int IDENTITY, 
        constraint_name     sysname COLLATE CATALOG_DEFAULT, 
        column_name         sysname COLLATE CATALOG_DEFAULT, 
        is_included_column  bit, 
        is_descending_key   bit
    );

Example usage: 
    TRUNCATE TABLE #index_key_columns;
    INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
    VALUES 
        ('IDX_performance_counter_instances1', 'object_name', 0, 0), 
        ('IDX_performance_counter_instances1', 'counter_name', 0, 0), 
        ('IDX_performance_counter_instances1', 'instance_name', 1, 0);
    GO

    EXEC #create_or_alter_primary_key_or_index
        @table_schema = 'snapshots', @table_name = 'performance_counter_instances', 
        @object_type = 'INDEX', @constraint_or_index_name = 'IDX_performance_counter_instances1', 
        @ignore_dup_key = 0, @clustered = 0;
    GO

The TRUNCATE is typically not necessary, but is a good practice just in case two 
indexes have the same name. 
*/
IF OBJECT_ID ('tempdb..#create_or_alter_primary_key_or_index') IS NOT NULL 
BEGIN
    DROP PROC #create_or_alter_primary_key_or_index 
END;
GO
CREATE PROC #create_or_alter_primary_key_or_index 
    @table_schema sysname, 
    @table_name sysname, 
    @object_type sysname, 
    @constraint_or_index_name sysname, 
    @ignore_dup_key bit = 0, 
    @clustered bit = 1
AS
BEGIN
    RAISERROR (N'Validating %s [%s] on table [%s].[%s]...', 0, 1, 
        @object_type, @constraint_or_index_name, @table_schema, @table_name) WITH NOWAIT;

    -- Get the list of key columns and included columns that are supposed to be in 
    -- this index/PK constraint
    DECLARE @column_name sysname;
    DECLARE @is_included_column bit;
    DECLARE @is_descending_key bit;
    DECLARE @key_column_list nvarchar(max);
    DECLARE @include_column_list nvarchar(max);
    DECLARE @sql nvarchar(max);
    DECLARE @expected_column_count int;
    SET @key_column_list = '';
    SET @include_column_list = '';
    SET @expected_column_count = 0;
    DECLARE cols INSENSITIVE CURSOR FOR 
    SELECT column_name, is_included_column, is_descending_key
    FROM #index_key_columns
    WHERE constraint_name = @constraint_or_index_name
    ORDER BY key_ordinal ASC;
    
    OPEN cols;
    FETCH NEXT FROM cols INTO @column_name, @is_included_column, @is_descending_key;

    WHILE (@@FETCH_STATUS = 0)
    BEGIN
        IF (@is_included_column = 1)
        BEGIN
            SET @include_column_list = @include_column_list 
                + CASE WHEN LEN (@include_column_list) > 0 THEN ', ' ELSE '' END 
                + QUOTENAME (@column_name)
            IF (@object_type = 'PRIMARY KEY')
            BEGIN
                RAISERROR ('Error in script -- INCLUDEd columns are not allowed in a primary key', 21, 1);
            END
        END
        ELSE BEGIN
            SET @key_column_list = @key_column_list 
                + CASE WHEN LEN (@key_column_list) > 0 THEN ', ' ELSE '' END 
                + QUOTENAME (@column_name)
                + CASE @is_descending_key WHEN 1 THEN ' DESC' ELSE ' ASC' END;
        END;
        SET @expected_column_count = @expected_column_count + 1;
        
        FETCH NEXT FROM cols INTO @column_name, @is_included_column, @is_descending_key;
    END;
    CLOSE cols;
    DEALLOCATE cols;

    -- Compare the object definition to the expected definition
    DECLARE @unexpected_column_count int;
    DECLARE @total_column_count int;
    DECLARE @total_included_count int;
    DECLARE @out_of_order_key_count int;
    DECLARE @wrong_sort_direction_count int;
    DECLARE @wrong_included_column_count int;
    DECLARE @key_ordinal_base int;
    DECLARE @existing_constraint_or_index_name  sysname;
    DECLARE @current_ignore_dup_key bit;
    DECLARE @current_clustered bit;
    DECLARE @table_object_id int;

    -- Get the target table's object id
    SET @table_object_id = OBJECT_ID (QUOTENAME (@table_schema) + '.' + QUOTENAME (@table_name));

    -- Get the indentity value of the first column in this constraint
    SELECT @key_ordinal_base = MIN (key_ordinal) - 1
    FROM #index_key_columns
    WHERE constraint_name = @constraint_or_index_name;

    -- Check the constraint schema against expected schema, note any differences
    SELECT 
        @total_column_count = COUNT(*), 
        @wrong_included_column_count = SUM (
            CASE 
                WHEN ic.is_included_column != ckc.is_included_column THEN 1
                ELSE 0
            END), 
        @wrong_sort_direction_count = SUM (
            CASE 
                WHEN ic.is_descending_key != ckc.is_descending_key AND ckc.is_included_column = 0 THEN 1
                ELSE 0
            END), 
        @unexpected_column_count = SUM (
            CASE
                -- [#index_key_columns].[column_name] will be NULL for any unexpected columns
                WHEN ckc.column_name IS NULL THEN 1
                ELSE 0
            END), 
        @out_of_order_key_count = SUM (
            CASE 
                WHEN ic.key_ordinal != (ckc.key_ordinal - @key_ordinal_base) AND ckc.is_included_column = 0 THEN 1
                ELSE 0
            END), 
        @existing_constraint_or_index_name = MIN (i.name), 
        @current_ignore_dup_key = MAX (CASE WHEN i.[ignore_dup_key] = 1 THEN 1 ELSE 0 END), 
        @current_clustered = MAX (CASE WHEN i.type_desc = 'CLUSTERED' THEN 1 ELSE 0 END)
    FROM sys.indexes AS i
    INNER JOIN sys.index_columns AS ic ON 
        i.[object_id] = ic.[object_id] AND i.index_id = ic.index_id
    INNER JOIN sys.columns AS c ON 
        ic.[object_id] = c.[object_id] AND c.column_id = ic.column_id
    LEFT OUTER JOIN #index_key_columns AS ckc ON 
        c.name = ckc.column_name 
    WHERE 
        i.[object_id] = @table_object_id AND 
        ckc.constraint_name = @constraint_or_index_name AND 
        -- Match index on expected name unless it's a PK, in which case we'll just look for [is_primary_key]=1
        (i.name = @constraint_or_index_name OR (@object_type = 'PRIMARY KEY' AND i.is_primary_key = 1)) AND 
        i.is_hypothetical = 0;

    -- If the constraint doesn't yet exist, or if it exists but is defined incorrectly, we 
    -- need to create it (and may need to drop some related objects before we create it). 
    IF (@total_column_count != @expected_column_count OR 
        @wrong_included_column_count > 0 OR 
        @wrong_sort_direction_count > 0 OR 
        @unexpected_column_count > 0 OR 
        @out_of_order_key_count > 0 OR 
        @current_ignore_dup_key != @ignore_dup_key OR
        @current_clustered != @clustered)
    BEGIN
        -- If this is a clustered index or clustered primary key, drop any NC indexes. 
        -- The calling script will rebuild them later, and if we don't drop them before we 
        -- drop the existing clustered PK/index, we'll end up wasting time by rebuilding 
        -- the nonclustered indexes twice (first when the clustered index is dropped, then 
        -- again when it gets recreated). 
        IF (@clustered = 1)
        BEGIN
            DECLARE @nc_index_name sysname;

            RAISERROR (N'    Dropping nonclustered indexes before dropping the clustered index...', 0, 1) WITH NOWAIT;

            DECLARE nc_indexes INSENSITIVE CURSOR FOR 
            SELECT i.name AS nc_index_name
            FROM sys.indexes AS i
            WHERE i.type_desc = 'NONCLUSTERED' AND i.is_unique_constraint = 0 AND i.is_hypothetical = 0 
                AND i.is_primary_key = 0 AND i.[object_id] = @table_object_id
            OPEN nc_indexes;
            FETCH NEXT FROM nc_indexes INTO @nc_index_name;

            WHILE (@@FETCH_STATUS = 0)
            BEGIN
                SET @sql = N'DROP INDEX ' + QUOTENAME (@table_schema) + '.' + QUOTENAME (@table_name) 
                    + '.' + QUOTENAME (@nc_index_name) + ';';
                RAISERROR (N'    Dropping nonclustered index: %s', 0, 1, @sql) WITH NOWAIT;
                EXEC (@sql);
                FETCH NEXT FROM nc_indexes INTO @nc_index_name;
            END;
            CLOSE nc_indexes;
            DEALLOCATE nc_indexes;
        END;

        -- If this is a primary key referenced by foreign keys, we have to drop the foreign keys before 
        -- we can drop the PK.  We also have to drop the foreign keys if we are about to drop a clustered 
        -- primary key to "make room" for a clustered non-PK index. 
        IF ((@object_type = 'PRIMARY KEY') OR 
            (@object_type = 'INDEX' AND @clustered = 1 AND 
                EXISTS (SELECT * FROM sys.indexes AS i WHERE i.[object_id] = @table_object_id AND i.is_primary_key = 1 AND i.type_desc = 'CLUSTERED')))
        BEGIN
            DECLARE @fk_constraint_name sysname;
            DECLARE @fk_table_schema sysname;
            DECLARE @fk_table_name sysname;

            DECLARE fk_constraints INSENSITIVE CURSOR FOR 
            SELECT 
                OBJECT_SCHEMA_NAME (fk.parent_object_id) AS fk_table_schema,  
                OBJECT_NAME (fk.parent_object_id) AS fk_table_name,
                fk.name AS fk_constraint_name
            FROM sys.foreign_keys AS fk
            WHERE referenced_object_id = @table_object_id
            OPEN fk_constraints;
            FETCH NEXT FROM fk_constraints INTO @fk_table_schema, @fk_table_name, @fk_constraint_name;

            WHILE (@@FETCH_STATUS = 0)
            BEGIN
                RAISERROR (N'    Dropping foreign key [%s].[%s].[%s] because it references table [%s]...', 0, 1, 
                    @fk_table_schema, @fk_table_name, @fk_constraint_name, @table_name) WITH NOWAIT;
                SET @sql = N'ALTER TABLE ' + QUOTENAME (@fk_table_schema) + '.' + QUOTENAME (@fk_table_name) 
                    + ' DROP CONSTRAINT ' + QUOTENAME (@fk_constraint_name) + ';';
                EXEC (@sql);
                FETCH NEXT FROM fk_constraints INTO @fk_table_schema, @fk_table_name, @fk_constraint_name;
            END;
            CLOSE fk_constraints;
            DEALLOCATE fk_constraints;
        END;

        -- Drop the incorrect constraint, if it exists. 
        IF (@total_column_count > 0)
        BEGIN
            RAISERROR (N'    Incorrect existing definition (current %d vs. expected %d key columns, %d wrong included columns, %d wrong sort direction, %d unexpected columns, %d out-of-order columns)', 0, 1, 
                @total_column_count, @expected_column_count, @wrong_included_column_count, @wrong_sort_direction_count, @unexpected_column_count, @out_of_order_key_count) WITH NOWAIT;
                                
            -- Drop the existing malformed constraint or index so that we can recreate it correctly
            IF @object_type = 'PRIMARY KEY'
            BEGIN
                SET @sql = N'ALTER TABLE ' + QUOTENAME (@table_schema) + '.' + QUOTENAME (@table_name) 
                    + ' DROP CONSTRAINT ' + QUOTENAME (@existing_constraint_or_index_name) + ';';
            END
            ELSE BEGIN
                SET @sql = N'DROP INDEX ' + QUOTENAME (@table_schema) + '.' + QUOTENAME (@table_name) 
                    + '.' + QUOTENAME (@existing_constraint_or_index_name) + ';';
            END;
            RAISERROR (N'    Dropping existing object: %s', 0, 1, @sql) WITH NOWAIT;
            EXEC (@sql);
        END;
        
        -- If we are about to create a clustered PK index but a clustered (non-PK) index exists, 
        -- we must drop the clustered index before we can create the clustered PK (or vice versa). 
        -- Example: Table has a clustered index, and we need to convert an existing non-clustered 
        -- PK into a clustered PK. 
        IF (@clustered = 1)
        BEGIN
            DECLARE @is_primary_key bit;
            SET @existing_constraint_or_index_name = NULL;
            SELECT 
                @existing_constraint_or_index_name = i.name, 
                @is_primary_key = is_primary_key
            FROM sys.indexes AS i
            WHERE i.type_desc = 'CLUSTERED' AND i.is_hypothetical = 0 AND i.[object_id] = @table_object_id
            
            IF (@existing_constraint_or_index_name IS NOT NULL)
            BEGIN
                -- Drop the existing clustered index to make way for our clustered PK 
                -- (or drop the existing clustered PK to make way for our clustered index)
                IF (@is_primary_key = 1)
                BEGIN
                    SET @sql = N'ALTER TABLE ' + QUOTENAME (@table_schema) + '.' + QUOTENAME (@table_name) 
                        + ' DROP CONSTRAINT ' + QUOTENAME (@existing_constraint_or_index_name) + ';';
                END
                ELSE BEGIN
                    SET @sql = N'DROP INDEX ' + QUOTENAME (@table_schema) + '.' + QUOTENAME (@table_name) 
                        + '.' + QUOTENAME (@existing_constraint_or_index_name) + ';';
                END;
                RAISERROR (N'    Dropping existing clustered index: %s', 0, 1, @sql) WITH NOWAIT;
                EXEC (@sql);
            END;
        END;

        -- Finally, create the PK or index
        IF @object_type = 'PRIMARY KEY'
        BEGIN
            SET @sql = N'ALTER TABLE ' + QUOTENAME (@table_schema) + '.' + QUOTENAME (@table_name) 
                + ' ADD CONSTRAINT ' + QUOTENAME (@constraint_or_index_name) + ' PRIMARY KEY ' 
                + CASE @clustered WHEN 1 THEN 'CLUSTERED' ELSE 'NONCLUSTERED' END 
                + ' (' + @key_column_list + ')'
                + CASE @ignore_dup_key WHEN 1 THEN ' WITH (IGNORE_DUP_KEY=ON)' ELSE '' END 
                + ';';
        END
        ELSE BEGIN
            SET @sql = N'CREATE ' + CASE @clustered WHEN 1 THEN 'CLUSTERED' ELSE 'NONCLUSTERED' END 
                + ' INDEX ' + QUOTENAME (@constraint_or_index_name) + ' ON ' 
                + QUOTENAME (@table_schema) + '.' + QUOTENAME (@table_name) 
                + ' (' + @key_column_list + ')'
                + CASE WHEN LEN (@include_column_list) > 0 THEN ' INCLUDE (' + @include_column_list + ') ' ELSE '' END 
                + CASE @ignore_dup_key WHEN 1 THEN ' WITH (IGNORE_DUP_KEY=ON)' ELSE '' END 
                + ';';
        END;
        RAISERROR (N'    Creating new object: %s', 0, 1, @sql) WITH NOWAIT;
        EXEC (@sql);
    END;
END;
GO

-- Create the temp table used to pass column lists to helper proc [#create_or_alter_primary_key_or_index]
IF OBJECT_ID ('tempdb..#index_key_columns') IS NOT NULL
BEGIN
    DROP TABLE #index_key_columns;
END;
GO
CREATE TABLE #index_key_columns (
    key_ordinal         int IDENTITY, 
    constraint_name     sysname COLLATE CATALOG_DEFAULT, 
    column_name         sysname COLLATE CATALOG_DEFAULT, 
    is_included_column  bit, 
    is_descending_key   bit
);
GO


-- Start a transaction
BEGIN TRANSACTION InstMdwSql
GO


/**********************************************************************/
/* UPGRADE SECTION - UPDATE EXISTING OBJECTS                          */
/**********************************************************************/

--
-- >>> CTP5 -> CTP6 Upgrade
--

-- Update notable_query_plan
IF (OBJECT_ID(N'snapshots.notable_query_plan', 'U') IS NOT NULL)
BEGIN
    RAISERROR ('Updating table [snapshots].[notable_query_plan]...', 0, 1) WITH NOWAIT;

    -- CTP6 added the [plan_generation_num] column to this table
    IF NOT EXISTS (SELECT column_id FROM sys.all_columns c WHERE c.name = N'plan_generation_num' AND c.object_id = OBJECT_ID(N'snapshots.notable_query_plan', 'U'))
    BEGIN
        ALTER TABLE [snapshots].[notable_query_plan]
            ADD [plan_generation_num] bigint DEFAULT 0 NOT NULL;
    END;
END;
GO

-- Update active_sessions_and_requests
-- Add a column
IF (OBJECT_ID(N'snapshots.active_sessions_and_requests', 'U') IS NOT NULL)
BEGIN
    RAISERROR ('Updating table [snapshots].[active_sessions_and_requests]...', 0, 1) WITH NOWAIT;

    IF NOT EXISTS (SELECT column_id FROM sys.all_columns c WHERE c.name = N'is_blocking' AND c.object_id = OBJECT_ID(N'snapshots.active_sessions_and_requests', 'U'))
    BEGIN
        ALTER TABLE [snapshots].[active_sessions_and_requests] 
            ADD [is_blocking] bit DEFAULT 0 NOT NULL
    END;

    -- Alter column length for [command] as 32; column : [command] sys.dm_exec_requests in SQL 11 is of type nvarchar(32)
    IF EXISTS (SELECT c.system_type_id FROM sys.columns c WHERE c.object_id = OBJECT_ID('snapshots.active_sessions_and_requests', 'U') AND c.name = N'command')
    BEGIN
        RAISERROR ('Updating [command] column length to 32...', 0, 1) WITH NOWAIT;
        
        ALTER TABLE  [snapshots].[active_sessions_and_requests] 
            ALTER COLUMN [command] nvarchar(32) NULL;
    END;
	
	-- Alter column length for [host_name] as 128; column : [host_name] sys.dm_exec_requests in SQL 11 is of type nvarchar(128)
    IF EXISTS (SELECT c.system_type_id FROM sys.columns c WHERE c.object_id = OBJECT_ID('snapshots.active_sessions_and_requests', 'U') 
				AND c.name = N'host_name' AND c.max_length < 128)
    BEGIN
        RAISERROR ('Updating [host_name] column length to 128...', 0, 1) WITH NOWAIT;
        
        ALTER TABLE  [snapshots].[active_sessions_and_requests] 
            ALTER COLUMN [host_name] nvarchar(128) NOT NULL;
    END;

	-- Alter column length for [program_name] as 128; column : [program_name] sys.dm_exec_requests in SQL 11 is of type nvarchar(128)
    IF EXISTS (SELECT c.system_type_id FROM sys.columns c WHERE c.object_id = OBJECT_ID('snapshots.active_sessions_and_requests', 'U') 
				AND c.name = N'program_name' AND c.max_length < 128)
    BEGIN
        RAISERROR ('Updating [program_name] column length to 128...', 0, 1) WITH NOWAIT;
        
        ALTER TABLE  [snapshots].[active_sessions_and_requests] 
            ALTER COLUMN [program_name] nvarchar(128) NOT NULL;
    END;
	
END;
GO


-- Update performance_counter_instances
-- Add Unique constraint for [path] column
-- This constraint creation generates a warning. We will deal with the warning when fix for Defect# 130259 is provided
IF (OBJECT_ID(N'snapshots.performance_counter_instances', 'U') IS NOT NULL)
BEGIN
    RAISERROR ('Updating table [snapshots].[performance_counter_instances]...', 0, 1) WITH NOWAIT;
    
    IF NOT EXISTS (SELECT index_id FROM sys.indexes WHERE object_id = OBJECT_ID('snapshots.performance_counter_instances', 'U') AND name = N'UN_performance_counter_path')
    BEGIN
        ALTER TABLE [snapshots].[performance_counter_instances] 
            ADD CONSTRAINT [UN_performance_counter_path] UNIQUE
            (
                [path]
            ) WITH (IGNORE_DUP_KEY = ON)
            ON [PRIMARY];
    END;
END;
GO

-- Update performance_counter_instances
IF (OBJECT_ID(N'snapshots.performance_counter_values', 'U') IS NOT NULL)
BEGIN
    RAISERROR ('Updating table [snapshots].[performance_counter_values]...', 0, 1) WITH NOWAIT;

    IF EXISTS (SELECT c.system_type_id FROM sys.columns c WHERE c.object_id = OBJECT_ID('snapshots.performance_counter_values', 'U') AND c.name = N'formatted_value' AND c.system_type_id = 127)
    BEGIN
        RAISERROR ('Changing [formatted_value] data type to float...', 0, 1) WITH NOWAIT;
        
        -- This index will be recreated later (after the table definition)
        IF EXISTS (SELECT * FROM sys.indexes AS i WHERE i.name = 'IDX_performance_counter_values1' AND [object_id] = OBJECT_ID ('snapshots.performance_counter_values'))
        BEGIN
            DROP INDEX [IDX_performance_counter_values1] ON [snapshots].[performance_counter_values];
        END;
       
        ALTER TABLE [snapshots].[performance_counter_values]
            ALTER COLUMN [formatted_value] float NOT NULL;
    END;
END;
GO

-- Change int log_id columns to bigint 
IF EXISTS (
    SELECT * 
    FROM sys.columns AS c
    INNER JOIN sys.types AS t ON c.system_type_id = t.system_type_id
    WHERE [object_id] = OBJECT_ID ('core.snapshots_internal')
        AND c.name = 'log_id' AND t.name = 'int'
    )
BEGIN
    RAISERROR ('Changing [core].[snapshots_internal].[log_id] column datatype from int to bigint...', 0, 1) WITH NOWAIT;
    ALTER TABLE [core].[snapshots_internal] ALTER COLUMN [log_id] bigint NOT NULL;
END;
GO
IF (OBJECT_ID ('core.snapshots') IS NOT NULL AND OBJECT_ID ('core.snapshots_internal') IS NOT NULL)
BEGIN
    -- Refresh the view to ensure that it reflects the datatype change
    EXEC sp_refreshview 'core.snapshots'
END
GO

--
-- >>> CTP6 -> CTP6 Refresh/RC0 Upgrade
--
-- Our query stats data collection query merges query stats from sys.dm_exec_query_stats (completed query stats) 
-- and sys.dm_exec_requests (in-progress query stats).  We may not have access to all query stats for any queries 
-- that were only visible as in-progress queries in dm_exec_requests.  For example, this DMV doesn't expose CLR 
-- stats. Some of the columns in our query_stats table must therefore tolerate NULLs. 
IF EXISTS (
    SELECT c.* 
    FROM INFORMATION_SCHEMA.TABLES AS t
    INNER JOIN INFORMATION_SCHEMA.COLUMNS AS c ON c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME 
    WHERE t.TABLE_SCHEMA = 'snapshots' AND t.TABLE_NAME = 'query_stats'
        AND c.COLUMN_NAME = 'snapshot_clr_time' AND c.IS_NULLABLE = 'NO'
)
BEGIN
    RAISERROR ('Making [snapshots].[query_stats] columns NULLable...', 0, 1) WITH NOWAIT;
    ALTER TABLE snapshots.query_stats ALTER COLUMN min_clr_time bigint NULL;
    ALTER TABLE snapshots.query_stats ALTER COLUMN max_clr_time bigint NULL;
    ALTER TABLE snapshots.query_stats ALTER COLUMN total_clr_time bigint NULL;
    ALTER TABLE snapshots.query_stats ALTER COLUMN snapshot_clr_time bigint NULL;
    ALTER TABLE snapshots.query_stats ALTER COLUMN min_worker_time bigint NULL;
    ALTER TABLE snapshots.query_stats ALTER COLUMN min_physical_reads bigint NULL;
    ALTER TABLE snapshots.query_stats ALTER COLUMN min_logical_writes bigint NULL;
    ALTER TABLE snapshots.query_stats ALTER COLUMN min_logical_reads bigint NULL;
    ALTER TABLE snapshots.query_stats ALTER COLUMN min_elapsed_time bigint NULL;
END;
GO

-- Add the [snapshot_execution_count] column (new to CTP6 Refresh) to [query_stats]
IF OBJECT_ID ('snapshots.query_stats', 'U') IS NOT NULL AND NOT EXISTS (
    SELECT c.* 
    FROM INFORMATION_SCHEMA.TABLES AS t
    INNER JOIN INFORMATION_SCHEMA.COLUMNS AS c ON c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME 
    WHERE t.TABLE_SCHEMA = 'snapshots' AND t.TABLE_NAME = 'query_stats'
        AND c.COLUMN_NAME = 'snapshot_execution_count'
)
BEGIN
    RAISERROR ('Adding [snapshot_execution_count] column to [snapshots].[query_stats]...', 0, 1) WITH NOWAIT;
    ALTER TABLE snapshots.query_stats ADD [snapshot_execution_count] bigint NULL;
END;
GO

/**********************************************************************/
/* CORE SCHEMA                                                        */
/**********************************************************************/
RAISERROR('', 0, 1)  WITH NOWAIT;
RAISERROR('Create schema core...', 0, 1)  WITH NOWAIT;
GO
IF (SCHEMA_ID('core') IS NULL)
BEGIN
    DECLARE @sql nvarchar(128)
    SET @sql = 'CREATE SCHEMA core'
    EXEC sp_executesql @sql
END
GO

-- SUPPORTED_COLLECTOR_TYPES
--
RAISERROR('', 0, 1)  WITH NOWAIT;
GO
IF (OBJECT_ID(N'[core].[supported_collector_types_internal]', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [core].[supported_collector_types_internal]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [core].[supported_collector_types_internal] (
        collector_type_uid            uniqueidentifier NOT NULL,
    ) ON [PRIMARY]

END
GO

-- supported_collector_types_internal.PK_supported_collector_types_internal
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_supported_collector_types_internal', 'collector_type_uid', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'core', @table_name = 'supported_collector_types_internal', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_supported_collector_types_internal', 
    @ignore_dup_key = 0, @clustered = 1;
GO


IF (NOT OBJECT_ID(N'core.supported_collector_types', 'V') IS NULL)
BEGIN
    RAISERROR('Dropping view [core].[supported_collector_types]...', 0, 1)  WITH NOWAIT;
    DROP VIEW core.supported_collector_types
END
GO

RAISERROR('Creating view [core].[supported_collector_types]...', 0, 1)  WITH NOWAIT;
GO
CREATE VIEW core.supported_collector_types
AS 
    SELECT collector_type_uid
    FROM core.supported_collector_types_internal
GO

-- SOURCE_INFO
--
IF (OBJECT_ID(N'[core].[source_info_internal]', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [core].[source_info_internal]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [core].[source_info_internal] (
        source_id                   int IDENTITY NOT NULL,
        collection_set_uid          uniqueidentifier NOT NULL,  -- GUID of the collection set that loads the data
        instance_name               sysname COLLATE Latin1_General_CI_AI NOT NULL, -- the name of the machine the data is uploaded from
        days_until_expiration       smallint NOT NULL,          -- how many days data from this source should be kept in the warehouse. 0 indicates forever
        operator                    sysname NOT NULL,           -- login name of the principal who is uploading the data
        
        CONSTRAINT [UQ_collection_set_uid_instance_name] UNIQUE (collection_set_uid, instance_name, operator)
    ) ON [PRIMARY]
END
GO

-- source_info_internal.PK_source_info_internal
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_source_info_internal', 'source_id', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'core', @table_name = 'source_info_internal', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_source_info_internal', 
    @ignore_dup_key = 0, @clustered = 1;
GO


-- SNAPSHOT_TIMETABLE
--
IF (OBJECT_ID(N'[core].[snapshot_timetable_internal]', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [core].[snapshot_timetable_internal]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [core].[snapshot_timetable_internal] (
        snapshot_time_id            int IDENTITY NOT NULL,
        snapshot_time                datetimeoffset(7) NOT NULL,
            ) ON [PRIMARY]
END
GO

-- snapshot_timetable_internal.PK_snapshots_timetable_internal
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_snapshots_timetable_internal', 'snapshot_time_id', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'core', @table_name = 'snapshot_timetable_internal', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_snapshots_timetable_internal', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- snapshot_timetable_internal.IDX_snapshot_time
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('IDX_snapshot_time', 'snapshot_time', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'core', @table_name = 'snapshot_timetable_internal', 
    @object_type = 'INDEX', @constraint_or_index_name = 'IDX_snapshot_time', 
    @ignore_dup_key = 0, @clustered = 0;
GO


-- SNAPSHOTS
--
IF (OBJECT_ID(N'[core].[snapshots_internal]', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [core].[snapshots_internal]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [core].[snapshots_internal] (
        snapshot_id                    int IDENTITY NOT NULL,
        snapshot_time_id               int NOT NULL,
        source_id                      int NOT NULL,
        log_id                         bigint NOT NULL,             -- reference to the log table
    ) ON [PRIMARY]

    CREATE STATISTICS [STAT_snapshots_internal2] ON [core].[snapshots_internal](
        [snapshot_time_id], 
        [source_id]
    )

    CREATE STATISTICS [STAT_snapshots_internal3] ON [core].[snapshots_internal](
        [snapshot_time_id], [snapshot_id], [source_id])

END
GO

-- snapshots_internal.PK_snapshots_internal
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_snapshots_internal', 'snapshot_id', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'core', @table_name = 'snapshots_internal', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_snapshots_internal', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- snapshots_internal.IDX_snapshot_time_id
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('IDX_snapshot_time_id', 'snapshot_time_id', 0, 0), 
    ('IDX_snapshot_time_id', 'source_id', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'core', @table_name = 'snapshots_internal', 
    @object_type = 'INDEX', @constraint_or_index_name = 'IDX_snapshot_time_id', 
    @ignore_dup_key = 0, @clustered = 0;
GO

-- snapshots_internal.FK_snapshots_source_info
IF OBJECT_ID ('core.FK_snapshots_source_info', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_snapshots_source_info] on core.snapshots_internal ...', 0, 1) WITH NOWAIT;
    ALTER TABLE core.snapshots_internal
        ADD CONSTRAINT [FK_snapshots_source_info] FOREIGN KEY(source_id)
        REFERENCES core.source_info_internal (source_id)
END;
GO

-- snapshots_internal.FK_snapshots_snapshots_timetable
IF OBJECT_ID ('core.FK_snapshots_snapshots_timetable', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_snapshots_snapshots_timetable] on core.snapshots_internal ...', 0, 1) WITH NOWAIT;
    ALTER TABLE core.snapshots_internal
        ADD CONSTRAINT [FK_snapshots_snapshots_timetable] FOREIGN KEY(snapshot_time_id)
        REFERENCES core.snapshot_timetable_internal (snapshot_time_id)
END;
GO


-- SNAPSHOTS VIEW
--
IF (NOT OBJECT_ID(N'core.snapshots', 'V') IS NULL)
BEGIN
    RAISERROR('Dropping view [core].[snapshots]...', 0, 1)  WITH NOWAIT;
    DROP VIEW core.snapshots
END
GO

RAISERROR('Creating view [core].[snapshots]...', 0, 1)  WITH NOWAIT;
GO
CREATE VIEW core.snapshots
AS 
    SELECT 
        s.source_id,
        s.snapshot_id, 
        s.snapshot_time_id,
        t.snapshot_time, 
        CASE src.days_until_expiration
            WHEN 0 THEN NULL
            ELSE DATEADD(DAY, src.days_until_expiration, t.snapshot_time)
        END AS valid_through,
        src.instance_name, 
        src.collection_set_uid, 
        src.operator,
        s.log_id
    FROM core.source_info_internal src, core.snapshots_internal s, core.snapshot_timetable_internal t
    WHERE src.source_id = s.source_id AND s.snapshot_time_id = t.snapshot_time_id
GO

--
-- This stored proc creates a snapshot entry and return the id
--
IF (NOT OBJECT_ID(N'core.sp_create_snapshot', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [core].[sp_create_snapshot] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [core].[sp_create_snapshot]
END
GO 

RAISERROR('Creating procedure [core].[sp_create_snapshot] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE core.sp_create_snapshot
    @collection_set_uid     uniqueidentifier,
    @collector_type_uid     uniqueidentifier,
    @machine_name           sysname,
    @named_instance         sysname,
    @log_id                 bigint,
    @snapshot_id            int OUTPUT
AS 
BEGIN
    SET NOCOUNT ON
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

    -- Security check (role membership)
    IF (NOT (ISNULL(IS_MEMBER(N'mdw_writer'), 0) = 1) AND NOT (ISNULL(IS_SRVROLEMEMBER(N'sysadmin'), 0) = 1))
    BEGIN
        RAISERROR(14677, 16, -1, 'mdw_writer')
        RETURN(1) -- Failure
    END
    
    DECLARE @operator sysname;
    SELECT @operator = SUSER_SNAME();

    DECLARE @instance_name sysname
    SET @named_instance = NULLIF(RTRIM(LTRIM(@named_instance)), N'')
    IF (@named_instance = 'MSSQLSERVER')
        SET @instance_name = @machine_name
    ELSE
        SET @instance_name = @machine_name + N'\' + @named_instance

    -- Parameters check
    -- Find the source_id that matches the requested collection set and operator
    DECLARE @source_id  int
    SET @source_id = (SELECT source_id 
                        FROM core.source_info_internal
                        WHERE collection_set_uid = @collection_set_uid 
                          AND operator = @operator
                          AND instance_name = @instance_name)

    IF(@source_id IS NULL)
    BEGIN
        DECLARE @collection_set_uid_as_char NVARCHAR(36)
        SELECT @collection_set_uid_as_char = CONVERT(NVARCHAR(36), @collection_set_uid)
        RAISERROR(14679, -1, -1, N'@collection_set_uid', @collection_set_uid_as_char)
    END

    -- Make sure the collector_type is registered in this warehouse
    IF NOT EXISTS (SELECT collector_type_uid FROM core.supported_collector_types WHERE collector_type_uid = @collector_type_uid)
    BEGIN
        DECLARE @collector_type_uid_as_char NVARCHAR(36)
        SELECT @collector_type_uid_as_char = CONVERT(NVARCHAR(36), @collector_type_uid)
        RAISERROR(14679, -1, -1, N'@collector_type_uid', @collector_type_uid_as_char)
    END

    -- Get the snapshot time
    BEGIN TRY
        BEGIN TRAN
        DECLARE @snapshot_time_id int

        IF NOT EXISTS (SELECT snapshot_time_id FROM core.snapshot_timetable_internal WITH(UPDLOCK) WHERE snapshot_time > DATEADD (minute, -1, SYSDATETIMEOFFSET()))
        BEGIN
            INSERT INTO core.snapshot_timetable_internal
            (
                snapshot_time
            )
            VALUES
            (
                SYSDATETIMEOFFSET()
            )
            SET @snapshot_time_id = SCOPE_IDENTITY()
        END
        ELSE
        BEGIN
            SET @snapshot_time_id = (SELECT MAX(snapshot_time_id) FROM core.snapshot_timetable_internal)
        END

        -- Finally insert an entry into snapshots table
        INSERT INTO core.snapshots_internal
        (
            snapshot_time_id,
            source_id,
            log_id
        )
        VALUES
        (
            @snapshot_time_id,
            @source_id,
            @log_id
        )
        SET @snapshot_id = SCOPE_IDENTITY()

        IF (@snapshot_id IS NULL)
        BEGIN
            RAISERROR(14262, -1, -1, '@snapshot_id', @snapshot_id)
            RETURN(1)
        END
        ELSE
        BEGIN
            COMMIT TRAN
        END
    END TRY
    BEGIN CATCH
        IF (@@TRANCOUNT > 0) 
            ROLLBACK TRANSACTION

        -- Rethrow the error
        DECLARE @ErrorMessage   NVARCHAR(4000);
        DECLARE @ErrorSeverity  INT;
        DECLARE @ErrorState     INT;
        DECLARE @ErrorNumber    INT;
        DECLARE @ErrorLine      INT;
        DECLARE @ErrorProcedure NVARCHAR(200);
        SELECT @ErrorLine = ERROR_LINE(),
               @ErrorSeverity = ERROR_SEVERITY(),
               @ErrorState = ERROR_STATE(),
               @ErrorNumber = ERROR_NUMBER(),
               @ErrorMessage = ERROR_MESSAGE(),
               @ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-');

        RAISERROR (14684, -1, -1 , @ErrorNumber, @ErrorSeverity, @ErrorState, @ErrorProcedure, @ErrorLine, @ErrorMessage);
    END CATCH

END
GO

--
-- This stored proc updates data in source_info table 
--
IF (NOT OBJECT_ID(N'core.sp_update_data_source', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [core].[sp_update_data_source] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [core].[sp_update_data_source]
END
GO 

RAISERROR('Creating procedure [core].[sp_update_data_source] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [core].[sp_update_data_source]
    @collection_set_uid     uniqueidentifier,
    @machine_name           sysname,
    @named_instance         sysname,
    @days_until_expiration  smallint,
    @source_id              int OUTPUT
AS
BEGIN
    SET NOCOUNT ON
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

    -- Security check (role membership)
    IF (NOT (ISNULL(IS_MEMBER(N'mdw_writer'), 0) = 1) AND NOT (ISNULL(IS_SRVROLEMEMBER(N'sysadmin'), 0) = 1))
    BEGIN
        RAISERROR(14677, 16, -1, 'mdw_writer')
        RETURN(1) -- Failure
    END

    -- Parameters check
    IF (@collection_set_uid IS NULL)
    BEGIN
        RAISERROR(14200, -1, -1, '@collection_set_uid')
        RETURN(1) -- Failure
    END

    SET @machine_name = NULLIF(RTRIM(LTRIM(@machine_name)), N'')
    IF (@machine_name IS NULL)
    BEGIN
        RAISERROR(14200, -1, -1, '@machine_name')
        RETURN(1) -- Failure
    END

    DECLARE @instance_name sysname
    SET @named_instance = NULLIF(RTRIM(LTRIM(@named_instance)), N'')
    IF (@named_instance = 'MSSQLSERVER')
        SET @instance_name = @machine_name
    ELSE
        SET @instance_name = @machine_name + N'\' + @named_instance

    IF (@days_until_expiration IS NULL)
    BEGIN
        RAISERROR(14200, -1, -1, '@days_until_expiration')
        RETURN(1) -- Failure
    END

    IF (@days_until_expiration < 0)
    BEGIN
        RAISERROR(14266, -1, -1, '@days_until_expiration', ' >= 0')
        RETURN (1) -- Failure
    END
    
    DECLARE @operator sysname
    SELECT @operator = SUSER_SNAME()

    BEGIN TRY
        BEGIN TRAN
        
        -- Insert data into the table
        -- We specify the lock hint, in order to keep the exlusive lock on the row from the moment we select it till we 
        -- update it
        SET @source_id = (SELECT source_id FROM core.source_info_internal WITH(UPDLOCK) WHERE collection_set_uid = @collection_set_uid AND instance_name = @instance_name AND operator = @operator)
        IF @source_id IS NULL
        BEGIN
            INSERT INTO core.source_info_internal
            (
                collection_set_uid,
                instance_name,
                days_until_expiration,
                operator
            )
            VALUES
            (
                @collection_set_uid,
                @instance_name,
                @days_until_expiration,
                @operator
            )
            SET @source_id = SCOPE_IDENTITY()
        END
        ELSE
        BEGIN
            UPDATE core.source_info_internal
            SET 
                days_until_expiration = @days_until_expiration
            WHERE source_id = @source_id;
        END
            
        COMMIT TRAN
        RETURN (0)
    END TRY
    BEGIN CATCH
        IF (@@TRANCOUNT > 0) 
            ROLLBACK TRANSACTION

        -- Rethrow the error
        DECLARE @ErrorMessage   NVARCHAR(4000);
        DECLARE @ErrorSeverity  INT;
        DECLARE @ErrorState     INT;
        DECLARE @ErrorNumber    INT;
        DECLARE @ErrorLine      INT;
        DECLARE @ErrorProcedure NVARCHAR(200);
        SELECT @ErrorLine = ERROR_LINE(),
               @ErrorSeverity = ERROR_SEVERITY(),
               @ErrorState = ERROR_STATE(),
               @ErrorNumber = ERROR_NUMBER(),
               @ErrorMessage = ERROR_MESSAGE(),
               @ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-');

        RAISERROR (14684, -1, -1 , @ErrorNumber, @ErrorSeverity, @ErrorState, @ErrorProcedure, @ErrorLine, @ErrorMessage);
        RETURN (1)
    END CATCH
END
GO


--
-- This stored proc adds new entry in supported_collector_types table
--
IF (NOT OBJECT_ID(N'core.sp_add_collector_type', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [core].[sp_add_collector_type] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [core].[sp_add_collector_type]
END
GO 

RAISERROR('Creating procedure [core].[sp_add_collector_type] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [core].[sp_add_collector_type]
    @collector_type_uid     uniqueidentifier
AS
BEGIN
    -- Security check (role membership)
    IF (NOT (ISNULL(IS_MEMBER(N'mdw_admin'), 0) = 1) AND NOT (ISNULL(IS_SRVROLEMEMBER(N'sysadmin'), 0) = 1))
    BEGIN
        RAISERROR(14677, 16, -1, 'mdw_admin')
        RETURN(1) -- Failure
    END

    -- Parameters check
    IF (@collector_type_uid IS NULL)
    BEGIN
        RAISERROR(14200, -1, -1, '@collector_type_uid')
        RETURN(1) -- Failure
    END

    -- Insert new collector type
    IF NOT EXISTS (SELECT collector_type_uid FROM core.supported_collector_types WHERE collector_type_uid = @collector_type_uid)
    BEGIN
        INSERT INTO core.supported_collector_types
        (
            collector_type_uid
        )
        VALUES
        (
            @collector_type_uid
        )
    END
    ELSE
    BEGIN
        -- Raise an info message, but do not fail
        DECLARE @collector_type_uid_as_char NVARCHAR(36)
        SELECT @collector_type_uid_as_char = CONVERT(NVARCHAR(36), @collector_type_uid)
        RAISERROR(14261, 10, -1, '@collector_type_uid', @collector_type_uid_as_char)
    END

    RETURN (0)
END
GO

--
-- This stored proc removes and existing entry from supported_collector_types table
--
IF (NOT OBJECT_ID(N'core.sp_remove_collector_type', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [core].[sp_remove_collector_type] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [core].[sp_remove_collector_type]
END
GO 

RAISERROR('Creating procedure [core].[sp_remove_collector_type] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [core].[sp_remove_collector_type]
    @collector_type_uid     uniqueidentifier
AS
BEGIN
    -- Security check (role membership)
    IF (NOT (ISNULL(IS_MEMBER(N'mdw_admin'), 0) = 1) AND NOT (ISNULL(IS_SRVROLEMEMBER(N'sysadmin'), 0) = 1))
    BEGIN
        RAISERROR(14677, 16, -1, 'mdw_admin')
        RETURN(1) -- Failure
    END

    -- Parameters check
    IF (@collector_type_uid IS NULL)
    BEGIN
        RAISERROR(14200, -1, -1, '@collector_type_uid')
        RETURN(1) -- Failure
    END

    -- Delete collector type
    IF EXISTS (SELECT collector_type_uid FROM core.supported_collector_types WHERE collector_type_uid = @collector_type_uid)
    BEGIN
        DELETE FROM core.supported_collector_types WHERE collector_type_uid = @collector_type_uid
    END
    ELSE
    BEGIN
        DECLARE @collector_type_uid_as_char NVARCHAR(36)
        SELECT @collector_type_uid_as_char = CONVERT(NVARCHAR(36), @collector_type_uid)
        RAISERROR(14262, -1, -1, '@collector_type_uid', @collector_type_uid_as_char)
        RETURN (1) -- Failure
    END

    RETURN (0)
END
GO

--
-- This table is used to determine status of current purge operation.
-- Contains records only if a sp_stop_purge operation was requested.
--
RAISERROR('', 0, 1)  WITH NOWAIT;
GO
IF (OBJECT_ID(N'[core].[purge_info_internal]', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [core].[purge_info_internal]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [core].[purge_info_internal] (
      stop_purge bit NOT NULL,
    ) ON [PRIMARY]

END
GO


--
-- This stored proc removes orphaned notable_query_plan data from the warehouse 
--
IF (NOT OBJECT_ID(N'core.sp_purge_orphaned_notable_query_plan', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [core].[sp_purge_orphaned_notable_query_plan] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [core].[sp_purge_orphaned_notable_query_plan]
END
GO 

RAISERROR('Creating procedure [core].[sp_purge_orphaned_notable_query_plan] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [core].[sp_purge_orphaned_notable_query_plan]
    @duration smallint = NULL,
    @end_time datetime = NULL,
    @delete_batch_size int = 500
AS
BEGIN
    PRINT 'Begin purging orphaned records in snapshots.notable_query_plan Current UTC Time:' + CONVERT(VARCHAR, GETUTCDATE())

    DECLARE @stop_purge int

    -- Delete orphaned rows from snapshots.notable_query_plan.  Query plans are not deleted by the generic purge 
    -- process that deletes other data (above) because query plan rows are not tied to a particular snapshot ID. 
    -- Purging query plans table  as a special case, by looking for plans that 
    -- are no longer referenced by any of the rows in the snapshots.query_stats table.  We need to delete these 
    -- rows in small chunks, since deleting many GB in a single delete statement would cause lock escalation and 
    -- an explosion in the size of the transaction log (individual query plans can be 10-50MB).  
    DECLARE @rows_affected int;
    -- set expected rows affected as delete batch size
    SET @rows_affected = @delete_batch_size;
    
    -- select set of orphaned query plans to be deleted into a temp table 
    SELECT qp.[sql_handle],
        qp.plan_handle,
        qp.plan_generation_num,
        qp.statement_start_offset,
        qp.statement_end_offset,
        qp.creation_time
    INTO #tmp_notable_query_plan
    FROM snapshots.notable_query_plan AS qp 
    WHERE NOT EXISTS (
        SELECT snapshot_id 
        FROM snapshots.query_stats AS qs
        WHERE qs.[sql_handle] = qp.[sql_handle] AND qs.plan_handle = qp.plan_handle 
            AND qs.plan_generation_num = qp.plan_generation_num 
            AND qs.statement_start_offset = qp.statement_start_offset 
            AND qs.statement_end_offset = qp.statement_end_offset 
            AND qs.creation_time = qp.creation_time)

    WHILE (@rows_affected = @delete_batch_size)
    BEGIN
        -- Deleting TOP N orphaned rows in query plan table by joining info from temp table variable
        -- This is done to speed up delete query. 
        DELETE TOP (@delete_batch_size) snapshots.notable_query_plan 
        FROM snapshots.notable_query_plan AS qp , #tmp_notable_query_plan AS tmp
        WHERE tmp.[sql_handle] = qp.[sql_handle] 
            AND tmp.plan_handle = qp.plan_handle 
            AND tmp.plan_generation_num = qp.plan_generation_num 
            AND tmp.statement_start_offset = qp.statement_start_offset 
            AND tmp.statement_end_offset = qp.statement_end_offset 
            AND tmp.creation_time = qp.creation_time
        
        SET @rows_affected = @@ROWCOUNT;
        IF(@rows_affected > 0)
        BEGIN
            RAISERROR ('Deleted %d orphaned rows from snapshots.notable_query_plan', 0, 1, @rows_affected) WITH NOWAIT;
        END

        -- Check if the execution of the stored proc exceeded the @duration specified
        IF (@duration IS NOT NULL)
        BEGIN
            IF (GETUTCDATE()>=@end_time)
            BEGIN
                PRINT 'Stopping purge. More than ' + CONVERT(VARCHAR, @duration) + ' minutes passed since the start of operation.';
                BREAK
            END
        END

        -- Check if somebody wanted to stop the purge operation
        SELECT @stop_purge = COUNT(stop_purge) FROM [core].[purge_info_internal]
        IF (@stop_purge > 0)
        BEGIN
            PRINT 'Stopping purge. Detected a user request to stop purge.';
            BREAK
        END
    END;
    
    PRINT 'End purging orphaned records in snapshots.notable_query_plan Current UTC Time:' + CONVERT(VARCHAR, GETUTCDATE())
END

GO

--
-- This stored proc removes orphaned notable_query_text data from the warehouse 
--
IF (NOT OBJECT_ID(N'core.sp_purge_orphaned_notable_query_text', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [core].[sp_purge_orphaned_notable_query_text] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [core].[sp_purge_orphaned_notable_query_text]
END
GO 

RAISERROR('Creating procedure [core].[sp_purge_orphaned_notable_query_text] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [core].[sp_purge_orphaned_notable_query_text]
    @duration smallint = NULL,
    @end_time datetime = NULL,
    @delete_batch_size int = 500
AS
BEGIN
    PRINT 'Begin purging orphaned records in snapshots.notable_query_text Current UTC Time:' + CONVERT(VARCHAR, GETUTCDATE())

    DECLARE @stop_purge int

    -- Delete orphaned rows from snapshots.notable_query_text.  Query texts are not deleted by the generic purge 
    -- process that deletes other data (above) because query text rows are not tied to a particular snapshot ID. 
    -- Purging  query text table as a special case, by looking for plans that 
    -- are no longer referenced by any of the rows in the snapshots.query_stats table.  We need to delete these 
    -- rows in small chunks, since deleting many GB in a single delete statement would cause lock escalation and 
    -- an explosion in the size of the transaction log (individual query plans can be 10-50MB).  
    DECLARE @rows_affected int;
    -- set expected rows affected as delete batch size
    SET @rows_affected = @delete_batch_size;

    SELECT qt.[sql_handle]
    INTO #tmp_notable_query_text
    FROM snapshots.notable_query_text AS qt
    WHERE NOT EXISTS (
        SELECT snapshot_id 
        FROM snapshots.query_stats AS qs
        WHERE qs.[sql_handle] = qt.[sql_handle])

    WHILE (@rows_affected = @delete_batch_size)
    BEGIN
        -- Deleting TOP N orphaned rows in query text table by joining info from temp table
        -- This is done to speed up delete query. 
        DELETE TOP (@delete_batch_size) snapshots.notable_query_text 
         FROM snapshots.notable_query_text AS qt, #tmp_notable_query_text AS tmp
        WHERE tmp.[sql_handle] = qt.[sql_handle]
        
        SET @rows_affected = @@ROWCOUNT;
        IF(@rows_affected > 0)
        BEGIN
            RAISERROR ('Deleted %d orphaned rows from snapshots.notable_query_text', 0, 1, @rows_affected) WITH NOWAIT;
        END

        -- Check if the execution of the stored proc exceeded the @duration specified
        IF (@duration IS NOT NULL)
        BEGIN
            IF (GETUTCDATE()>=@end_time)
            BEGIN
                PRINT 'Stopping purge. More than ' + CONVERT(VARCHAR, @duration) + ' minutes passed since the start of operation.';
                BREAK
            END
        END

        -- Check if somebody wanted to stop the purge operation
        SELECT @stop_purge = COUNT(stop_purge) FROM [core].[purge_info_internal]
        IF (@stop_purge > 0)
        BEGIN
            PRINT 'Stopping purge. Detected a user request to stop purge.';
            BREAK
        END
    END;

    PRINT 'End purging orphaned records in snapshots.notable_query_text Current UTC Time:' + CONVERT(VARCHAR, GETUTCDATE())

END

GO

--
-- This stored proc removes data from the warehouse that reached its expiration date
--
IF (NOT OBJECT_ID(N'core.sp_purge_data', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [core].[sp_purge_data] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [core].[sp_purge_data]
END
GO 

RAISERROR('Creating procedure [core].[sp_purge_data] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [core].[sp_purge_data]
    @retention_days smallint = NULL,
    @instance_name sysname = NULL,
    @collection_set_uid uniqueidentifier = NULL,
    @duration smallint = NULL,
    @delete_batch_size int = 500
AS
BEGIN
    -- Security check (role membership)
    IF (NOT (ISNULL(IS_MEMBER(N'mdw_admin'), 0) = 1) AND NOT (ISNULL(IS_SRVROLEMEMBER(N'sysadmin'), 0) = 1))
    BEGIN
        RAISERROR(14677, 16, -1, 'mdw_admin')
        RETURN(1) -- Failure
    END

    -- Validate parameters
    IF ((@retention_days IS NOT NULL) AND (@retention_days < 0))
    BEGIN
        RAISERROR(14200, -1, -1, '@retention_days')
        RETURN(1) -- Failure
    END

    IF ((@duration IS NOT NULL) AND (@duration < 0))
    BEGIN
        RAISERROR(14200, -1, -1, '@duration')
        RETURN(1) -- Failure
    END

    -- This table will contain a record if somebody requests purge to stop
    -- If user requested us to purge data - we reset the content of it - and proceed with purge
    -- If somebody in a different session wants purge operations to stop he adds a record
    -- that we will discover while purge in progress
    --
    -- We dont clear this flag when we exit since multiple purge operations with differnet
    -- filters may proceed, and we want all of them to stop.
    DELETE FROM [core].[purge_info_internal]

    SET @instance_name = NULLIF(LTRIM(RTRIM(@instance_name)), N'')

    -- Calculate the time when the operation should stop (NULL otherwise)
    DECLARE @end_time datetime
    IF (@duration IS NOT NULL)
    BEGIN
        SET @end_time = DATEADD(minute, @duration, GETUTCDATE())
    END

    -- Declare table that will be used to find what are the valid
    -- candidate snapshots that could be selected for purge
    DECLARE @purge_candidates table
    (
        snapshot_id int NOT NULL,
        snapshot_time datetime NOT NULL,
        instance_name sysname NOT NULL,
        collection_set_uid uniqueidentifier NOT NULL
    )

    -- Find candidates that match the retention_days criteria (if specified)
    IF (@retention_days IS NULL)
    BEGIN
        -- User did not specified a value for @retention_days, therfore we
        -- will use the default expiration day as marked in the source info
        INSERT INTO @purge_candidates
        SELECT s.snapshot_id, s.snapshot_time, s.instance_name, s.collection_set_uid
        FROM core.snapshots s
        WHERE (GETUTCDATE() >= s.valid_through)
    END
    ELSE
    BEGIN
        -- User specified a value for @retention_days, we will use this overriden value
        -- when deciding what means old enough to qualify for purge this overrides
        -- the days_until_expiration value specified in the source_info_internal table
        INSERT INTO @purge_candidates
        SELECT s.snapshot_id, s.snapshot_time, s.instance_name, s.collection_set_uid
        FROM core.snapshots s
        WHERE GETUTCDATE() >= DATEADD(DAY, @retention_days, s.snapshot_time)
    END

    -- Determine which is the oldest snapshot, from the list of candidates
    DECLARE oldest_snapshot_cursor CURSOR FORWARD_ONLY READ_ONLY FOR
    SELECT p.snapshot_id, p.instance_name, p.collection_set_uid
    FROM @purge_candidates p
    WHERE 
        ((@instance_name IS NULL) or (p.instance_name = @instance_name)) AND
        ((@collection_set_uid IS NULL) or (p.collection_set_uid = @collection_set_uid))
    ORDER BY p.snapshot_time ASC    

    OPEN oldest_snapshot_cursor

    DECLARE @stop_purge int
    DECLARE @oldest_snapshot_id int
    DECLARE @oldest_instance_name sysname
    DECLARE @oldest_collection_set_uid uniqueidentifier

    FETCH NEXT FROM oldest_snapshot_cursor
    INTO @oldest_snapshot_id, @oldest_instance_name, @oldest_collection_set_uid

    -- As long as there are snapshots that matched the time criteria
    WHILE @@FETCH_STATUS = 0
    BEGIN

        -- Filter out records that do not match the other filter crieria
        IF ((@instance_name IS NULL) or (@oldest_instance_name = @instance_name))
        BEGIN

            -- There was no filter specified for instance_name or the instance matches the filter
            IF ((@collection_set_uid IS NULL) or (@oldest_collection_set_uid = @collection_set_uid))
            BEGIN

                -- There was no filter specified for the collection_set_uid or the collection_set_uid matches the filter
                BEGIN TRANSACTION tran_sp_purge_data

                -- Purge data associated with this snapshot. Note: deleting this snapshot
                -- triggers cascade delete in all warehouse tables based on the foreign key 
                -- relationship to snapshots table

                -- Cascade cleanup of all data related referencing oldest snapshot
                DELETE core.snapshots_internal
                FROM core.snapshots_internal s
                WHERE s.snapshot_id = @oldest_snapshot_id

                COMMIT TRANSACTION tran_sp_purge_data

                PRINT 'Snapshot #' + CONVERT(VARCHAR, @oldest_snapshot_id) + ' purged.';
            END

        END

        -- Check if the execution of the stored proc exceeded the @duration specified
        IF (@duration IS NOT NULL)
        BEGIN
            IF (GETUTCDATE()>=@end_time)
            BEGIN
                PRINT 'Stopping purge. More than ' + CONVERT(VARCHAR, @duration) + ' minutes passed since the start of operation.';
                BREAK
            END
        END

        -- Check if somebody wanted to stop the purge operation
        SELECT @stop_purge = COUNT(stop_purge) FROM [core].[purge_info_internal]
        IF (@stop_purge > 0)
        BEGIN
                PRINT 'Stopping purge. Detected a user request to stop purge.';
            BREAK
        END

        -- Move to next oldest snapshot
        FETCH NEXT FROM oldest_snapshot_cursor
        INTO @oldest_snapshot_id, @oldest_instance_name, @oldest_collection_set_uid

    END

    CLOSE oldest_snapshot_cursor
    DEALLOCATE oldest_snapshot_cursor

    -- delete orphaned query plans
    EXEC [core].[sp_purge_orphaned_notable_query_plan] @duration = @duration, @end_time = @end_time, @delete_batch_size = @delete_batch_size

    -- delete orphaned query text
    EXEC [core].[sp_purge_orphaned_notable_query_text] @duration = @duration, @end_time = @end_time, @delete_batch_size = @delete_batch_size
       
END
GO


--
-- This stored procedure is used to stop all purge operations in progress
--
IF (NOT OBJECT_ID(N'core.sp_stop_purge', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [core].[sp_stop_purge] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [core].[sp_stop_purge]
END
GO 

RAISERROR('Creating procedure [core].[sp_stop_purge] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [core].[sp_stop_purge]
AS
BEGIN
    INSERT INTO [core].[purge_info_internal] (stop_purge) VALUES (1)
END
GO


--
-- This function verifies if the caller matches the operator set for the snapshot 
--
IF (OBJECT_ID(N'core.fn_check_operator', 'FN') IS NULL)
BEGIN
    RAISERROR('Creating function [core].[fn_check_operator] ...', 0, 1)  WITH NOWAIT;
    DECLARE @sql nvarchar(max)
    SET @sql = 
    'CREATE FUNCTION [core].[fn_check_operator](
        @snapshot_id int
    )
    RETURNS bit
    AS
    BEGIN
        DECLARE @retval bit;
        
        DECLARE @operator sysname;
        SELECT @operator=operator FROM core.snapshots WHERE snapshot_id = @snapshot_id;
        IF (@operator = SUSER_SNAME())
            SELECT @retval = 1;
        ELSE
            SELECT @retval = 0;
        RETURN @retval;
    END;'

    EXEC sp_executesql @sql;
END
GO

-- Table wait_categories
--
-- This table contains CPU and wait categories.
--
IF (NOT OBJECT_ID(N'core.wait_types', 'U') IS NULL)
BEGIN
    -- Must drop wait_types first since it references wait_categories via FK
    RAISERROR('Dropping table [core].[wait_types] ...', 0, 1)  WITH NOWAIT;
    DROP TABLE [core].[wait_types]
END

IF (NOT OBJECT_ID(N'core.wait_categories', 'U') IS NULL)
BEGIN
    RAISERROR('Dropping table [core].[wait_categories] ...', 0, 1)  WITH NOWAIT;
    DROP TABLE [core].[wait_categories]
END

RAISERROR('Creating table [core].[wait_categories] ...', 0, 1)  WITH NOWAIT;

CREATE TABLE [core].[wait_categories](
        [category_id] [smallint] NOT NULL,
        [category_name] [nvarchar](20) NOT NULL,
        [ignore] [bit] NOT NULL, 
     CONSTRAINT [PK_categories] PRIMARY KEY CLUSTERED ([category_id] ASC)WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]

) ON [PRIMARY]
GO

-- Table wait_types
--
-- This table contains relation between cpu/wait events and cpu/wait categories for each version of SQL Server.
--
--
RAISERROR('Creating table [core].[wait_types] ...', 0, 1)  WITH NOWAIT;

CREATE TABLE [core].[wait_types](
        [category_id] [smallint] NOT NULL,
        [wait_type] [nvarchar](45) NOT NULL,
     CONSTRAINT [PK_events] PRIMARY KEY CLUSTERED ([wait_type] ASC) WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [core].[wait_types] ADD CONSTRAINT [FK_events_categories] FOREIGN KEY([category_id])
REFERENCES [core].[wait_categories] ([category_id])
GO


IF (NOT OBJECT_ID(N'core.wait_types_categorized', 'V') IS NULL)
BEGIN
    RAISERROR('Dropping view [core].[wait_types_categorized] ...', 0, 1)  WITH NOWAIT;
    DROP VIEW [core].[wait_types_categorized];
END;
GO

RAISERROR('Creating view [core].[wait_types_categorized] ...', 0, 1)  WITH NOWAIT;
GO
CREATE VIEW [core].[wait_types_categorized]
AS
    SELECT 
        ct.category_name,
        ev.wait_type, 
        ct.category_id, 
        ct.ignore
    FROM core.wait_categories ct
    INNER JOIN core.wait_types ev ON (ev.category_id = ct.category_id)
GO

-- Insert Categories
--
SET NOCOUNT ON;
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (0, N'CPU', 0);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (1, N'Backup', 0);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (2, N'SQLCLR', 0);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (3, N'Parallelism', 1);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (4, N'Latch', 0);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (5, N'Lock', 0);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (6, N'Network I/O', 0);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (7, N'Buffer I/O', 0);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (8, N'Buffer Latch', 0);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (9, N'Memory', 0);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (10, N'Logging', 0);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (11, N'Compilation', 0);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (12, N'Transaction', 0);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (13, N'Idle', 1);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (14, N'User Waits', 1);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (15, N'Other', 0);
INSERT INTO [core].[wait_categories] ([category_id], [category_name], [ignore]) VALUES (16, N'Full Text Search', 0);

-- Insert Events
--
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (0, N'CPU');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'MISCELLANEOUS');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_SCH_S');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_SCH_M');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_S');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_U');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_X');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_IS');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_IU');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_IX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_SIU');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_SIX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_UIX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_BU');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_RS_S');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_RS_U');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_RIn_NL');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_RIn_S');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_RIn_U');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_RIn_X');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_RX_S');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_RX_U');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (5, N'LCK_M_RX_X');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (4, N'LATCH_NL');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (4, N'LATCH_KP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (4, N'LATCH_SH');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (4, N'LATCH_UP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (4, N'LATCH_EX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (4, N'LATCH_DT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (8, N'PAGELATCH_NL');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (8, N'PAGELATCH_KP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (8, N'PAGELATCH_SH');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (8, N'PAGELATCH_UP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (8, N'PAGELATCH_EX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (8, N'PAGELATCH_DT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (7, N'PAGEIOLATCH_NL');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (7, N'PAGEIOLATCH_KP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (7, N'PAGEIOLATCH_SH');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (7, N'PAGEIOLATCH_UP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (7, N'PAGEIOLATCH_EX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (7, N'PAGEIOLATCH_DT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'TRAN_MARKLATCH_NL');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'TRAN_MARKLATCH_KP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'TRAN_MARKLATCH_SH');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'TRAN_MARKLATCH_UP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'TRAN_MARKLATCH_EX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'TRAN_MARKLATCH_DT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'LAZYWRITER_SLEEP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (7, N'IO_COMPLETION');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (7, N'ASYNC_IO_COMPLETION');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (6, N'ASYNC_NETWORK_IO');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'SLEEP_BPOOL_FLUSH');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'CHKPT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'SLEEP_TASK');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'SLEEP_SYSTEMTASK');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (9, N'RESOURCE_SEMAPHORE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'DTC');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (6, N'OLEDB');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'FAILPOINT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'RESOURCE_QUEUE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (7, N'ASYNC_DISKPOOL_LOCK');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'THREADPOOL');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DEBUG');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (7, N'REPLICA_WRITES');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'BROKER_RECEIVE_WAITFOR');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DBMIRRORING_CMD');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'WAIT_FOR_RESULTS');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (0, N'SOS_SCHEDULER_YIELD');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (9, N'SOS_VIRTUALMEMORY_LOW');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (9, N'SOS_RESERVEDMEMBLOCKLIST');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SOS_LOCALALLOCATORLIST');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SOS_CALLBACK_REMOVAL');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (9, N'LOWFAIL_MEMMGR_QUEUE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (1, N'BACKUP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (1, N'BACKUPBUFFER');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (1, N'BACKUPIO');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (1, N'BACKUPTHREAD');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DBMIRROR_DBM_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DBMIRROR_DBM_EVENT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (6, N'DBMIRROR_SEND');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'CURSOR_ASYNC');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'HTTP_ENUMERATION');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (16, N'SOAP_READ');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (16, N'SOAP_WRITE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DUMP_LOG_COORDINATOR');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (1, N'DISKIO_SUSPEND');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'IMPPROV_IOWAIT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'QNMANAGER_ACQUIRE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DEADLOCK_TASK_SEARCH');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'REPL_SCHEMA_ACCESS');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'REPL_CACHE_ACCESS');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SQLSORT_SORTMUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SQLSORT_NORMMUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SQLTRACE_WAIT_ENTRIES');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SQLTRACE_LOCK');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'SQLTRACE_BUFFER_FLUSH');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SQLTRACE_SHUTDOWN');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'MSQL_SYNC_PIPE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'QUERY_TRACEOUT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (6, N'DTC_STATE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (7, N'FCB_REPLICA_WRITE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (7, N'FCB_REPLICA_READ');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (10, N'WRITELOG');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'HTTP_ENDPOINT_COLLCREATE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (3, N'EXCHANGE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DBTABLE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'EC');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'TEMPOBJ');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'XACTLOCKINFO');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (10, N'LOGMGR');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (9, N'CMEMTHREAD');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (3, N'CXPACKET');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (14, N'WAITFOR');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'CURSOR');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (3, N'EXECSYNC');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SOSHOST_INTERNAL');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SOSHOST_SLEEP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SOSHOST_WAITFORDONE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SOSHOST_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SOSHOST_EVENT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SOSHOST_SEMAPHORE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SOSHOST_RWLOCK');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SOSHOST_TRACELOCK');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'MSQL_XP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (6, N'MSQL_DQ');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (10, N'LOGBUFFER');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'TRANSACTION_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (16, N'MSSEARCH');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'XACTWORKSPACE_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (2, N'CLR_JOIN');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (2, N'CLR_CRST');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (2, N'CLR_SEMAPHORE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'CLR_MANUAL_EVENT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'CLR_AUTO_EVENT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (2, N'CLR_MONITOR');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (2, N'CLR_RWLOCK_READER');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (2, N'CLR_RWLOCK_WRITER');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (2, N'SQLCLR_QUANTUM_PUNISHMENT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (2, N'SQLCLR_APPDOMAIN');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (2, N'SQLCLR_ASSEMBLY');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'KTM_ENLISTMENT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'KTM_RECOVERY_RESOLUTION');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'KTM_RECOVERY_MANAGER');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (2, N'SQLCLR_DEADLOCK_DETECTION');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'QPJOB_WAITFOR_ABORT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'QPJOB_KILL');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'BAD_PAGE_PROCESS');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (1, N'BACKUP_OPERATOR');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'PRINT_ROLLBACK_PROGRESS');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'ENABLE_VERSIONING');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DISABLE_VERSIONING');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'REQUEST_DISPENSER_PAUSE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DROPTEMP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'FT_RESTART_CRAWL');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'FT_RESUME_CRAWL');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (10, N'LOGMGR_RESERVE_APPEND');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (10, N'LOGMGR_FLUSH');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'XACT_OWN_TRANSACTION');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'XACT_RECLAIM_SESSION');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'DTC_WAITFOR_OUTCOME');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'DTC_RESOLVE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SEC_DROP_TEMP_KEY');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SRVPROC_SHUTDOWN');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (6, N'NET_WAITFOR_PACKET');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'DTC_ABORT_REQUEST');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'DTC_TMDOWN_REQUEST');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'RECOVER_CHANGEDB');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'WORKTBL_DROP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'MIRROR_SEND_MESSAGE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'SNI_HTTP_ACCEPT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SNI_HTTP_WAITFOR_0_DISCON');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (9, N'UTIL_PAGE_ALLOC');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'SERVER_IDLE_CHECK');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (1, N'BACKUP_CLIENTLOCK');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (4, N'DEADLOCK_ENUM_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (4, N'INDEX_USAGE_STATS_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (4, N'VIEW_DEFINITION_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'QUERY_NOTIFICATION_MGR_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'QUERY_NOTIFICATION_TABLE_MGR_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'QUERY_NOTIFICATION_SUBSCRIPTION_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'QUERY_NOTIFICATION_UNITTEST_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'IMP_IMPORT_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (11, N'RESOURCE_SEMAPHORE_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'IO_AUDIT_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'BUILTIN_HASHKEY_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SOS_PROCESS_AFFINITY_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'MSQL_XACT_MGR_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (12, N'MSQL_XACT_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'QRY_MEM_GRANT_INFO_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SOS_STACKSTORE_INIT_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SOS_SYNC_TASK_ENQUEUE_EVENT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SOS_OBJECT_STORE_DESTROY_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'EE_PMOLOCK');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (11, N'RESOURCE_SEMAPHORE_QUERY_COMPILE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (11, N'RESOURCE_SEMAPHORE_SMALL_QUERY');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'ABR');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (2, N'ASSEMBLY_LOAD');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'BROKER_CONNECTION_RECEIVE_TASK');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'BROKER_ENDPOINT_STATE_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'BROKER_EVENTHANDLER');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'BROKER_INIT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'BROKER_MASTERSTART');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'BROKER_REGISTERALLENDPOINTS');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'BROKER_SHUTDOWN');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'BROKER_TASK_STOP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'BROKER_TRANSMITTER');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'CHECK_PRINT_RECORD');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'CHECKPOINT_QUEUE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (2, N'CLR_MEMORY_SPY');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (2, N'CLR_TASK_START');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (2, N'CLRHOST_STATE_ACCESS');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DAC_INIT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DBCC_COLUMN_TRANSLATION_CACHE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DBMIRROR_EVENTS_QUEUE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DBMIRROR_WORKER_QUEUE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DLL_LOADING_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DUMP_LOG_COORDINATOR_QUEUE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'DUMPTRIGGER');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'EE_SPECPROC_MAP_INIT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'ERROR_REPORTING_MANAGER');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'EXECUTION_PIPE_EVENT_INTERNAL');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (2, N'FS_GARBAGE_COLLECTOR_SHUTDOWN');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'FULLTEXT GATHERER');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'GUARDIAN');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'HTTP_START');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'INTERNAL_TESTING');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'KSOURCE_WAKEUP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'LOGMGR_QUEUE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'ONDEMAND_TASK_QUEUE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'PARALLEL_BACKUP_QUEUE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'QUERY_ERRHDL_SERVICE_DONE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'QUERY_EXECUTION_INDEX_SORT_EVENT_OPEN');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'QUERY_OPTIMIZER_PRINT_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'QUERY_REMOTE_BRICKS_DONE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'REQUEST_FOR_DEADLOCK_SEARCH');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SEQUENTIAL_GUID');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SHUTDOWN');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'SLEEP_DBSTARTUP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'SLEEP_DCOMSTARTUP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'SLEEP_MSDBSTARTUP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'SLEEP_TEMPDBSTARTUP');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SNI_CRITICAL_SECTION');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SNI_LISTENER_ACCESS');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SNI_TASK_COMPLETION');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'SOS_DISPATCHER_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'TIMEPRIV_TIMEPERIOD');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'TRACEWRITE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'VIA_ACCEPT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'WAITFOR_TASKSHUTDOWN');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'WAITSTAT_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'WCC');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'XE_TIMER_EVENT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'XE_DISPATCHER_WAIT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (13, N'FSAGENT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'XE_TIMER_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'XE_TIMER_TASK_DONE');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'XE_BUFFERMGR_ALLPROCECESSED_EVENT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'XE_BUFFERMGR_FREEBUF_EVENT');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'XE_DISPATCHER_JOIN');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'XE_MODULEMGR_SYNC');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'XE_OLS_LOCK');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'XE_SERVICES_MUTEX');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'XE_SESSION_CREATE_SYNC');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'XE_SESSION_SYNC');
INSERT INTO [core].[wait_types] ([category_id],[wait_type]) VALUES (15, N'XE_STM_CREATE')
GO

IF (NOT OBJECT_ID(N'core.fn_query_text_from_handle', 'TF') IS NULL)
BEGIN
    RAISERROR('Dropping function [core].[fn_query_text_from_handle] ...', 0, 1)  WITH NOWAIT;
    DROP FUNCTION [core].[fn_query_text_from_handle]
END
GO 

RAISERROR('Creating function [core].[fn_query_text_from_handle] ...', 0, 1)  WITH NOWAIT;
GO
CREATE FUNCTION [core].[fn_query_text_from_handle](
    @handle varbinary(64), 
    @statement_start_offset int, 
    @statement_end_offset int)
RETURNS @query_text TABLE (database_id smallint, object_id int, encrypted bit, query_text nvarchar(max))
BEGIN
      IF @handle IS NOT NULL
      BEGIN
            DECLARE @start int, @end int
            DECLARE @dbid smallint, @objectid int, @encrypted bit
            DECLARE @batch nvarchar(max), @query nvarchar(max)

            -- statement_end_offset is zero prior to beginning query execution (e.g., compilation)
            SELECT 
                  @start = ISNULL(@statement_start_offset, 0), 
                  @end = CASE WHEN @statement_end_offset is null or @statement_end_offset = 0 THEN -1
                                    ELSE @statement_end_offset 
                              END
 
            SELECT @dbid = t.dbid, 
                  @objectid = t.objectid, 
                  @encrypted = t.encrypted, 
                  @batch = t.text 
            FROM sys.dm_exec_sql_text(@handle) AS t

            SELECT @query = CASE 
                        WHEN @encrypted = CAST(1 as bit) THEN N'encrypted text' 
                        ELSE LTRIM(SUBSTRING(@batch, @start / 2 + 1, ((CASE WHEN @end = -1 THEN DATALENGTH(@batch) 
                                          ELSE @end END) - @start) / 2))
                  end

            -- Found internal queries (e.g., CREATE INDEX) with end offset of original batch that is 
            -- greater than the length of the internal query and thus returns nothing if we don''t do this
            IF DATALENGTH(@query) = 0
            BEGIN
                  SELECT @query = @batch
            END

            INSERT INTO @query_text (database_id, object_id, encrypted, query_text) 
            VALUES (@dbid, @objectid, @encrypted, @query)
      END
      RETURN
END
GO


-- Table performance_counter_report_groups
--
-- This table lists the counters that are used by each report.  Referenced in [snapshots].[rpt_generic_perfmon]. 
--
IF (NOT OBJECT_ID(N'[core].[performance_counter_report_group_items]', 'U') IS NULL)
BEGIN
    -- Must drop wait_types first since it references wait_categories via FK
    RAISERROR('Dropping table [core].[performance_counter_report_group_items] ...', 0, 1)  WITH NOWAIT;
    DROP TABLE [core].[performance_counter_report_group_items]
END
GO

RAISERROR('Creating table [core].[performance_counter_report_group_items] ...', 0, 1)  WITH NOWAIT;
CREATE TABLE [core].[performance_counter_report_group_items](
        [counter_group_item_id] int IDENTITY NOT NULL PRIMARY KEY CLUSTERED, 
        [counter_group_id] nvarchar(128) NOT NULL,          -- e.g. report name
        [counter_subgroup_id] nvarchar(128) NOT NULL,       -- e.g. chart name, used by the chart to filter out its rows from the larger resultset
        [series_name] nvarchar(512) NOT NULL,               -- data series label used for output
        [object_name] nvarchar(2048) NOT NULL,              -- perfmon object name
        [object_name_wildcards] bit NOT NULL,               -- 1 if wildcard expansion is needed for the perfmon object name (e.g. '%SQL%:General Statistics'), 0 otherwise
        [counter_name] nvarchar(2048) NOT NULL,             -- perfmon counter name
        [instance_name] nvarchar(2048) NULL,                -- perfmon instance name(s) to include (evaluated with LIKE)
        [not_instance_name] nvarchar(2048) NULL,            -- perfmon instance name(s) to omit (evaluated with LIKE, use NULL to skip this criteria)
        [multiply_by] numeric(28,10) NOT NULL,              -- value to multiple the counter  by on output (for unit conversion) 
        [divide_by_cpu_count] bit NOT NULL                  -- 1 if counter should be divided by the machine's CPU count (e.g. "Process(abc)\% Processor Time", 0 otherwise
) 
GO

-- Insert perfmon counter list for each report group 
--
DECLARE @CONVERT_BYTES_TO_MB numeric(28,10)
DECLARE @CONVERT_KB_TO_MB numeric(28,10)
SET @CONVERT_BYTES_TO_MB = 1.0 / (1024*1024)
SET @CONVERT_KB_TO_MB = 1.0 / (1024);

INSERT INTO [core].[performance_counter_report_group_items] ([counter_group_id],[counter_subgroup_id],[series_name],
    [object_name],[object_name_wildcards],[counter_name],[instance_name],[not_instance_name],[multiply_by],[divide_by_cpu_count]) 
VALUES 
    ('ServerActivity', 'memoryUsage', 'System', 'Process', 0, 'Working Set', '_Total', NULL, @CONVERT_BYTES_TO_MB, 0 ), 
    ('ServerActivity', 'memoryUsage', 'SQL Server', 'Process', 0, 'Working Set', '$(TARGETPROCESS)', NULL, @CONVERT_BYTES_TO_MB, 0 ), 
    ('ServerActivity', 'IOUsage', 'System', 'Process', 0, 'IO Read Bytes/sec', '_Total', NULL, @CONVERT_BYTES_TO_MB, 0 ), 
    ('ServerActivity', 'IOUsage', 'System', 'Process', 0, 'IO Write Bytes/sec', '_Total', NULL, @CONVERT_BYTES_TO_MB, 0 ),               -- changed to fix Bug # 234905 
    ('ServerActivity', 'IOUsage', 'SQL Server', 'Process', 0, 'IO Read Bytes/sec', '$(TARGETPROCESS)', NULL, @CONVERT_BYTES_TO_MB, 0 ), 
    ('ServerActivity', 'IOUsage', 'SQL Server', 'Process', 0, 'IO Write Bytes/sec', '$(TARGETPROCESS)', NULL, @CONVERT_BYTES_TO_MB, 0 ), -- changed to fix Bug # 234905 
    ('ServerActivity', 'cpuUsage', 'System', 'Processor', 0, '% Processor Time', '_Total', NULL, 1.0, 0 ), 
    ('ServerActivity', 'cpuUsage', 'SQL Server', 'Process', 0, '% Processor Time', '$(TARGETPROCESS)', NULL, 1.0, 1 ), 
    ('ServerActivity', 'networkUsage', 'System', 'Network Interface', 0, 'Bytes Total/sec', '%', NULL, 1.0, 0 ), 
    ('ServerActivity', 'sqlActivity', 'Logins/sec', '%SQL%:General Statistics', 1, 'Logins/sec', '', NULL, 1.0, 0 ), 
    ('ServerActivity', 'sqlActivity', 'Logouts/sec', '%SQL%:General Statistics', 1, 'Logouts/sec', '', NULL, 1.0, 0 ), 
    ('ServerActivity', 'sqlActivity', 'Transactions', '%SQL%:General Statistics', 1, 'Transactions', '', NULL, 1.0, 0 ), 
    ('ServerActivity', 'sqlActivity', 'User Connections', '%SQL%:General Statistics', 1, 'User Connections', '', NULL, 1.0, 0 ), 
    ('ServerActivity', 'sqlActivity', 'Batch Requests/sec', '%SQL%:SQL Statistics', 1, 'Batch Requests/sec', '', NULL, 1.0, 0 ), 
    ('ServerActivity', 'sqlActivity', 'SQL Compilations/sec', '%SQL%:SQL Statistics', 1, 'SQL Compilations/sec', '', NULL, 1.0, 0 ), 
    ('ServerActivity', 'sqlActivity', 'SQL Re-Compilations/sec', '%SQL%:SQL Statistics', 1, 'SQL Re-Compilations/sec', '', NULL, 1.0, 0 ), 

    ('SystemDiskUsage', 'diskSpeed', '[COUNTER_INSTANCE]', 'LogicalDisk', 0, 'Avg. Disk sec/Transfer', '%', NULL, 1.0, 0 ), 
    ('SystemDiskUsage', 'diskQueues', '[COUNTER_INSTANCE]', 'LogicalDisk', 0, 'Avg. Disk Queue Length', '%', NULL, 1.0, 0 ), 
    ('SystemDiskUsage', 'diskRates', '[COUNTER_INSTANCE]', 'LogicalDisk', 0, 'Disk Bytes/sec', '%', NULL, @CONVERT_BYTES_TO_MB, 0 ), 
    ('SystemDiskUsage', 'processStats', '[COUNTER_INSTANCE]', 'Process', 0, 'IO Read Bytes/sec', '%', '$(TARGETPROCESS)', 1.0, 0 ), 
    ('SystemDiskUsage', 'processStats', '[COUNTER_INSTANCE]', 'Process', 0, 'IO Write Bytes/sec', '%', '$(TARGETPROCESS)', 1.0, 0 ), 
    ('SystemDiskUsage', 'processStats', '[COUNTER_INSTANCE]', 'Process', 0, 'Disk Reads/sec', '%', '$(TARGETPROCESS)', 1.0, 0 ), 
    ('SystemDiskUsage', 'processStats', '[COUNTER_INSTANCE]', 'Process', 0, 'Disk Writes/sec', '%', '$(TARGETPROCESS)', 1.0, 0 ), 
    ('SystemDiskUsage', 'diskSpeed', '[COUNTER_INSTANCE]', 'LogicalDisk', 0, 'Avg. Disk sec/Transfer', '%', NULL, 1.0, 0 ), 
    ('SystemDiskUsage', 'diskQueues', '[COUNTER_INSTANCE]', 'LogicalDisk', 0, 'Avg. Disk Queue Length', '%', NULL, 1.0, 0 ), 
    ('SystemDiskUsage', 'diskRates', '[COUNTER_INSTANCE]', 'LogicalDisk', 0, 'Disk Bytes/sec', '%', NULL, @CONVERT_BYTES_TO_MB, 0 ), 

    ('SystemDiskUsagePivot', 'processStats', '[COUNTER_INSTANCE]', 'Process', 0, 'IO Read Bytes/sec', '%', '$(TARGETPROCESS)', 1.0, 0 ), 
    ('SystemDiskUsagePivot', 'processStats', '[COUNTER_INSTANCE]', 'Process', 0, 'IO Write Bytes/sec', '%', '$(TARGETPROCESS)', 1.0, 0 ), 
    ('SystemDiskUsagePivot', 'processStats', '[COUNTER_INSTANCE]', 'Process', 0, 'Disk Reads/sec', '%', '$(TARGETPROCESS)', 1.0, 0 ), 
    ('SystemDiskUsagePivot', 'processStats', '[COUNTER_INSTANCE]', 'Process', 0, 'Disk Writes/sec', '%', '$(TARGETPROCESS)', 1.0, 0 ), 

    ('SqlMemoryUsage', 'memoryRates', 'Page life expectancy', '%SQL%:Buffer Manager', 1, 'Page life expectancy', '', NULL, 1.0, 0 ), 

    ('SystemMemoryUsage', 'memoryUsage', 'Total Working Set', 'Process', 0, 'Working Set', '_Total', NULL, @CONVERT_BYTES_TO_MB, 0 ), 
    ('SystemMemoryUsage', 'memoryUsage', 'Cache Bytes', 'Memory', 0, 'Cache Bytes', '', NULL, @CONVERT_BYTES_TO_MB, 0 ), 
    ('SystemMemoryUsage', 'memoryUsage', 'Pool Nonpaged Bytes', 'Memory', 0, 'Pool Nonpaged Bytes', '', NULL, @CONVERT_BYTES_TO_MB, 0 ), 
    ('SystemMemoryUsage', 'memoryRates', 'Page Reads/sec', 'Memory', 0, 'Page Reads/sec', '', NULL, 1.0, 0 ), 
    ('SystemMemoryUsage', 'memoryRates', 'Page Writes/sec', 'Memory', 0, 'Page Writes/sec', '', NULL, 1.0, 0 ), 

    ('SystemMemoryUsagePivot', 'processStats', '[COUNTER_INSTANCE]', 'Process', 0, 'Working Set', '%', '$(TARGETPROCESS)', @CONVERT_BYTES_TO_MB, 0 ), 
    ('SystemMemoryUsagePivot', 'processStats', '[COUNTER_INSTANCE]', 'Process', 0, 'Private Bytes', '%', '$(TARGETPROCESS)', @CONVERT_BYTES_TO_MB, 0 ), 

    ('SystemCpuUsage', 'cpuUsage', 'CPU [COUNTER_INSTANCE]', 'Processor', 0, '% Processor Time', '%', NULL, 1.0, 0 ), 

    ('SystemCpuUsagePivot', 'processStats', '[COUNTER_INSTANCE]', 'Process', 0, '% Processor Time', '%', '$(TARGETPROCESS)', 1.0, 1 ), 
    ('SystemCpuUsagePivot', 'processStats', '[COUNTER_INSTANCE]', 'Process', 0, 'Thread Count', '%', '$(TARGETPROCESS)', 1.0, 0 ), 

    ('SqlActivity', 'sessionsAndConnections', 'User Connections', '%SQL%:General Statistics', 1, 'User Connections', '', NULL, 1.0, 0 ), 
    ('SqlActivity', 'sessionsAndConnections', 'Active Transactions', '%SQL%:Databases', 1, 'Active Transactions', '_Total', NULL, 1.0, 0 ), 
    ('SqlActivity', 'sessionsAndConnections', 'Active requests', '%SQL%:Workload Group Stats', 1, 'Active requests', '%', NULL, 1.0, 0 ), 
    ('SqlActivity', 'requestsAndCompilations', 'Batch Requests/sec', '%SQL%:SQL Statistics', 1, 'Batch Requests/sec', '', NULL, 1.0, 0 ), 
    ('SqlActivity', 'requestsAndCompilations', 'SQL Compilations/sec', '%SQL%:SQL Statistics', 1, 'SQL Compilations/sec', '', NULL, 1.0, 0 ), 
    ('SqlActivity', 'requestsAndCompilations', 'SQL Re-Compilations/sec', '%SQL%:SQL Statistics', 1, 'SQL Re-Compilations/sec', '', NULL, 1.0, 0 ), 
    ('SqlActivity', 'planCache', '[COUNTER_INSTANCE]', '%SQL%:Plan Cache', 1, 'Cache Hit Ratio', '%', NULL, 1.0, 0 ), 
    ('SqlActivity', 'tempDb', 'Free Space in tempdb (MB)', '%SQL%:Transactions', 1, 'Free Space in tempdb (KB)', '', NULL, @CONVERT_KB_TO_MB, 0 ), 
    ('SqlActivity', 'tempDb', 'Active Temp Tables', '%SQL%:General Statistics', 1, 'Active Temp Tables', '', NULL, 1.0, 0 ), 
    ('SqlActivity', 'tempDb', 'Transactions/sec', '%SQL%:Databases', 1, 'Transactions/sec', 'tempdb', NULL, 1.0, 0 )
GO


/**********************************************************************/
/* SNAPSHOTS SCHEMA                                                   */
/**********************************************************************/
RAISERROR('', 0, 1)  WITH NOWAIT;
RAISERROR('Create schema snapshots...', 0, 1)  WITH NOWAIT;
RAISERROR('', 0, 1)  WITH NOWAIT;
GO
IF (SCHEMA_ID('snapshots') IS NULL)
BEGIN
    -- Dynamic SQL here because we must skip the creation attempt if the schema already 
    -- exists, but we can't use a simple IF check because CREATE SCHEMA must be the first 
    -- statement in its batch. 
    DECLARE @sql nvarchar(128)
    SET @sql = 'CREATE SCHEMA snapshots'
    EXEC sp_executesql @sql
END
GO

--
-- PERFORMANCE COUNTERS COLLECTOR TYPE SUPPORT
--

-- This table holds information about instances of perf counters collected (their path and type)
IF (OBJECT_ID(N'[snapshots].[performance_counter_instances]', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[performance_counter_instances]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[performance_counter_instances](
        [performance_counter_id]            int IDENTITY NOT NULL,
        [path]                                nvarchar(2048) NOT NULL,
        [object_name]                        nvarchar(2048) NOT NULL,
        [counter_name]                        nvarchar(2048) NOT NULL,
        [instance_name]                        nvarchar(2048) NULL,
        [counter_type]                        int NOT NULL,

        -- This constraint creation generates a warning. We will deal with the warning when fix for #130259 is provided
        CONSTRAINT [UN_performance_counter_path] UNIQUE
        (
            [path]
        ) WITH (IGNORE_DUP_KEY = ON)
    )
END;
GO

-- performance_counter_instances.PK_performance_counter_instances
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_performance_counter_instances', 'performance_counter_id', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'performance_counter_instances', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_performance_counter_instances', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- performance_counter_instances.IDX_performance_counter_instances1
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('IDX_performance_counter_instances1', 'object_name', 0, 0), 
    ('IDX_performance_counter_instances1', 'counter_name', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'performance_counter_instances', 
    @object_type = 'INDEX', @constraint_or_index_name = 'IDX_performance_counter_instances1', 
    @ignore_dup_key = 0, @clustered = 0;
GO


-- This table holds information about actual values collected for perf counters (formatted, raw values, and time when counter was collected)
IF (OBJECT_ID(N'[snapshots].[performance_counter_values]', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[performance_counter_values]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[performance_counter_values](
        [performance_counter_instance_id]    int NOT NULL,
        [snapshot_id]                        int NOT NULL,                    
        [collection_time]                    datetimeoffset(7) NOT NULL,
        [formatted_value]                    float NOT NULL,
        [raw_value_first]                    bigint NOT NULL,
        [raw_value_second]                    bigint NULL,
    ) ON [PRIMARY];
    
    ALTER TABLE [snapshots].[performance_counter_values]
    ADD CONSTRAINT [CHK_performance_counter_values_check_operator] CHECK (core.fn_check_operator(snapshot_id) = 1)
END;
GO

-- performance_counter_values.PK_performance_counter_values
-- 
-- Ignore duplicate keys on primary key and not fail uploads because of that.
-- This is needed because in some cases we may have collection item that
-- collects the same counter twice, for valid reasons. To not force users
-- to provide some exclusion lists, we will ignore duplicates. 
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_performance_counter_values', 'performance_counter_instance_id', 0, 0), 
    ('PK_performance_counter_values', 'snapshot_id', 0, 0), 
    ('PK_performance_counter_values', 'collection_time', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'performance_counter_values', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_performance_counter_values', 
    @ignore_dup_key = 1, @clustered = 1;
GO

-- performance_counter_values.IDX_performance_counter_values1
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('IDX_performance_counter_values1', 'snapshot_id', 0, 0), 
    ('IDX_performance_counter_values1', 'performance_counter_instance_id', 0, 0), 
    ('IDX_performance_counter_values1', 'collection_time', 0, 0), 
    ('IDX_performance_counter_values1', 'formatted_value', 1, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'performance_counter_values', 
    @object_type = 'INDEX', @constraint_or_index_name = 'IDX_performance_counter_values1', 
    @ignore_dup_key = 0, @clustered = 0;
GO

-- performance_counter_values.FK_performance_counter_values_snapshot_id
IF OBJECT_ID ('snapshots.FK_performance_counter_values_snapshot_id', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_performance_counter_values_snapshot_id] on snapshots.performance_counter_values ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[performance_counter_values]
        ADD CONSTRAINT [FK_performance_counter_values_snapshot_id] FOREIGN KEY(snapshot_id)
        REFERENCES [core].[snapshots_internal] (snapshot_id) ON DELETE CASCADE
END;
GO

-- performance_counter_values.FK_performance_counter_values_instance_id
IF OBJECT_ID ('snapshots.FK_performance_counter_values_snapshot_id', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_performance_counter_values_instance_id] on snapshots.performance_counter_values ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[performance_counter_values]
        ADD CONSTRAINT [FK_performance_counter_values_instance_id] FOREIGN KEY(performance_counter_instance_id)
        REFERENCES [snapshots].[performance_counter_instances] (performance_counter_id) ON DELETE CASCADE
END;
GO


-- This view shows user friendly information about performance counters
IF (OBJECT_ID(N'[snapshots].[performance_counters]', 'V') IS NOT NULL)
BEGIN
    RAISERROR('Dropping view [snapshots].[performance_counters]...', 0, 1)  WITH NOWAIT;
    DROP VIEW [snapshots].[performance_counters]
END

RAISERROR('Creating view [snapshots].[performance_counters]...', 0, 1)  WITH NOWAIT;
GO
CREATE VIEW [snapshots].[performance_counters]
AS
SELECT     
    pci.performance_counter_id, 
    pcv.snapshot_id AS snapshot_id, 
    pcv.collection_time AS collection_time,
    pci.path AS path, 
    pci.object_name AS performance_object_name,
    pci.counter_name AS performance_counter_name,
    pci.instance_name AS performance_instance_name,
    pcv.formatted_value AS formatted_value, 
    pcv.raw_value_first, 
    pcv.raw_value_second
FROM
    snapshots.performance_counter_instances pci
    INNER JOIN snapshots.performance_counter_values pcv ON pci.performance_counter_id = pcv.performance_counter_instance_id
GO

-- Function to obtain user friendly formatted perf counters values for a given instance and time window
IF (OBJECT_ID(N'snapshots.fn_get_performance_counters', 'IF') IS NOT NULL)
BEGIN
    RAISERROR('Dropping function [snapshots].[fn_get_performance_counters] ...', 0, 1)  WITH NOWAIT;
    DROP FUNCTION [snapshots].[fn_get_performance_counters]
END
GO 

RAISERROR('Creating function [snapshots].[fn_get_performance_counters] ...', 0, 1)  WITH NOWAIT;
GO
CREATE FUNCTION [snapshots].[fn_get_performance_counters]  
(
    @instance_name       sysname,
    @start_time          datetimeoffset(7) = NULL,
    @end_time            datetimeoffset(7) = NULL
) 
RETURNS TABLE
AS
RETURN
(
    SELECT 
        pc.performance_counter_id AS performance_counter_id,
        pc.collection_time AS collection_time,
        pc.path AS path,
        pc.performance_object_name AS performance_object_name,
        pc.performance_counter_name AS performance_counter_name,
        pc.performance_instance_name AS performance_instance_name,
        pc.formatted_value AS formatted_value
    FROM [snapshots].[performance_counters] as pc
    JOIN [core].[snapshots] s on s.snapshot_id = pc.snapshot_id
    WHERE
        @instance_name = s.instance_name AND
        ISNULL(@start_time,CAST (0 AS DATETIME)) <= pc.collection_time AND
        ISNULL(@end_time,GETDATE()) >= pc.collection_time
)
GO

-- Function to obtain user friendly statistics about a perf counter for a given instance, counter path and time window
IF (OBJECT_ID(N'snapshots.fn_get_performance_counter_statistics', 'IF') IS NOT NULL)
BEGIN
    RAISERROR('Dropping function [snapshots].[fn_get_performance_counter_statistics] ...', 0, 1)  WITH NOWAIT;
    DROP FUNCTION [snapshots].[fn_get_performance_counter_statistics]
END
GO 

RAISERROR('Creating function [snapshots].[fn_get_performance_counter_statistics] ...', 0, 1)  WITH NOWAIT;
GO
CREATE FUNCTION [snapshots].[fn_get_performance_counter_statistics]  
(
    @instance_name       sysname,
    @path_pattern         nvarchar(2048),
    @start_time          datetimeoffset(7) = NULL,
    @end_time            datetimeoffset(7) = NULL
) 
RETURNS TABLE
AS
RETURN
(
    SELECT 
        pc.path                        as path,
        MIN(pc.formatted_value)        as minimum_value,
        MAX(pc.formatted_value)        as maximum_value,
        AVG(pc.formatted_value)        as average_value,
        STDEV(pc.formatted_value)    as standard_deviation,
        VAR(pc.formatted_value)        as statistical_variance
    FROM [snapshots].[performance_counters] as pc
    JOIN [core].[snapshots] s on s.snapshot_id = pc.snapshot_id
    WHERE
        s.instance_name = @instance_name AND
        pc.path LIKE @path_pattern AND
        pc.collection_time >= @start_time AND
        pc.collection_time <= @end_time
    GROUP BY pc.path
)
GO



--
-- SQLTRACE COLLECTOR TYPE SUPPORT
--

-- This table holds information about captured traces
IF (OBJECT_ID(N'[snapshots].[trace_info]', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[trace_info]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[trace_info] (
        trace_info_id                 int IDENTITY NOT NULL,
        source_id                     int NOT NULL,      -- id of source_info record corresponding to source of this data
        collection_item_id            int NOT NULL,      -- id of the trace on the target instance
        last_snapshot_id              int NULL,          -- references core.snapshots table. Identifies the most recent snapshot id that was generated for this trace
        start_time                    datetime NULL,     -- time when this trace was started on the target machine
        last_event_sequence           bigint NULL,       -- event sequence of the most recent event for this trace was captured. 
        is_running                    bit NULL,          -- 0 - trace is stopped, 1 - trace is running
        event_count                   bigint NULL,       -- total number of events captured by this trace
        dropped_event_count           int NULL,          -- total number of events dropped by this trace.
    );
END;
GO

-- trace_info.PK_trace_info
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_trace_info', 'trace_info_id', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'trace_info', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_trace_info', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- trace_info.FK_trace_info_last_snapshot_id
IF OBJECT_ID ('snapshots.FK_trace_info_last_snapshot_id', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_trace_info_last_snapshot_id] on snapshots.trace_info ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[trace_info]
        ADD CONSTRAINT [FK_trace_info_last_snapshot_id] FOREIGN KEY(last_snapshot_id)
        REFERENCES [core].[snapshots_internal] (snapshot_id) ON DELETE CASCADE
END;
GO

-- trace_info.FK_trace_info_last_snapshot_id
IF OBJECT_ID ('snapshots.FK_trace_info_source_id', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_trace_info_source_id] on snapshots.trace_info ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[trace_info]
        ADD CONSTRAINT [FK_trace_info_source_id] FOREIGN KEY(source_id)
        REFERENCES [core].[source_info_internal] (source_id)
END;
GO


-- This table holds all trace data 
IF (OBJECT_ID(N'[snapshots].[trace_data]', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[trace_data]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[trace_data] (
        trace_info_id                   int NOT NULL,    -- references id of the trace from trace_info table
        snapshot_id                     int NOT NULL,    -- references snapshot_id from core.snapshots
        -- Following all all trace columns that match the list from sys.trace_columns system view
        TextData                        nvarchar(max) NULL,
        BinaryData                      varbinary(max) NULL,
        DatabaseID                      int NULL,
        TransactionID                   bigint NULL,
        LineNumber                      int NULL,
        NTUserName                      nvarchar(256) NULL,
        NTDomainName                    nvarchar(256) NULL,
        HostName                        nvarchar(256) NULL,
        ClientProcessID                 int NULL,
        ApplicationName                 nvarchar(256) NULL,
        LoginName                       nvarchar(256) NULL,
        SPID                            int NULL,
        Duration                        bigint NULL,
        StartTime                       datetimeoffset(7) NULL,
        EndTime                         datetimeoffset(7) NULL,
        Reads                           bigint NULL,
        Writes                          bigint NULL,
        CPU                             int NULL,
        Permissions                     bigint NULL,
        Severity                        int NULL,
        EventSubClass                   int NULL,
        ObjectID                        int NULL,
        Success                         int NULL,
        IndexID                         int NULL,
        IntegerData                     int NULL,
        ServerName                      nvarchar(256) NULL,
        EventClass                      int NULL,
        ObjectType                      int NULL,
        NestLevel                       int NULL,
        State                           int NULL,
        Error                           int NULL,
        Mode                            int NULL,
        Handle                          int NULL,
        ObjectName                      nvarchar(256) NULL,
        DatabaseName                    nvarchar(256) NULL,
        FileName                        nvarchar(256) NULL,
        OwnerName                       nvarchar(256) NULL,
        RoleName                        nvarchar(256) NULL,
        TargetUserName                  nvarchar(256) NULL,
        DBUserName                      nvarchar(256) NULL,
        LoginSid                        varbinary(max) NULL,
        TargetLoginName                 nvarchar(256) NULL,
        TargetLoginSid                  varbinary(max) NULL,
        ColumnPermissions               int NULL,
        LinkedServerName                nvarchar(256) NULL,
        ProviderName                    nvarchar(256) NULL,
        MethodName                      nvarchar(256) NULL,
        RowCounts                       bigint NULL,
        RequestID                       int NULL,
        XactSequence                    bigint NULL,
        EventSequence                   bigint NOT NULL,
        BigintData1                     bigint NULL,
        BigintData2                     bigint NULL,
        GUID                            uniqueidentifier NULL,
        IntegerData2                    int NULL,
        ObjectID2                       bigint NULL,
        Type                            int NULL,
        OwnerID                         int NULL,
        ParentName                      nvarchar(256) NULL,
        IsSystem                        int NULL,
        Offset                          int NULL,
        SourceDatabaseID                int NULL,
        SqlHandle                       varbinary(64) NULL,
        SessionLoginName                nvarchar(256) NULL,
        PlanHandle                      varbinary(64) NULL, 
        GroupID                         int NULL

    )  ON [PRIMARY];

    ALTER TABLE [snapshots].[trace_data]
        ADD CONSTRAINT [CHK_trace_data_check_operator] CHECK (core.fn_check_operator(snapshot_id) = 1);
END;
GO

-- trace_data.IDX_trace_data_EventSequence
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('IDX_trace_data_EventSequence', 'snapshot_id', 0, 0), 
    ('IDX_trace_data_EventSequence', 'EventSequence', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'trace_data', 
    @object_type = 'INDEX', @constraint_or_index_name = 'IDX_trace_data_EventSequence', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- trace_data.IDX_trace_data_trace_info_id
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('IDX_trace_data_trace_info_id', 'trace_info_id', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'trace_data', 
    @object_type = 'INDEX', @constraint_or_index_name = 'IDX_trace_data_trace_info_id', 
    @ignore_dup_key = 0, @clustered = 0;
GO

-- trace_data.IDX_trace_data_EventSequence
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('IDX_trace_data_StartTime_EventClass', 'StartTime', 0, 0), 
    ('IDX_trace_data_StartTime_EventClass', 'EventClass', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'trace_data', 
    @object_type = 'INDEX', @constraint_or_index_name = 'IDX_trace_data_StartTime_EventClass', 
    @ignore_dup_key = 0, @clustered = 0;
GO

-- trace_data.FK_trace_data_trace_info_id
IF OBJECT_ID ('snapshots.FK_trace_data_trace_info_id', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_trace_data_trace_info_id] on snapshots.trace_data ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[trace_data]
        ADD CONSTRAINT [FK_trace_data_trace_info_id] FOREIGN KEY(trace_info_id)
        REFERENCES [snapshots].[trace_info] (trace_info_id);
END;
GO

-- trace_data.FK_trace_data_snapshot_id
IF OBJECT_ID ('snapshots.FK_trace_data_snapshot_id', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_trace_data_snapshot_id] on snapshots.trace_data ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[trace_data]
        ADD CONSTRAINT [FK_trace_data_snapshot_id] FOREIGN KEY(snapshot_id)
        REFERENCES [core].[snapshots_internal] (snapshot_id) ON DELETE CASCADE;
END;
GO


-- This stored proc inserts a new row int the snapshots.trace_info table
IF (NOT OBJECT_ID(N'snapshots.sp_trace_get_info', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[sp_trace_get_info] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[sp_trace_get_info]
END
GO 

RAISERROR('Creating procedure [snapshots].[sp_trace_get_info] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[sp_trace_get_info]
    @source_id           int,               -- references source of data from core.source_info_internal
    @collection_item_id  int,               -- idenitfies the collection item id within the source info
    @start_time          datetime,          -- time when the trace has started
    @last_event_sequence bigint OUTPUT,     -- returns the event sequence number for last trace event uploaded, or 0
    @trace_info_id       int OUTPUT         -- returns id of trace_info record
AS
BEGIN
    SET NOCOUNT ON;

    -- Security check (role membership)
    IF (NOT (ISNULL(IS_MEMBER(N'mdw_writer'), 0) = 1) AND NOT (ISNULL(IS_SRVROLEMEMBER(N'sysadmin'), 0) = 1))
    BEGIN
        RAISERROR(14677, 16, -1, 'mdw_writer');
        RETURN(1); -- Failure
    END;

    -- Parameters check - mandatory parameters
    IF (@source_id IS NULL)
    BEGIN
        RAISERROR(14200, -1, -1, '@source_id')
        RETURN(1) -- Failure
    END;

    IF (@collection_item_id IS NULL)
    BEGIN
        RAISERROR(14200, -1, -1, '@collection_item_id')
        RETURN(1) -- Failure
    END;

    IF (@start_time IS NULL)
    BEGIN
        RAISERROR(14200, -1, -1, '@start_time')
        RETURN(1) -- Failure
    END;

    SELECT 
        @trace_info_id = trace_info_id,
        @last_event_sequence = last_event_sequence 
    FROM snapshots.trace_info ti
    WHERE 
        ti.source_id = @source_id
        AND ti.collection_item_id = @collection_item_id
        AND ti.is_running = 1
        AND ti.start_time = @start_time;

    IF (@trace_info_id IS NULL)
    BEGIN
        SELECT @last_event_sequence = 0;

        -- Insert new record
        INSERT INTO [snapshots].[trace_info]
        (
            source_id,
            collection_item_id,
            start_time,
            is_running,
            last_event_sequence
        )
        VALUES
        (
            @source_id,
            @collection_item_id,
            @start_time,
            1,
            @last_event_sequence
        );
        SELECT @trace_info_id = SCOPE_IDENTITY();
    END;

    RETURN (0);
END
GO


-- This stored proc updates a row in the snapshots.trace_info table
IF (NOT OBJECT_ID(N'snapshots.sp_trace_update_info', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[sp_trace_update_info] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[sp_trace_update_info]
END
GO 

RAISERROR('Creating procedure [snapshots].[sp_trace_update_info] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[sp_trace_update_info]
    @trace_info_id       int,
    @snapshot_id         int,
    @last_event_sequence bigint,
    @is_running          bit,
    @event_count         bigint,
    @dropped_event_count int
AS
BEGIN
    SET NOCOUNT ON;

    -- Security check (role membership)
    IF (NOT (ISNULL(IS_MEMBER(N'mdw_writer'), 0) = 1) AND NOT (ISNULL(IS_SRVROLEMEMBER(N'sysadmin'), 0) = 1))
    BEGIN
        RAISERROR(14677, 16, -1, 'mdw_writer');
        RETURN(1); -- Failure
    END;

    -- Parameters check - mandatory parameters
    IF (@trace_info_id IS NULL)
    BEGIN
        RAISERROR(14200, -1, -1, '@trace_info_id')
        RETURN(1) -- Failure
    END;

    IF NOT EXISTS (SELECT trace_info_id from snapshots.trace_info where trace_info_id = @trace_info_id)
    BEGIN
        DECLARE @trace_info_id_as_char NVARCHAR(10)
        SELECT @trace_info_id_as_char = CONVERT(NVARCHAR(36), @trace_info_id)
        RAISERROR(14679, -1, -1, N'@trace_info_id', @trace_info_id_as_char)
        RETURN(1) -- Failure
    END;

    IF (@snapshot_id IS NULL)
    BEGIN
        RAISERROR(14200, -1, -1, '@snapshot_id')
        RETURN(1) -- Failure
    END;

    IF NOT EXISTS (SELECT snapshot_id from core.snapshots where snapshot_id = @snapshot_id)
    BEGIN
        DECLARE @snapshot_id_as_char NVARCHAR(36)
        SELECT @snapshot_id_as_char = CONVERT(NVARCHAR(36), @snapshot_id)
        RAISERROR(14679, -1, -1, N'@snapshot_id', @snapshot_id_as_char)
        RETURN(1) -- Failure
    END;

    IF (@last_event_sequence IS NULL)
    BEGIN
        RAISERROR(14200, -1, -1, '@last_event_sequence')
        RETURN(1) -- Failure
    END;

    IF (@is_running IS NULL)
    BEGIN
        RAISERROR(14200, -1, -1, '@is_running')
        RETURN(1) -- Failure
    END;

    IF (@event_count IS NULL)
    BEGIN
        RAISERROR(14200, -1, -1, '@event_count')
        RETURN(1) -- Failure
    END;

    IF (@dropped_event_count IS NULL)
    BEGIN
        RAISERROR(14200, -1, -1, '@dropped_event_count')
        RETURN(1) -- Failure
    END;

    -- Update existing record
    UPDATE [snapshots].[trace_info]
    SET
        last_snapshot_id = @snapshot_id,
        last_event_sequence = @last_event_sequence,
        is_running = @is_running,
        event_count = ISNULL(event_count,0) + @event_count,
        dropped_event_count = @dropped_event_count
    WHERE
        trace_info_id = @trace_info_id;

    RETURN(0);
END
GO


-- This function returns all data captured for the specified trace
IF (NOT OBJECT_ID(N'snapshots.fn_trace_gettable', 'IF') IS NULL)
BEGIN
    RAISERROR('Dropping function [snapshots].[fn_trace_gettable] ...', 0, 1)  WITH NOWAIT;
    DROP FUNCTION [snapshots].[fn_trace_gettable]
END
GO 

RAISERROR('Creating function [snapshots].[fn_trace_gettable] ...', 0, 1)  WITH NOWAIT;
GO
CREATE FUNCTION [snapshots].[fn_trace_gettable]  
(
    @trace_info_id       int,
    @start_time          datetimeoffset(7) = NULL,
    @end_time            datetimeoffset(7) = NULL
) 
RETURNS TABLE
AS
RETURN
(
    SELECT 
        TextData,
        BinaryData,
        DatabaseID,
        TransactionID,
        LineNumber,
        NTUserName,
        NTDomainName,
        HostName,
        ClientProcessID,
        ApplicationName,
        LoginName,
        SPID,
        Duration,
        StartTime,
        EndTime,
        Reads,
        Writes,
        CPU,
        Permissions,
        Severity,
        EventSubClass,
        ObjectID,
        Success,
        IndexID,
        IntegerData,
        ServerName,
        EventClass,
        ObjectType,
        NestLevel,
        State,
        Error,
        Mode,
        Handle,
        ObjectName,
        DatabaseName,
        FileName,
        OwnerName,
        RoleName,
        TargetUserName,
        DBUserName,
        LoginSid,
        TargetLoginName,
        TargetLoginSid,
        ColumnPermissions,
        LinkedServerName,
        ProviderName,
        MethodName,
        RowCounts,
        RequestID,
        XactSequence,
        EventSequence,
        BigintData1,
        BigintData2,
        GUID,
        IntegerData2,
        ObjectID2,
        Type,
        OwnerID,
        ParentName,
        IsSystem,
        Offset,
        SourceDatabaseID,
        SqlHandle,
        SessionLoginName,
        PlanHandle
    FROM snapshots.trace_data
    WHERE
        trace_info_id = @trace_info_id
        AND StartTime >= ISNULL(@start_time, '1753-01-01')
        AND StartTime <= ISNULL(@end_time, '9999-12-31')
)
GO

--
-- GENERAL TABLES FOR SYSTEM COLLECTION SETS
--

-- Association of batch text with sql_handle and object_name
--
IF (OBJECT_ID(N'snapshots.notable_query_text', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[notable_query_text]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[notable_query_text] (
        [sql_handle]        varbinary(64) NOT NULL,
        [database_id]       smallint      NULL,
        [object_id]         int           NULL,
        [object_name]       nvarchar(128) NULL,
        [sql_text]          nvarchar(max) NULL,
        [source_id]         int           NOT NULL
    ) ON [PRIMARY]
END
GO

-- notable_query_text.PK_notable_query_text
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_notable_query_text', 'source_id', 0, 0), 
    ('PK_notable_query_text', 'sql_handle', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'notable_query_text', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_notable_query_text', 
    @ignore_dup_key = 1, @clustered = 1; -- IGNORE_DUP_KEY
GO

-- notable_query_text.FK_distinct_query_to_handle_notable_query_text
IF OBJECT_ID ('snapshots.FK_notable_query_text_source_info_internal', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_distinct_query_to_handle_notable_query_text] on snapshots.notable_query_text ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[notable_query_text] ADD CONSTRAINT [FK_notable_query_text_source_info_internal] FOREIGN KEY([source_id])
        REFERENCES [core].[source_info_internal] (source_id)
        ON DELETE CASCADE
END;


-- Association of batch plan with sql_handle and plan_handle and object_name
--
IF (OBJECT_ID(N'snapshots.notable_query_plan', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[notable_query_plan]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[notable_query_plan] (
        [sql_handle]                varbinary(64)   NOT NULL,
        [plan_handle]               varbinary(64)   NOT NULL,
        [statement_start_offset]    int             NOT NULL,
        [statement_end_offset]      int             NOT NULL,
        [plan_generation_num]       bigint          NOT NULL,
        [creation_time]             datetimeoffset(7)  NOT NULL,
        [database_id]               smallint        NULL,
        [object_id]                 int             NULL,
        [object_name]               nvarchar(128)   NULL,
        [query_plan]                nvarchar(max)   NULL,
        [source_id]                 int             NOT NULL,
    ) ON [PRIMARY]
END;

-- notable_query_plan.PK_notable_query_plan
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_notable_query_plan', 'source_id', 0, 0), 
    ('PK_notable_query_plan', 'sql_handle', 0, 0), 
    ('PK_notable_query_plan', 'plan_handle', 0, 0), 
    ('PK_notable_query_plan', 'statement_start_offset', 0, 0), 
    ('PK_notable_query_plan', 'statement_end_offset', 0, 0), 
    ('PK_notable_query_plan', 'creation_time', 0, 0), 
    ('PK_notable_query_plan', 'plan_generation_num', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'notable_query_plan', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_notable_query_plan', 
    @ignore_dup_key = 1, @clustered = 1;
GO

-- notable_query_plan.IDX_notable_query_plan_plan_handle
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('IDX_notable_query_plan_plan_handle', 'source_id', 0, 0), 
    ('IDX_notable_query_plan_plan_handle', 'plan_handle', 0, 0), 
    ('IDX_notable_query_plan_plan_handle', 'statement_start_offset', 0, 0), 
    ('IDX_notable_query_plan_plan_handle', 'statement_end_offset', 0, 0), 
    ('IDX_notable_query_plan_plan_handle', 'creation_time', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'notable_query_plan', 
    @object_type = 'INDEX', @constraint_or_index_name = 'IDX_notable_query_plan_plan_handle', 
    @ignore_dup_key = 0, @clustered = 0;
GO

-- notable_query_plan.FK_notable_query_plan_source_info_internal
IF OBJECT_ID ('snapshots.FK_notable_query_plan_source_info_internal', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_notable_query_plan_source_info_internal] on snapshots.notable_query_plan ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[notable_query_plan] ADD CONSTRAINT [FK_notable_query_plan_source_info_internal] FOREIGN KEY([source_id])
        REFERENCES [core].[source_info_internal] (source_id)
        ON DELETE CASCADE;
END;
GO


IF (OBJECT_ID(N'snapshots.distinct_queries', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[distinct_queries]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[distinct_queries] (
        [distinct_query_hash]   bigint          NOT NULL,
        [distinct_sql_text]     nvarchar(512)   NOT NULL,
        [source_id]             int             NOT NULL,
    );
END;
GO

-- distinct_queries.PK_distinct_queries
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_distinct_queries', 'source_id', 0, 0), 
    ('PK_distinct_queries', 'distinct_query_hash', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'distinct_queries', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_distinct_queries', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- distinct_queries.FK_distinct_queries_source_info_internal
IF OBJECT_ID ('snapshots.FK_distinct_queries_source_info_internal', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_distinct_queries_source_info_internal] on snapshots.distinct_queries ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[distinct_queries] ADD CONSTRAINT [FK_distinct_queries_source_info_internal] FOREIGN KEY([source_id])
        REFERENCES [core].[source_info_internal] (source_id)
        ON DELETE CASCADE
END;
GO


IF (OBJECT_ID(N'snapshots.distinct_query_to_handle', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[distinct_query_to_handle]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[distinct_query_to_handle] (
        [distinct_query_hash]   bigint          NOT NULL,
        [sql_handle]            varbinary(64)   NOT NULL,
        [source_id]             int             NOT NULL
    ) ON [PRIMARY]
END
GO

-- distinct_query_to_handle.PK_distinct_query_to_handle
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_distinct_query_to_handle', 'source_id', 0, 0), 
    ('PK_distinct_query_to_handle', 'distinct_query_hash', 0, 0), 
    ('PK_distinct_query_to_handle', 'sql_handle', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'distinct_query_to_handle', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_distinct_query_to_handle', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- distinct_query_to_handle.FK_distinct_query_to_handle_notable_query_text
IF OBJECT_ID ('snapshots.FK_distinct_query_to_handle_notable_query_text', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key snapshots.distinct_query_to_handle.FK_distinct_query_to_handle_notable_query_text...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[distinct_query_to_handle] ADD CONSTRAINT [FK_distinct_query_to_handle_notable_query_text] FOREIGN KEY([source_id], [sql_handle])
        REFERENCES [snapshots].[notable_query_text] ([source_id], [sql_handle])
END;
GO

-- distinct_query_to_handle.FK_distinct_query_to_handle_distinct_queries
IF OBJECT_ID ('snapshots.FK_distinct_query_to_handle_distinct_queries', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key snapshots.distinct_query_to_handle.FK_distinct_query_to_handle_distinct_queries...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[distinct_query_to_handle] ADD CONSTRAINT [FK_distinct_query_to_handle_distinct_queries] FOREIGN KEY([source_id], [distinct_query_hash])
        REFERENCES [snapshots].[distinct_queries] ([source_id], [distinct_query_hash])
END;
GO

-- distinct_query_to_handle.FK_distinct_query_to_handle_source_info_internal
IF OBJECT_ID ('snapshots.FK_distinct_query_to_handle_source_info_internal', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key snapshots.distinct_query_to_handle.FK_distinct_query_to_handle_source_info_internal...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[distinct_query_to_handle] ADD CONSTRAINT [FK_distinct_query_to_handle_source_info_internal] FOREIGN KEY([source_id])
        REFERENCES [core].[source_info_internal] (source_id)
        ON DELETE CASCADE
END;
GO


-- This function returns fragment within sql query text in the specified start_offset and end_offset range (offsets are specified in bytes)
IF (NOT OBJECT_ID(N'snapshots.fn_get_query_fragment', 'FN') IS NULL)
BEGIN
    RAISERROR('Dropping function [snapshots].[fn_get_query_fragment] ...', 0, 1)  WITH NOWAIT;
    DROP FUNCTION [snapshots].[fn_get_query_fragment]
END
GO 
RAISERROR('Creating function [snapshots].[fn_get_query_fragment] ...', 0, 1)  WITH NOWAIT;
GO
CREATE FUNCTION [snapshots].[fn_get_query_fragment](
    @sqltext nvarchar(max),
    @start_offset int, 
    @end_offset int
)
RETURNS NVARCHAR(MAX)
BEGIN
    DECLARE @query_text NVARCHAR(MAX)
    
    DECLARE @query_text_length int
    SET @query_text_length = DATALENGTH(@sqltext) 

    -- If start_offset was set as null, default to starting byte 0
    IF (@start_offset IS NULL)
    BEGIN
        SET @start_offset = 0
    END 

    -- Validate start_offset, return this function if  offset is less than 0
    -- Validate sqltext, if input is NULL, we dont need to continue
    IF (@start_offset < 0 OR @sqltext IS NULL)
    BEGIN
        -- exceptions are not thrown here because caller calls this function on a report query where
        -- throwing exceptions would abort report rendering
        RETURN @query_text
    END

    -- ending position of the query that the row describes within the text of its batch or persisted object. 
    -- value of -1 indicates the end of the batch.
    IF (@end_offset IS NULL OR @end_offset = -1 )
    BEGIN
        SET @end_offset = @query_text_length
    END 

    -- Set the offset to closest even number. Ex: start_offset = 5, set as start_offset = 4th byte
    SET @start_offset = CEILING(@start_offset/2) *2
    SET @end_offset = CEILING(@end_offset/2) *2

    -- Validate start and end offsets
    IF (@start_offset <= @query_text_length    -- start offset should be  less than length of query string
        AND @end_offset <= @query_text_length      -- end offset should be  less than length of query string
        AND @start_offset <= @end_offset)          -- start offset should be less than end offset
    BEGIN
        -- start and end offsets are the byte's position as reported  in DMV sys.dm_exec_query_stats.
        -- sqltext has a NVARCHAR string where every character takes 2 bytes. SUBSTRING() deals with starting character's position 
        -- and length of characters from starting position. so, we are dividing by 2 to convert byte position to character position 
        SELECT @query_text = SUBSTRING(@sqltext, @start_offset/2, (@end_offset - @start_offset)/2 + 1)
    END

    RETURN @query_text
END
GO

-- This function returns text and database and object context for a query identified by a sql_handle
IF (NOT OBJECT_ID(N'snapshots.fn_get_query_text', 'TF') IS NULL)
BEGIN
    RAISERROR('Dropping function [snapshots].[fn_get_query_text] ...', 0, 1)  WITH NOWAIT;
    DROP FUNCTION [snapshots].[fn_get_query_text]
END
GO 

RAISERROR('Creating function [snapshots].[fn_get_query_text] ...', 0, 1)  WITH NOWAIT;
GO
CREATE FUNCTION [snapshots].[fn_get_query_text](
    @source_id int,
    @sql_handle varbinary(64), 
    @statement_start_offset int, 
    @statement_end_offset int
)
RETURNS @query_text TABLE (database_id smallint NULL, object_id int NULL, object_name sysname NULL, query_text nvarchar(max) NULL)

BEGIN
    IF @sql_handle IS NOT NULL AND 
       EXISTS (SELECT sql_handle FROM snapshots.notable_query_text WHERE sql_handle = @sql_handle AND source_id = @source_id)
    BEGIN
        INSERT INTO @query_text 
        (
            database_id, 
            object_id, 
            object_name, 
            query_text
        ) 
        SELECT 
            t.database_id,
            t.object_id,
            t.object_name,
            [snapshots].[fn_get_query_fragment](t.sql_text, @statement_start_offset, @statement_end_offset)
        FROM
            snapshots.notable_query_text t
        WHERE
            t.sql_handle = @sql_handle
            AND t.source_id = @source_id
    END

    RETURN
END
GO

-- This stored procedure returns all rows from notable_query_text table 
-- where sql_text value is NULL, meaning we did not caputure the text for that query yet.
IF (NOT OBJECT_ID(N'snapshots.sp_get_unknown_query_text', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[sp_get_unknown_query_text] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[sp_get_unknown_query_text]
END
GO 

RAISERROR('Creating procedure [snapshots].[sp_get_unknown_query_text] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[sp_get_unknown_query_text]
    @source_id  int
AS
BEGIN
    SET NOCOUNT ON;

    SELECT
        [source_id],
        [sql_handle]
    FROM
        [snapshots].[notable_query_text]
    WHERE
        [source_id] = @source_id
        AND [sql_text] IS NULL
END;
GO

-- This stored procedure returns all rows from notable_query_plan table 
-- where sql_text value is NULL, meaning we did not caputure the plan for that query yet.
IF (NOT OBJECT_ID(N'snapshots.sp_get_unknown_query_plan', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[sp_get_unknown_query_plan] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[sp_get_unknown_query_plan]
END
GO 

RAISERROR('Creating procedure [snapshots].[sp_get_unknown_query_plan] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[sp_get_unknown_query_plan]
    @source_id       int
AS
BEGIN
    SET NOCOUNT ON;

    SELECT
        [source_id],
        [sql_handle],
        [plan_handle],
        [statement_start_offset],
        [statement_end_offset],
        [plan_generation_num]
    FROM
        [snapshots].[notable_query_plan]
    WHERE
        [source_id] = @source_id
        AND [query_plan] IS NULL
END;
GO

-- Updates a single row with new text for the sql_handle
IF (NOT OBJECT_ID(N'snapshots.sp_update_query_text', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[sp_update_query_text] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[sp_update_query_text]
END
GO 

RAISERROR('Creating procedure [snapshots].[sp_update_query_text] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[sp_update_query_text]
    @source_id       int,
    @sql_handle      varbinary(64),
    @database_id     smallint     ,
    @object_id       int          ,
    @object_name     nvarchar(128),
    @sql_text        nvarchar(max)
AS
BEGIN
    SET NOCOUNT ON;

    UPDATE [snapshots].[notable_query_text]
    SET
        database_id = @database_id,
        object_id   = @object_id,
        object_name = @object_name,
        sql_text    = @sql_text
    WHERE
        source_id = @source_id 
        AND sql_handle = @sql_handle
        
END;
GO

-- Updates a single row with new plan for the plan_handle
IF (NOT OBJECT_ID(N'snapshots.sp_update_query_plan', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[sp_update_query_plan] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[sp_update_query_plan]
END
GO 

RAISERROR('Creating procedure [snapshots].[sp_update_query_plan] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[sp_update_query_plan]
    @source_id                 int,
    @sql_handle                varbinary(64),
    @plan_handle               varbinary(64),
    @statement_start_offset    int          ,
    @statement_end_offset      int          ,
    @plan_generation_num       bigint       ,
    @database_id               smallint     ,
    @object_id                 int          ,
    @object_name               nvarchar(128),
    @query_plan                nvarchar(max)
AS
BEGIN
    SET NOCOUNT ON;

    UPDATE [snapshots].[notable_query_plan]
    SET
        database_id     = @database_id,
        object_id       = @object_id,
        object_name     = @object_name,
        query_plan      = @query_plan
    WHERE
        source_id = @source_id
        AND sql_handle = @sql_handle
        AND plan_handle = @plan_handle
        AND statement_start_offset = @statement_start_offset
        AND statement_end_offset = @statement_end_offset
        AND plan_generation_num = @plan_generation_num

END;
GO




--
-- SERVER ACTIVITY COLLECTION SET SUPPORT
--

-- os_wait_stats table
--
IF (OBJECT_ID(N'snapshots.os_wait_stats', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[os_wait_stats]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[os_wait_stats](
        [wait_type]             nvarchar(45) NOT NULL,
        [waiting_tasks_count]   bigint NOT NULL,
        [wait_time_ms]          bigint NOT NULL,
        [signal_wait_time_ms]   bigint NOT NULL,
        [collection_time]       datetimeoffset(7) NOT NULL,
        [snapshot_id]           int NOT NULL,
    );
    
    ALTER TABLE [snapshots].[os_wait_stats]  WITH CHECK ADD  CONSTRAINT [CHK_os_wait_stats_check_operator] CHECK  (([core].[fn_check_operator]([snapshot_id])=(1)))
    
    CREATE STATISTICS [STAT_os_wait_stats1] ON [snapshots].[os_wait_stats](
        [collection_time], 
        [snapshot_id], 
        [wait_type]
    )

    CREATE STATISTICS [STAT_os_wait_stats2] ON [snapshots].[os_wait_stats](
        [collection_time], 
        [wait_type]
    )

END;
GO

-- os_wait_stats.PK_os_wait_stats
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_os_wait_stats', 'snapshot_id', 0, 0), 
    ('PK_os_wait_stats', 'collection_time', 0, 0), 
    ('PK_os_wait_stats', 'wait_type', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'os_wait_stats', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_os_wait_stats', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- os_wait_stats.IDX_os_wait_stats1
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('IDX_os_wait_stats1', 'collection_time', 0, 1), 
    ('IDX_os_wait_stats1', 'snapshot_id', 0, 0), 
    ('IDX_os_wait_stats1', 'wait_type', 1, 0), 
    ('IDX_os_wait_stats1', 'signal_wait_time_ms', 1, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'os_wait_stats', 
    @object_type = 'INDEX', @constraint_or_index_name = 'IDX_os_wait_stats1', 
    @ignore_dup_key = 0, @clustered = 0;
GO

-- os_wait_stats.IDX_os_wait_stats1
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('IDX_os_wait_stats2', 'snapshot_id', 0, 0), 
    ('IDX_os_wait_stats2', 'collection_time', 0, 0); 
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'os_wait_stats', 
    @object_type = 'INDEX', @constraint_or_index_name = 'IDX_os_wait_stats2', 
    @ignore_dup_key = 0, @clustered = 0;
GO

-- os_wait_stats.FK_os_wait_stats_snapshots_internal
IF OBJECT_ID ('snapshots.FK_os_wait_stats_snapshots_internal', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_os_wait_stats_snapshots_internal] on snapshots.os_wait_stats ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[os_wait_stats] ADD CONSTRAINT [FK_os_wait_stats_snapshots_internal] FOREIGN KEY([snapshot_id])
        REFERENCES [core].[snapshots_internal] ([snapshot_id])
        ON DELETE CASCADE
END;
GO


-- os_latch_stats table
--
IF (OBJECT_ID(N'snapshots.os_latch_stats', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[os_latch_stats]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[os_latch_stats](
        [latch_class]               nvarchar(45) NOT NULL,
        [waiting_requests_count]    bigint NOT NULL,
        [wait_time_ms]              bigint NOT NULL,
        [collection_time]           datetimeoffset(7) NOT NULL,
        [snapshot_id]               int NOT NULL,
    );
    
    ALTER TABLE [snapshots].[os_latch_stats] WITH CHECK ADD CONSTRAINT [CHK_os_latch_stats_check_operator] CHECK (([core].[fn_check_operator]([snapshot_id])=(1)))
END;
GO

-- os_latch_stats.PK_os_latch_stats
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_os_latch_stats', 'snapshot_id', 0, 0), 
    ('PK_os_latch_stats', 'collection_time', 0, 0), 
    ('PK_os_latch_stats', 'latch_class', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'os_latch_stats', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_os_latch_stats', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- os_latch_stats.FK_os_latch_stats_snapshots_internal
IF OBJECT_ID ('snapshots.FK_os_latch_stats_snapshots_internal', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_os_latch_stats_snapshots_internal] on snapshots.os_latch_stats ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[os_latch_stats] ADD CONSTRAINT [FK_os_latch_stats_snapshots_internal] FOREIGN KEY([snapshot_id])
        REFERENCES [core].[snapshots_internal] ([snapshot_id])
        ON DELETE CASCADE
END;
GO


IF (OBJECT_ID(N'snapshots.active_sessions_and_requests', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[active_sessions_and_requests]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[active_sessions_and_requests](
        [row_id]                    int NOT NULL,
        [session_id]                smallint NOT NULL,
        [request_id]                int NOT NULL,
        [exec_context_id]           int NOT NULL,
        [blocking_session_id]       smallint NULL,
        [blocking_exec_context_id]  int NULL,
        [scheduler_id]              int NULL,
        [database_name]             nvarchar(128) NULL,
        [user_id]                   int NULL,
        [task_state]                nvarchar(10) NULL,
        [request_status]            nvarchar(15) NULL,
        [session_status]            nvarchar(15) NOT NULL,
        [executing_managed_code]    bit NULL,
        [login_time]                datetimeoffset(7) NOT NULL,
        [is_user_process]           bit NOT NULL,
        [host_name]                 nvarchar(128) NOT NULL,
        [program_name]              nvarchar(128) NOT NULL,
        [login_name]                nvarchar(30) NOT NULL,
        [wait_type]                 nvarchar(45) NOT NULL,
        [last_wait_type]            nvarchar(45) NOT NULL,
        [wait_duration_ms]          bigint NOT NULL,
        [wait_resource]             nvarchar(50) NOT NULL,
        [resource_description]      nvarchar(140) NOT NULL,
        [transaction_id]            bigint NULL,
        [open_transaction_count]    int NOT NULL,
        [transaction_isolation_level] smallint NULL,
        [request_cpu_time]          int NULL,
        [request_logical_reads]     bigint NULL,
        [request_reads]             bigint NULL,
        [request_writes]            bigint NULL,
        [request_total_elapsed_time] int NULL,
        [request_start_time]        datetimeoffset(7) NULL,
        [memory_usage]              int NOT NULL,
        [session_cpu_time]          int NOT NULL,
        [session_reads]             bigint NOT NULL,
        [session_writes]            bigint NOT NULL,
        [session_logical_reads]     bigint NOT NULL,
        [session_total_scheduled_time] int NOT NULL,
        [session_total_elapsed_time] int NOT NULL,
        [session_last_request_start_time] datetimeoffset(7) NOT NULL,
        [session_last_request_end_time] datetimeoffset(7) NULL,
        [open_resultsets]           int NULL,
        [session_row_count]         bigint NOT NULL,
        [prev_error]                int NOT NULL,
        [pending_io_count]          int NULL,
        [command]                   nvarchar(32) NULL,
        [plan_handle]               varbinary(64) NULL,
        [sql_handle]                varbinary(64) NULL,
        [statement_start_offset]    int NULL,
        [statement_end_offset]      int NULL,
        [collection_time]           datetimeoffset(7) NOT NULL,
        [snapshot_id]               int NOT NULL,
        [is_blocking]               bit NOT NULL,
    );

    ALTER TABLE [snapshots].[active_sessions_and_requests] WITH CHECK ADD CONSTRAINT [CHK_active_sessions_and_requests_check_operator] CHECK (([core].[fn_check_operator]([snapshot_id])=(1)))
END;
GO

-- active_sessions_and_requests.PK_active_sessions_and_requests
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_active_sessions_and_requests', 'snapshot_id', 0, 0), 
    ('PK_active_sessions_and_requests', 'collection_time', 0, 0), 
    ('PK_active_sessions_and_requests', 'row_id', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'active_sessions_and_requests', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_active_sessions_and_requests', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- active_sessions_and_requests.IDX_blocking_session_id
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('IDX_blocking_session_id', 'blocking_session_id', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'active_sessions_and_requests', 
    @object_type = 'INDEX', @constraint_or_index_name = 'IDX_blocking_session_id', 
    @ignore_dup_key = 0, @clustered = 0;
GO

-- active_sessions_and_requests.IDX_collection_time
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('IDX_collection_time', 'collection_time', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'active_sessions_and_requests', 
    @object_type = 'INDEX', @constraint_or_index_name = 'IDX_collection_time', 
    @ignore_dup_key = 0, @clustered = 0;
GO

-- active_sessions_and_requests.FK_active_sessions_and_requests_snapshots_internal
IF OBJECT_ID ('snapshots.FK_active_sessions_and_requests_snapshots_internal', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_active_sessions_and_requests_snapshots_internal] on snapshots.active_sessions_and_requests ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[active_sessions_and_requests] ADD CONSTRAINT [FK_active_sessions_and_requests_snapshots_internal] FOREIGN KEY([snapshot_id])
        REFERENCES [core].[snapshots_internal] ([snapshot_id])
        ON DELETE CASCADE
END;
GO


IF (OBJECT_ID(N'snapshots.os_schedulers', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[os_schedulers]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[os_schedulers] (
        [parent_node_id]            int    NOT NULL,
        [scheduler_id]                int    NOT NULL,
        [cpu_id]                    int NOT NULL,
        [status]                    nvarchar(60) NOT NULL,
        [is_idle]                    bit NOT NULL,
        [preemptive_switches_count]    int NOT NULL,
        [context_switches_count]    int NOT NULL,
        [yield_count]                int NOT NULL,
        [current_tasks_count]        int NOT NULL,
        [runnable_tasks_count]        int NOT NULL,
        [work_queue_count]            bigint NOT NULL,
        [pending_disk_io_count]        int NOT NULL,
        [collection_time]           datetimeoffset(7) NOT NULL,
        [snapshot_id]               int NOT NULL,
    );

    ALTER TABLE [snapshots].[os_schedulers] WITH CHECK ADD CONSTRAINT [CHK_os_schedulers_check_operator] CHECK (([core].[fn_check_operator]([snapshot_id])=(1)))
END;
GO

-- os_schedulers.PK_os_schedulers
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_os_schedulers', 'snapshot_id', 0, 0), 
    ('PK_os_schedulers', 'collection_time', 0, 0), 
    ('PK_os_schedulers', 'scheduler_id', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'os_schedulers', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_os_schedulers', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- os_schedulers.FK_os_schedulers_snapshots_internal
IF OBJECT_ID ('snapshots.FK_os_schedulers_snapshots_internal', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_os_schedulers_snapshots_internal] on snapshots.os_schedulers ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[os_schedulers] ADD CONSTRAINT [FK_os_schedulers_snapshots_internal] FOREIGN KEY([snapshot_id])
        REFERENCES [core].[snapshots_internal] ([snapshot_id])
        ON DELETE CASCADE
END;
GO


IF (OBJECT_ID(N'snapshots.os_memory_nodes', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[os_memory_nodes]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[os_memory_nodes] (
        [memory_node_id]                        smallint NOT NULL, 
        [virtual_address_space_reserved_kb]     bigint NOT NULL, 
        [virtual_address_space_committed_kb]    bigint NOT NULL, 
        [locked_page_allocations_kb]            bigint NOT NULL, 
        [single_pages_kb]                       bigint NOT NULL, 
        [multi_pages_kb]                        bigint NOT NULL, 
        [shared_memory_reserved_kb]             bigint NOT NULL, 
        [shared_memory_committed_kb]            bigint NOT NULL,
        [collection_time]                       datetimeoffset(7) NOT NULL,
        [snapshot_id]                           int NOT NULL,
    );

    ALTER TABLE [snapshots].[os_memory_nodes] WITH CHECK ADD CONSTRAINT [CHK_os_memory_nodes_check_operator] CHECK (([core].[fn_check_operator]([snapshot_id])=(1)))
END;
GO

-- os_memory_nodes.PK_os_memory_nodes
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_os_memory_nodes', 'snapshot_id', 0, 0), 
    ('PK_os_memory_nodes', 'collection_time', 0, 0), 
    ('PK_os_memory_nodes', 'memory_node_id', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'os_memory_nodes', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_os_memory_nodes', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- os_memory_nodes.FK_os_memory_nodes_snapshots_internal
IF OBJECT_ID ('snapshots.FK_os_memory_nodes_snapshots_internal', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_os_memory_nodes_snapshots_internal] on snapshots.os_memory_nodes ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[os_memory_nodes] ADD CONSTRAINT [FK_os_memory_nodes_snapshots_internal] FOREIGN KEY([snapshot_id])
        REFERENCES [core].[snapshots_internal] ([snapshot_id])
        ON DELETE CASCADE
END;
GO


-- NOTE: Used by System Activity collection set up to CTP6 Refresh/RC0. Not used in RTM and later. 
IF (OBJECT_ID(N'snapshots.os_process_memory', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[os_process_memory]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[os_process_memory] (
        [physical_memory_in_use_kb]              bigint NOT NULL,
        [large_page_allocations_kb]              bigint NOT NULL, 
        [locked_page_allocations_kb]             bigint NOT NULL, 
        [total_virtual_address_space_kb]         bigint NOT NULL,
        [virtual_address_space_reserved_kb]      bigint NOT NULL,
        [virtual_address_space_committed_kb]     bigint NOT NULL,
        [virtual_address_space_available_kb]     bigint NOT NULL,
        [page_fault_count]                       bigint NOT NULL,
        [memory_utilization_percentage]          int NOT NULL,
        [available_commit_limit_kb]              bigint NOT NULL,
        [process_physical_memory_low]            bit NOT NULL,
        [process_virtual_memory_low]             bit NOT NULL,
        [collection_time]                        datetimeoffset(7) NOT NULL,
        [snapshot_id]                            int NOT NULL,
    );

    ALTER TABLE [snapshots].[os_process_memory] WITH CHECK ADD CONSTRAINT [CHK_os_process_memory_check_operator] CHECK (([core].[fn_check_operator]([snapshot_id])=(1)))
END;
GO

-- os_process_memory.PK_os_process_memory
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_os_process_memory', 'snapshot_id', 0, 0), 
    ('PK_os_process_memory', 'collection_time', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'os_process_memory', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_os_process_memory', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- os_process_memory.FK_os_process_memory_snapshots_internal
IF OBJECT_ID ('snapshots.FK_os_process_memory_snapshots_internal', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_os_process_memory_snapshots_internal] on snapshots.os_process_memory ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[os_process_memory] ADD CONSTRAINT [FK_os_process_memory_snapshots_internal] FOREIGN KEY([snapshot_id])
        REFERENCES [core].[snapshots_internal] ([snapshot_id])
        ON DELETE CASCADE
END;
GO


IF (OBJECT_ID(N'snapshots.sql_process_and_system_memory', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[sql_process_and_system_memory]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[sql_process_and_system_memory](
        [sql_physical_memory_in_use_kb]             bigint NOT NULL,
        [sql_large_page_allocations_kb]             bigint NOT NULL,
        [sql_locked_page_allocations_kb]            bigint NOT NULL,
        [sql_total_virtual_address_space_kb]        bigint NOT NULL,
        [sql_virtual_address_space_reserved_kb]     bigint NOT NULL,
        [sql_virtual_address_space_committed_kb]    bigint NOT NULL,
        [sql_virtual_address_space_available_kb]    bigint NOT NULL,
        [sql_page_fault_count]                      bigint NOT NULL,
        [sql_memory_utilization_percentage]         int NOT NULL,
        [sql_available_commit_limit_kb]             bigint NOT NULL,
        [sql_process_physical_memory_low]           bit NOT NULL,
        [sql_process_virtual_memory_low]            bit NOT NULL,
        [system_total_physical_memory_kb]           bigint NOT NULL,
        [system_available_physical_memory_kb]       bigint NOT NULL,
        [system_total_page_file_kb]                 bigint NOT NULL,
        [system_available_page_file_kb]             bigint NOT NULL,
        [system_cache_kb]                           bigint NOT NULL,
        [system_kernel_paged_pool_kb]               bigint NOT NULL,
        [system_kernel_nonpaged_pool_kb]            bigint NOT NULL,
        [system_high_memory_signal_state]           bit NOT NULL,
        [system_low_memory_signal_state]            bit NOT NULL,
        [bpool_commit_target]                       bigint NOT NULL,
        [bpool_committed]                           bigint NOT NULL,
        [bpool_visible]                             bigint NOT NULL, 
        [collection_time]                           datetimeoffset(7) NOT NULL,
        [snapshot_id]                               int NOT NULL,
    );

    ALTER TABLE [snapshots].[sql_process_and_system_memory] WITH CHECK ADD CONSTRAINT [CHK_sql_process_and_system_memory_check_operator] CHECK (([core].[fn_check_operator]([snapshot_id])=(1)));
END;
GO

-- sql_process_and_system_memory.PK_sql_process_and_system_memory
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_sql_process_and_system_memory', 'snapshot_id', 0, 0), 
    ('PK_sql_process_and_system_memory', 'collection_time', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'sql_process_and_system_memory', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_sql_process_and_system_memory', 
    @ignore_dup_key = 0, @clustered = 1;
GO



-- sql_process_and_system_memory.FK_sql_process_and_system_memory_internal
IF OBJECT_ID ('snapshots.FK_sql_process_and_system_memory_internal', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_sql_process_and_system_memory_internal] on snapshots.sql_process_and_system_memory ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[sql_process_and_system_memory] ADD CONSTRAINT [FK_sql_process_and_system_memory_internal] FOREIGN KEY([snapshot_id])
        REFERENCES [core].[snapshots_internal] ([snapshot_id])
        ON DELETE CASCADE;
END;
GO


IF (OBJECT_ID(N'snapshots.os_memory_clerks', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[os_memory_clerks]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[os_memory_clerks] (
        [type]                                  nvarchar(60),
        [memory_node_id]                        smallint,
        [single_pages_kb]                       bigint,
        [multi_pages_kb]                        bigint,
        [virtual_memory_reserved_kb]            bigint,
        [virtual_memory_committed_kb]           bigint,
        [awe_allocated_kb]                      bigint,
        [shared_memory_reserved_kb]             bigint,
        [shared_memory_committed_kb]            bigint,
        [collection_time]                       datetimeoffset(7) NOT NULL,
        [snapshot_id]                           int NOT NULL
    ) ON [PRIMARY]

    ALTER TABLE [snapshots].[os_memory_clerks] WITH CHECK ADD CONSTRAINT [CHK_os_memory_clerks_check_operator] CHECK (([core].[fn_check_operator]([snapshot_id])=(1)))
END
GO

-- os_memory_clerks.CIDX_os_memory_clerks
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('CIDX_os_memory_clerks', 'snapshot_id', 0, 0), 
    ('CIDX_os_memory_clerks', 'collection_time', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'os_memory_clerks', 
    @object_type = 'INDEX', @constraint_or_index_name = 'CIDX_os_memory_clerks', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- os_memory_clerks.FK_os_memory_clerks_snapshots_internal
IF OBJECT_ID ('snapshots.FK_os_memory_clerks_snapshots_internal', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_os_memory_clerks_snapshots_internal] on snapshots.os_memory_clerks ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[os_memory_clerks] ADD CONSTRAINT [FK_os_memory_clerks_snapshots_internal] FOREIGN KEY([snapshot_id])
        REFERENCES [core].[snapshots_internal] ([snapshot_id])
        ON DELETE CASCADE
END;
GO


--
-- QUERY ACTIVITY COLLECTION SET SUPPORT
--

IF (OBJECT_ID(N'snapshots.query_stats', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[query_stats]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[query_stats] (
           [sql_handle]                varbinary(64) NOT NULL,
         [statement_start_offset]   int NOT NULL,
         [statement_end_offset]        int NOT NULL,
         [plan_generation_num]        bigint NOT NULL,
         [plan_handle]                varbinary(64) NOT NULL,
         [creation_time]            datetimeoffset(7) NOT NULL,
         [last_execution_time]        datetimeoffset(7) NOT NULL,
         [execution_count]            bigint NOT NULL,
         [snapshot_execution_count]    bigint NULL,
         [total_worker_time]        bigint NOT NULL,
         [snapshot_worker_time]        bigint NOT NULL,
         [min_worker_time]            bigint NULL,        -- NULLable b/c we can't calc min for queries that are still in-progress 
         [max_worker_time]            bigint NOT NULL,
         [total_physical_reads]        bigint NOT NULL,
         [snapshot_physical_reads]    bigint NOT NULL,
         [min_physical_reads]        bigint NULL,        -- NULLable b/c we can't calc min for queries that are still in-progress 
         [max_physical_reads]        bigint NOT NULL,
         [total_logical_writes]        bigint NOT NULL,
         [snapshot_logical_writes]    bigint NOT NULL,
         [min_logical_writes]        bigint NULL,        -- NULLable b/c we can't calc min for queries that are still in-progress 
         [max_logical_writes]        bigint NOT NULL,
         [total_logical_reads]        bigint NOT NULL,
         [snapshot_logical_reads]    bigint NOT NULL,
         [min_logical_reads]        bigint NULL,        -- NULLable b/c we can't calc min for queries that are still in-progress 
         [max_logical_reads]        bigint NOT NULL,
         [total_clr_time]            bigint NULL,        -- NULLable b/c dm_exec_requests doesn't expose this stat
         [snapshot_clr_time]        bigint NULL,        -- NULLable b/c dm_exec_requests doesn't expose this stat
         [min_clr_time]                bigint NULL,        -- NULLable b/c we can't calc min for queries that are still in-progress 
         [max_clr_time]                bigint NULL,        -- NULLable b/c dm_exec_requests doesn't expose this stat 
         [total_elapsed_time]        bigint NOT NULL,
         [snapshot_elapsed_time]    bigint NOT NULL,
         [min_elapsed_time]            bigint NULL,        -- NULLable b/c we can't calc min for queries that are still in-progress 
         [max_elapsed_time]            bigint NOT NULL,
         [collection_time]          datetimeoffset(7) NOT NULL,
         [snapshot_id]              int NOT NULL

    ) ON [PRIMARY]

    ALTER TABLE [snapshots].[query_stats] WITH CHECK ADD CONSTRAINT [CHK_query_stats_check_operator] CHECK (([core].[fn_check_operator]([snapshot_id])=(1)))
END
GO

-- query_stats.PK_query_stats
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('CIDX_query_stats', 'snapshot_id', 0, 0), 
    ('CIDX_query_stats', 'collection_time', 0, 0), 
    ('CIDX_query_stats', 'sql_handle', 0, 0), 
    ('CIDX_query_stats', 'statement_start_offset', 0, 0), 
    ('CIDX_query_stats', 'statement_end_offset', 0, 0), 
    ('CIDX_query_stats', 'plan_handle', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'query_stats', 
    @object_type = 'INDEX', @constraint_or_index_name = 'CIDX_query_stats', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- query_stats.FK_query_stats_snapshots_internal
IF OBJECT_ID ('snapshots.FK_query_stats_snapshots_internal', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_query_stats_snapshots_internal] on snapshots.query_stats ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[query_stats] ADD CONSTRAINT [FK_query_stats_snapshots_internal] FOREIGN KEY([snapshot_id])
        REFERENCES [core].[snapshots_internal] ([snapshot_id])
        ON DELETE CASCADE
END;
GO


IF (NOT OBJECT_ID(N'snapshots.distinct_query_stats', 'V') IS NULL)
BEGIN
    RAISERROR('Dropping view [snapshots].[distinct_query_stats]...', 0, 1)  WITH NOWAIT;
    DROP VIEW [snapshots].[distinct_query_stats]
END
GO

RAISERROR('Creating view [snapshots].[distinct_query_stats]...', 0, 1)  WITH NOWAIT;
GO
CREATE VIEW [snapshots].[distinct_query_stats]
AS 
    SELECT 
        dqth.distinct_query_hash,
        SUM(qs.execution_count) AS execution_count,
        SUM(qs.total_worker_time) AS total_worker_time,
        SUM(qs.total_physical_reads) AS total_physical_reads,
        SUM(qs.total_logical_reads) AS total_logical_reads,
        SUM(qs.total_logical_writes) AS total_logical_writes,
        SUM(qs.total_clr_time) AS total_clr_time,
        SUM(qs.total_elapsed_time) AS total_elapsed_time
    FROM
        [snapshots].[query_stats] qs
        JOIN [core].[snapshots_internal] s ON (s.snapshot_id = qs.snapshot_id)
        JOIN [snapshots].[distinct_query_to_handle] dqth ON (s.source_id = dqth.source_id AND qs.sql_handle = dqth.sql_handle)
    GROUP BY
        dqth.distinct_query_hash
GO



--
-- DISK USAGE COLLECTION SET SUPPORT
--

-- disk_usage table
--
IF (OBJECT_ID(N'snapshots.disk_usage', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[disk_usage]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[disk_usage](
        [dbsize] [bigint] NULL,
        [logsize] [bigint] NULL,
        [ftsize] [bigint] NULL,
        [reservedpages] [bigint] NULL,
        [usedpages] [bigint] NULL,
        [pages] [bigint] NULL,
        [database_name] [nvarchar](128) NOT NULL,
        [collection_time] datetimeoffset(7) NOT NULL,
        [snapshot_id] [int] NOT NULL, 
    ) ON [PRIMARY]

    ALTER TABLE [snapshots].[disk_usage]  WITH CHECK ADD  CONSTRAINT [CHK_disk_usage_check_operator] CHECK (([core].[fn_check_operator]([snapshot_id])=(1)))
END
GO

-- disk_usage.PK_disk_usage
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_disk_usage', 'snapshot_id', 0, 0), 
    ('PK_disk_usage', 'collection_time', 0, 0), 
    ('PK_disk_usage', 'database_name', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'disk_usage', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_disk_usage', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- disk_usage.FK_disk_usage_snapshots_internal
IF OBJECT_ID ('snapshots.FK_disk_usage_snapshots_internal', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_disk_usage_snapshots_internal] on snapshots.disk_usage ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[disk_usage]  WITH CHECK ADD  CONSTRAINT [FK_disk_usage_snapshots_internal] FOREIGN KEY([snapshot_id])
        REFERENCES [core].[snapshots_internal] ([snapshot_id])
        ON DELETE CASCADE
END;
GO


-- log_usage table
--
IF (OBJECT_ID(N'snapshots.log_usage', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[log_usage]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[log_usage](
        [database_name] [nvarchar](128) NOT NULL,
        [log_size_mb] [float] NULL,
        [log_space_used] [float] NULL,
        [status] [int] NULL,
        [collection_time] datetimeoffset(7) NOT NULL,
        [snapshot_id] [int] NOT NULL, 
    ) ON [PRIMARY]

    ALTER TABLE [snapshots].[log_usage] WITH CHECK ADD CONSTRAINT [CHK_log_usage_check_operator] CHECK (([core].[fn_check_operator]([snapshot_id])=(1)))
END
GO

-- log_usage.PK_log_usage
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_log_usage', 'snapshot_id', 0, 0), 
    ('PK_log_usage', 'collection_time', 0, 0), 
    ('PK_log_usage', 'database_name', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'log_usage', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_log_usage', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- log_usage.FK_log_usage_snapshots_internal
IF OBJECT_ID ('snapshots.FK_log_usage_snapshots_internal', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_log_usage_snapshots_internal] on snapshots.log_usage ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[log_usage] WITH CHECK ADD CONSTRAINT [FK_log_usage_snapshots_internal] FOREIGN KEY([snapshot_id])
        REFERENCES [core].[snapshots_internal] ([snapshot_id])
        ON DELETE CASCADE
END;
GO


-- io_virtual_file_stats table
--
IF (OBJECT_ID(N'snapshots.io_virtual_file_stats', 'U') IS NULL)
BEGIN
    RAISERROR('Creating table [snapshots].[io_virtual_file_stats]...', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[io_virtual_file_stats] (
        [database_name] [nvarchar](128) NOT NULL,
        [database_id] [int] NOT NULL,
        [logical_file_name] [nvarchar](128) NOT NULL,
        [file_id] [int] NOT NULL,
        [type_desc] [nvarchar](60) NULL,
        [logical_disk] [nvarchar](255) NOT NULL,
        [num_of_reads] [bigint] NULL,
        [num_of_bytes_read] [bigint] NULL,
        [io_stall_read_ms] [bigint] NULL,
        [num_of_writes] [bigint] NULL,
        [num_of_bytes_written] [bigint] NULL,
        [io_stall_write_ms] [bigint] NULL,
        [size_on_disk_bytes] [bigint] NULL,
        [collection_time] [datetimeoffset](7) NOT NULL,
        [snapshot_id] [int] NOT NULL, 
        
    ) ON [PRIMARY]

    ALTER TABLE [snapshots].[io_virtual_file_stats] WITH CHECK ADD CONSTRAINT [CHK_io_virtual_file_stats_check_operator] CHECK (([core].[fn_check_operator]([snapshot_id])=(1)))
END
GO

-- io_virtual_file_stats.PK_io_virtual_file_stats
TRUNCATE TABLE #index_key_columns;
INSERT INTO #index_key_columns (constraint_name, column_name, is_included_column, is_descending_key) 
VALUES 
    ('PK_io_virtual_file_stats', 'snapshot_id', 0, 0), 
    ('PK_io_virtual_file_stats', 'collection_time', 0, 0), 
    ('PK_io_virtual_file_stats', 'logical_disk', 0, 0), 
    ('PK_io_virtual_file_stats', 'database_name', 0, 0), 
    ('PK_io_virtual_file_stats', 'file_id', 0, 0);
EXEC #create_or_alter_primary_key_or_index
    @table_schema = 'snapshots', @table_name = 'io_virtual_file_stats', 
    @object_type = 'PRIMARY KEY', @constraint_or_index_name = 'PK_io_virtual_file_stats', 
    @ignore_dup_key = 0, @clustered = 1;
GO

-- io_virtual_file_stats.FK_io_virtual_file_stats
IF OBJECT_ID ('snapshots.FK_io_virtual_file_stats', 'F') IS NULL
BEGIN
    RAISERROR ('Creating foreign key [FK_io_virtual_file_stats] on snapshots.io_virtual_file_stats ...', 0, 1) WITH NOWAIT;
    ALTER TABLE [snapshots].[io_virtual_file_stats] WITH CHECK ADD CONSTRAINT [FK_io_virtual_file_stats] FOREIGN KEY([snapshot_id])
        REFERENCES [core].[snapshots_internal] ([snapshot_id])
        ON DELETE CASCADE
END;
GO



/**********************************************************************/
/* REPORTING STORED PROCEDURES                                       */
/**********************************************************************/

--
-- snapshots.rpt_snapshot_times
--  Returns all snapshot times for a given collection set.  
--  Used by (nearly) all reports to return data for the timeline chart that sits at the top of each report. 
--
-- Parameters: 
--    @ServerName - SQL Server instance name
--    @EndTime - End of the user-selected time window (UTC)
--    @WindowSize - Number of minutes in the time window 
--    @CollectionSetUid - GUID of the collection set with the snapshot_times that the report cares about
--
IF (NOT OBJECT_ID(N'snapshots.rpt_snapshot_times', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_snapshot_times] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_snapshot_times]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_snapshot_times] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROC [snapshots].[rpt_snapshot_times]
    @ServerName sysname, 
    @EndTime datetime, 
    @WindowSize int, 
    @CollectionSetUid uniqueidentifier
AS
BEGIN
    DECLARE @end_time_internal datetimeoffset(7)
    SET @end_time_internal = TODATETIMEOFFSET (@EndTime, '+00:00')

    -- Get the time of the earliest and latest snapshots for this collection set
    DECLARE @min_snapshot_time datetimeoffset(7)
    DECLARE @max_snapshot_time datetimeoffset(7)
    DECLARE @total_data_collection_window int
    SELECT @min_snapshot_time = MIN (snapshot_time), @max_snapshot_time = MAX (snapshot_time)
    FROM core.snapshots 
    WHERE instance_name = @ServerName
        AND collection_set_uid = @CollectionSetUid
    IF @min_snapshot_time IS NULL SET @min_snapshot_time = SYSDATETIMEOFFSET()
    IF @max_snapshot_time IS NULL SET @max_snapshot_time = SYSDATETIMEOFFSET()
    SET @total_data_collection_window = DATEDIFF (minute, @min_snapshot_time, @max_snapshot_time)

    -- First return all snapshot_time values for this collection set
    SELECT DISTINCT 
        CONVERT (datetime, SWITCHOFFSET (CAST (snapshot_time AS datetimeoffset(7)), '+00:00')) AS snapshot_time,
        1 AS value, 
        'AllSnapshotTimes' AS series_name
    FROM core.snapshots
    WHERE instance_name = @ServerName
        AND collection_set_uid = @CollectionSetUid
    UNION ALL
     
    -- Then return the snapshot_time values that are in the selected time window, with a different series label. 
    SELECT DISTINCT 
        CONVERT (datetime, SWITCHOFFSET (CAST (snapshot_time AS datetimeoffset(7)), '+00:00')) AS snapshot_time,
        0.8 AS value, 
        'SelectedSnapshotTimes' AS series_name
    FROM core.snapshots
    WHERE instance_name = @ServerName
        AND collection_set_uid = @CollectionSetUid
        AND snapshot_time BETWEEN DATEADD (minute, -1 * @WindowSize, @end_time_internal) AND @end_time_internal 
    UNION ALL 
    SELECT DISTINCT 
        DATEADD (millisecond, 10, CONVERT (datetime, SWITCHOFFSET (CAST (snapshot_time AS datetimeoffset(7)), '+00:00'))) AS snapshot_time,
        1.2 AS value, 
        'SelectedSnapshotTimes' AS series_name
    FROM core.snapshots
    WHERE instance_name = @ServerName
        AND collection_set_uid = @CollectionSetUid
        AND snapshot_time BETWEEN DATEADD (minute, -1 * @WindowSize, @end_time_internal) AND @end_time_internal 

    -- Return a "fake" data point (will not be plotted) so that the timeline always extends to the current time
    UNION ALL 
        SELECT GETUTCDATE() AS snapshot_time,
        -1 AS value, 
        'Formatting' AS in_selected_time_window

    -- Order is important here since points are plotted in the order in which they are returned from the query. 
    -- The "SelectedSnapshotTimes" series must be drawn on top of the "AllSnapshotTimes" series, so we return it 
    -- last. 
    ORDER BY series_name ASC, snapshot_time
END
GO


--
-- snapshots.rpt_interval_collection_times
--  Helper proc used by other procs that need to return data from N evenly-spaced intervals within a larger user-specified time window
--  Sample usage: 
--  
--      CREATE TABLE #intervals (
--          interval_time_id        int, 
--          interval_start_time     datetimeoffset(7),
--          interval_end_time       datetimeoffset(7),
--          interval_id             int, 
--          first_collection_time   datetimeoffset(7), 
--          last_collection_time    datetimeoffset(7), 
--          first_snapshot_id       int,
--          last_snapshot_id        int, 
--          -- the following columns may be ignored if @include_snapshot_detail = 0
--          source_id               int,                
--          snapshot_id             int,
--          collection_time         datetimeoffset(7),
--          collection_time_id      int
--      )
--      -- GUID 49268954-... is the Server Activity CS
--      INSERT INTO #intervals
--      EXEC [snapshots].[rpt_interval_collection_times] 
--          @ServerName, @EndTime, @WindowSize, 'snapshots.os_memory_clerks', '49268954-4FD4-4EB6-AA04-CD59D9BB5714', 40
--      
--      SELECT ... 
--      FROM snapshots.dm_os_memory_clerks AS mc 
--      JOIN #intervals AS col ON mc.collection_time = col.last_collection_time AND mc.snapshot_id = col.last_snapshot_id
--      WHERE ...
--
--
-- Parameters: 
--    @ServerName - SQL Server instance name
--    @EndTime - End of the user-selected time window (UTC)
--    @WindowSize - Number of minutes in the time window 
--    @TargetCollectionTable - Name of the table from which to harvest the collection_time values. Must have a datetimeoffset [collection_time] 
--        column and be in the [snapshots] or [custom_snapshots] schema. 
--    @CollectionSetUid - GUID of the collection set that populates @TargetCollectionTable
--    @interval_count int - Number of time intervals to divide the time window up into (default 40)
--    @include_snapshot_detail - 0 if the caller only wants 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_interval_collection_times', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_interval_collection_times] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_interval_collection_times]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_interval_collection_times] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_interval_collection_times]
    @ServerName sysname,
    @EndTime datetime = NULL,
    @WindowSize int = NULL, 
    @TargetCollectionTable sysname, 
    @CollectionSetUid varchar(64), 
    @interval_count int = 40, 
    @include_snapshot_detail bit = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @start_time_internal datetimeoffset(7);
    DECLARE @end_time_internal datetimeoffset(7);
    
    -- Start time should be passed in as a UTC datetime
    IF (@EndTime IS NOT NULL)
    BEGIN
        -- Assumed time zone offset for this conversion is +00:00 (datetime must be passed in as UTC)
        SET @end_time_internal = CAST (@EndTime AS datetimeoffset(7));
    END
    ELSE BEGIN
        SELECT @end_time_internal = MAX(snapshot_time)
        FROM core.snapshots 
        WHERE instance_name = @ServerName AND collection_set_uid = @CollectionSetUid
    END
    SET @start_time_internal = DATEADD (minute, -1 * @WindowSize, @end_time_internal);
    
    -- Get the earliest and latest snapshot_id values that could contain data for the selected time interval. 
    -- This will allow a more efficient query plan. 
    DECLARE @start_snapshot_id int;
    DECLARE @end_snapshot_id int;
    
    SELECT @start_snapshot_id = MIN (t.snapshot_id)
    FROM
    (
        SELECT TOP 2 s.snapshot_id
        FROM core.snapshots AS s 
        WHERE s.instance_name = @ServerName AND s.collection_set_uid = @CollectionSetUid
            AND s.snapshot_time < @start_time_internal
        ORDER BY s.snapshot_id DESC
    ) AS t
    
    SELECT @end_snapshot_id = MAX (t.snapshot_id) 
    FROM 
    (
        SELECT TOP 2 snapshot_id
        FROM core.snapshots AS s 
        WHERE s.instance_name = @ServerName AND s.collection_set_uid = @CollectionSetUid
            AND s.snapshot_time >= @end_time_internal
        ORDER BY s.snapshot_id ASC
    ) AS t
    
    IF @start_snapshot_id IS NULL SELECT @start_snapshot_id = MIN (snapshot_id) FROM core.snapshots
    IF @end_snapshot_id IS NULL SELECT @end_snapshot_id = MAX (snapshot_id) FROM core.snapshots
    
    -- Divide the time window up into N equal intervals.  
    -- First, calculate the duration of one interval, in minutes. 
    DECLARE @group_interval_min int
    SET @group_interval_min = ROUND (DATEDIFF (second, @start_time_internal, @end_time_internal) / 60.0 / @interval_count, 0)
    IF @group_interval_min = 0 SET @group_interval_min = 1
        
    IF (ISNULL (PARSENAME (@TargetCollectionTable, 2), 'snapshots') IN ('snapshots', 'custom_snapshots'))
    BEGIN
        /*
        Some explanation of the expressions used in the query below: 
        
        DATEDIFF (minute, ''20000101'', dataTable.collection_time) / @group_interval_min AS interval_time_id
            - @group_interval_min is the length of one time interval (one Nth of the selected time window), in minutes
            - "DATEDIFF (minute, ''20000101'', dataTable.collection_time)" converts each collection time into an integer (the # of minutes since a fixed reference date)
            - This value is divided by @group_interval_min to get an integer "interval ID".  The query uses this in the GROUP BY clause to group together all collection 
              times that fall within the same time interval.

        DATEADD (minute, (<<interval_id_expression>> * @group_interval_min, ''20000101'') AS interval_start_time
            - This uses (interval number) * (minutes/interval) + (reference date) to generate the time interval's start time 
            - The next column ([interval_end_time]) is the same as the above, except the time is calculated for the following interval ID (an interval's end time is 
              also the start time for the following time interval)
        */

        -- Get the collection times that fall within the specified time window, and compute the time interval ID for each collection time
        CREATE TABLE #snapshots (
            interval_time_id int, 
            interval_start_time datetimeoffset(7), 
            interval_end_time datetimeoffset(7), 
            interval_id int, 
            collection_time datetimeoffset(7), 
            source_id int, 
            snapshot_id int
        )

        -- Dynamic SQL will re-evaluate object permissions -- the current user must have SELECT permission on the target table. 
        DECLARE @sql nvarchar(4000)
        SET @sql = '
        INSERT INTO #snapshots 
        SELECT DISTINCT 
            DATEDIFF (minute, ''20000101'', dataTable.collection_time) / @group_interval_min AS interval_time_id, 
            DATEADD (minute, (DATEDIFF (minute, ''20000101'', dataTable.collection_time) / @group_interval_min) * @group_interval_min, ''20000101'') AS interval_start_time, 
            DATEADD (minute, (DATEDIFF (minute, ''20000101'', dataTable.collection_time) / @group_interval_min + 1) * @group_interval_min, ''20000101'') AS interval_end_time, 
            DENSE_RANK() OVER (ORDER BY DATEDIFF (minute, ''20000101'', dataTable.collection_time) / @group_interval_min) AS interval_id,  
            dataTable.collection_time, s.source_id, s.snapshot_id 
        FROM ' + ISNULL (QUOTENAME (PARSENAME (@TargetCollectionTable, 2)), '[snapshots]') + '.' + QUOTENAME (PARSENAME (@TargetCollectionTable, 1)) + ' AS dataTable
        INNER JOIN core.snapshots AS s ON dataTable.snapshot_id = s.snapshot_id
        WHERE dataTable.collection_time BETWEEN @start_time_internal AND @end_time_internal
            AND s.snapshot_id BETWEEN @start_snapshot_id AND @end_snapshot_id
            AND s.instance_name = @ServerName AND s.collection_set_uid = @CollectionSetUid'
        
        EXEC sp_executesql 
            @sql, 
            N'@ServerName sysname, @CollectionSetUid nvarchar(64), @start_time_internal datetimeoffset(7), @end_time_internal datetimeoffset(7), @group_interval_min int, 
                @start_snapshot_id int, @end_snapshot_id int', 
            @ServerName = @ServerName, @CollectionSetUid = @CollectionSetUid, @start_time_internal = @start_time_internal, @end_time_internal = @end_time_internal, 
            @group_interval_min = @group_interval_min, @start_snapshot_id = @start_snapshot_id, @end_snapshot_id = @end_snapshot_id
        
        
        -- If the caller doesn't care about anything but the interval boundaries, don't bother returning the collection_time/snapshot_id values
        -- that fall in the middle of an interval. 
        IF (@include_snapshot_detail = 0)
        BEGIN
            SELECT interval_time_id, interval_start_time, interval_end_time, interval_id, 
                MIN (collection_time) AS first_collection_time, MAX (collection_time) AS last_collection_time, 
                MIN (snapshot_id) AS first_snapshot_id, MAX (snapshot_id) AS last_snapshot_id, 
                NULL AS source_id, NULL AS snapshot_id, NULL AS collection_time, NULL AS collection_time_id
            FROM #snapshots
            GROUP BY interval_time_id, interval_start_time, interval_end_time, interval_id
            ORDER BY interval_time_id
        END
        ELSE
        BEGIN
            SELECT interval_info.*, #snapshots.source_id, #snapshots.snapshot_id, #snapshots.collection_time, 
                DENSE_RANK() OVER (ORDER BY #snapshots.collection_time) AS collection_time_id
            FROM 
            (
                SELECT interval_time_id, interval_start_time, interval_end_time, interval_id, 
                    MIN (collection_time) AS first_collection_time, MAX (collection_time) AS last_collection_time, 
                    MIN (snapshot_id) AS first_snapshot_id, MAX (snapshot_id) AS last_snapshot_id
                FROM #snapshots
                GROUP BY interval_time_id, interval_start_time, interval_end_time, interval_id
            ) AS interval_info
            INNER JOIN #snapshots ON interval_info.interval_time_id = #snapshots.interval_time_id
            ORDER BY interval_info.interval_time_id, #snapshots.collection_time
        END
    END
    ELSE BEGIN
        /* Invalid parameter %s specified for %s. */
        RAISERROR (21055, 16, -1, @TargetCollectionTable, '@TargetCollectionTable')
    END
    
END;
GO


--
-- snapshots.rpt_next_and_previous_collection_times
--  Helper proc used by other procs that need to return data from N evenly-spaced intervals within a larger user-specified time window
--
--
-- Parameters: 
--    @ServerName - SQL Server instance name
--    @CollectionTime - End of the user-selected time window (UTC)
--    @TargetCollectionTable - Name of the table from which to harvest the collection_time values. Must have a datetimeoffset [collection_time] 
--        column and be in the [snapshots] or [custom_snapshots] schema. 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_next_and_previous_collection_times', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_next_and_previous_collection_times] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_next_and_previous_collection_times]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_next_and_previous_collection_times] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROC [snapshots].[rpt_next_and_previous_collection_times]
    @ServerName sysname, 
    @CollectionTime datetime, 
    @DataGroupID nvarchar(128)
AS
BEGIN
    DECLARE @current_collection_time datetimeoffset(7)  -- current collection time
    DECLARE @current_snapshot_id int                    -- current collection time''s snapshot ID
    DECLARE @previous_collection_time datetimeoffset(7) -- next collection time
    DECLARE @next_collection_time datetimeoffset(7)     -- prior collection time
    DECLARE @snapshot_table sysname                     -- name of the snapshot table we'll be querying
    
    -- The assumed time zone offset for this conversion is +00:00 (datetime must be passed in as UTC)
    SET @current_collection_time = CAST (@CollectionTime AS datetimeoffset(7));
    -- Compensate for RS truncation of fractional seconds 
    SET @current_collection_time = DATEADD(second, 1, @CollectionTime);
    
    -- Currently, we only call this stored procedure from one place, and that code only needs next and prev collection times for 
    -- the snapshots.active_sessions_and_requests table. If, in the future, we need to call this for other tables, the three 
    -- SELECT TOP 1 queries below will need to be converted to dynamic SQL, executed via sp_executesql with OUTPUT parameters.  
    -- The correct target table name (@snapshot_table) should be determined based on the @DataGroupID parameter.  
    IF (@DataGroupID = 'SqlActiveRequests')
    BEGIN
        SET @snapshot_table = 'snapshots.active_sessions_and_requests';
    END
    ELSE BEGIN
        /* Invalid parameter %s specified for %s. */
        RAISERROR (21055, 16, -1, @DataGroupID, '@DataGroupID');
        RETURN;
    END
    
    DECLARE @sql nvarchar(max);

    -- Find our exact collection time using the approx time passed in   
    SELECT TOP 1 @current_collection_time = r.collection_time, @current_snapshot_id = r.snapshot_id
    FROM core.snapshots AS s
    INNER JOIN snapshots.active_sessions_and_requests AS r ON s.snapshot_id = r.snapshot_id
    WHERE s.instance_name = @ServerName
        AND r.collection_time <= @current_collection_time
    ORDER BY collection_time DESC;
    
    -- Find the previous collection time
    SELECT TOP 1 @previous_collection_time = r.collection_time
    FROM core.snapshots AS s
    INNER JOIN snapshots.active_sessions_and_requests AS r ON s.snapshot_id = r.snapshot_id
    WHERE s.instance_name = @ServerName
      AND r.collection_time < @current_collection_time 
    ORDER BY collection_time DESC;
    
    -- Find the next collection time
    SELECT TOP 1 @next_collection_time = r.collection_time
    FROM core.snapshots AS s
    INNER JOIN snapshots.active_sessions_and_requests AS r ON s.snapshot_id = r.snapshot_id
    WHERE s.instance_name = @ServerName
      AND r.collection_time > @current_collection_time 
    ORDER BY collection_time ASC;

    IF @previous_collection_time IS NULL SET @previous_collection_time = @current_collection_time;
    IF @next_collection_time IS NULL SET @next_collection_time = @current_collection_time;

    SELECT 
        CONVERT (datetime, SWITCHOFFSET (@current_collection_time, '+00:00')) AS current_collection_time, 
        @current_snapshot_id AS current_snapshot_id, 
        CONVERT (datetime, SWITCHOFFSET (@previous_collection_time, '+00:00')) AS previous_collection_time, 
        CONVERT (datetime, SWITCHOFFSET (@next_collection_time, '+00:00')) AS next_collection_time;
END
GO


--
-- snapshots.rpt_generic_perfmon
--  Returns perf counter data for the counters associated with a "data group id" (typically, a report name). 
-- Parameters: 
--    @ServerName - SQL Server instance name
--    @EndTime - End of selected time window end (UTC)
--    @WindowSize - Number of minutes in the time window
--    @DataGroupID - Name of the report, used to return only the necessary counters
--    @CollectionSetUid - GUID identifier for the target collection set
--    @interval_count - Number of time intervals to divide the time window into (default 40)
--
IF (NOT OBJECT_ID(N'snapshots.rpt_generic_perfmon', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_generic_perfmon] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_generic_perfmon]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_generic_perfmon] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_generic_perfmon]
    @ServerName sysname, 
    @EndTime datetime, 
    @WindowSize int, 
    @DataGroupID nvarchar(128), 
    @CollectionSetUid nvarchar(64), 
    @interval_count int = 40
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @start_time_internal datetimeoffset(7);
    DECLARE @end_time_internal datetimeoffset(7);
    
    -- Start time should be passed in as a UTC datetime
    IF (@EndTime IS NOT NULL)
    BEGIN
        -- Assumed time zone offset for this conversion is +00:00 (datetime must be passed in as UTC)
        SET @end_time_internal = CAST (@EndTime AS datetimeoffset(7));
    END
    ELSE BEGIN
        SELECT @end_time_internal = MAX(snapshot_time)
        FROM core.snapshots 
        WHERE instance_name = @ServerName AND collection_set_uid = @CollectionSetUid
    END
    SET @start_time_internal = DATEADD (minute, -1 * @WindowSize, @end_time_internal);
    
    -- Divide the time window up into N equal intervals.  Each interval will correspond to one 
    -- point on a line chart.  Calc the duration of one interval, in seconds. 
    DECLARE @group_interval_sec int
    IF @interval_count < 1 SET @interval_count = 1
    SET @group_interval_sec = ROUND (DATEDIFF (second, @start_time_internal, @end_time_internal) / @interval_count, 0)
    IF @group_interval_sec < 10 SET @group_interval_sec = 10

    -- For counter groups that include the "Process(abc)\% Processor Time" counter (e.g. 'ServerActivity' and 'SystemCpuUsagePivot'), 
    -- we must determine the logical CPU count on the target system by querying the number of "Processor" counter instances we 
    -- captured in a perfmon sample that was captured around the same time. 
    DECLARE @cpu_count smallint
    SET @cpu_count = 1
    IF EXISTS (
        SELECT * FROM [core].[performance_counter_report_group_items] 
        WHERE counter_group_id = @DataGroupID AND [divide_by_cpu_count] = 1
    )
    BEGIN
        SELECT @cpu_count = COUNT (DISTINCT pc.performance_instance_name)
        FROM snapshots.performance_counters AS pc
        INNER JOIN core.snapshots s ON s.snapshot_id = pc.snapshot_id 
        WHERE pc.performance_object_name = 'Processor' AND pc.performance_counter_name = '% Processor Time'
            AND pc.performance_instance_name != '_Total' AND ISNUMERIC (pc.performance_instance_name) = 1
            AND s.instance_name = @ServerName AND s.collection_set_uid = @CollectionSetUid
            AND s.snapshot_id = 
                (SELECT TOP 1 s2.snapshot_id FROM core.snapshots AS s2 
                INNER JOIN snapshots.performance_counters AS pc2 ON s2.snapshot_id = pc2.snapshot_id 
                WHERE s2.snapshot_time > @start_time_internal
                    AND s2.instance_name = @ServerName AND s2.collection_set_uid = @CollectionSetUid
                    AND pc2.performance_object_name = 'Processor' AND pc2.performance_counter_name = '% Processor Time')
            AND pc.collection_time = 
                (SELECT TOP 1 collection_time FROM snapshots.performance_counter_values pcv2 WHERE pcv2.snapshot_id = s.snapshot_id)
        -- These trace flags are necessary for a good plan, due to the join on ascending PK w/range filter
        OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390)
        
        IF ISNULL (@cpu_count, 0) = 0
        BEGIN
            -- This message will never be shown on a report. It is included here only as a troubleshooting aid. 
            RAISERROR ('Unable to determine CPU count. Assuming 1 CPU for process CPU calculations', 9, 1)
            SET @cpu_count = 1
        END
    END

    -- Get the matching performance counter instances for this data group
    SELECT 
        pci.*, 
        cl.counter_group_item_id, cl.counter_group_id, cl.counter_subgroup_id, cl.series_name, 
        cl.multiply_by, cl.divide_by_cpu_count
    INTO #pci
    FROM snapshots.performance_counter_instances AS pci
    INNER JOIN [core].[performance_counter_report_group_items] AS cl 
        ON cl.counter_group_id = @DataGroupID
        AND pci.counter_name = cl.counter_name 
        AND ISNULL(pci.instance_name, N'') LIKE cl.instance_name 
        AND 
        (
            (cl.object_name_wildcards = 0 AND pci.[object_name] = cl.[object_name])
            OR (cl.object_name_wildcards = 1 AND pci.[object_name] LIKE cl.[object_name])
        )
        AND (cl.not_instance_name IS NULL OR pci.instance_name NOT LIKE cl.not_instance_name);
    
    -- Get the perfmon counter values for these counters in each time interval. 
    -- NOTE: If you change the schema of this resultset, you must also update the CREATE TABLE in [rpt_generic_perfmon_pivot]. 
    SELECT 
        #pci.counter_subgroup_id, 
        REPLACE (#pci.series_name, '[COUNTER_INSTANCE]', ISNULL(#pci.instance_name, N'')) AS series_name, 
        -- Using our time window end time (@end_time_internal) as a reference point, divide 
        -- the time window into [@interval_count] intervals of [@group_interval_sec] duration 
        -- per interval. 
        DATEDIFF (second, @end_time_internal, SWITCHOFFSET (CONVERT (datetimeoffset(7), pc.collection_time), '+00:00')) / @group_interval_sec AS interval_id, 
        -- Find the end time for the current time interval, and return as a UTC datetime
        -- Do this by converting [collection_time] into a second count, dividing and multiplying 
        -- the count by [@group_interval_sec] to discard any fraction of an interval, then 
        -- converting the second count back into a datetime.  That datetime is the end point for 
        -- the time interval that this [collection_time] value falls within. 
        CONVERT (datetime, 
            DATEADD (
                second, 
                (DATEDIFF (second, @end_time_internal, SWITCHOFFSET (CONVERT (datetimeoffset(7), pc.collection_time), '+00:00')) / @group_interval_sec) * @group_interval_sec, 
                @end_time_internal
            ) 
        ) AS interval_end_time, 
        #pci.counter_name, 
        AVG( pc.formatted_value * #pci.multiply_by / CASE WHEN #pci.divide_by_cpu_count = 1 THEN @cpu_count ELSE 1 END ) AS avg_formatted_value, 
        MAX( pc.formatted_value * #pci.multiply_by / CASE WHEN #pci.divide_by_cpu_count = 1 THEN @cpu_count ELSE 1 END ) AS max_formatted_value, 
        MIN( pc.formatted_value * #pci.multiply_by / CASE WHEN #pci.divide_by_cpu_count = 1 THEN @cpu_count ELSE 1 END ) AS min_formatted_value, 
        -- This column can be used to simulate a "_Total" instance for multi-instance counters that lack _Total -- use a "%" for #counterlist.instance_name
        -- Expression "1.0 * pc.formatted_value * cl.multiply_by / CASE WHEN cl.divide_by_cpu_count = 1 THEN @cpu_count ELSE 1 END" is the counter value. 
        -- Expression "AVG(<counter_value>) * COUNT (DISTINCT pc.performance_instance_name)" returns the simulated "_Total" instance.
        -- Only valid for multi-instance counters that don't already have a "_Total"). 
        CONVERT (bigint, AVG( 1.0 * pc.formatted_value * #pci.multiply_by / CASE WHEN #pci.divide_by_cpu_count = 1 THEN @cpu_count ELSE 1 END)) 
            * COUNT (DISTINCT #pci.instance_name) AS multi_instance_avg_formatted_value
    FROM snapshots.performance_counter_values AS pc
    INNER JOIN #pci ON #pci.performance_counter_id = pc.performance_counter_instance_id
    INNER JOIN core.snapshots s ON s.snapshot_id = pc.snapshot_id
    WHERE s.instance_name = @ServerName AND s.collection_set_uid = @CollectionSetUid
        AND pc.collection_time BETWEEN @start_time_internal AND @end_time_internal
    GROUP BY 
        DATEDIFF (second, @end_time_internal, SWITCHOFFSET (CONVERT (datetimeoffset(7), pc.collection_time), '+00:00')) / @group_interval_sec, #pci.counter_subgroup_id, 
        #pci.counter_name, 
        REPLACE (#pci.series_name, '[COUNTER_INSTANCE]', ISNULL(#pci.instance_name, N''))
    ORDER BY #pci.counter_subgroup_id, interval_end_time, 2, #pci.counter_name
    -- These trace flags are necessary for a good plan, due to the join on ascending PK w/range filter
    OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390)
END
GO


--
-- snapshots.rpt_generic_perfmon_pivot
--  Pivots the output of [rpt_generic_perfmon] so that multiple counter values can be returned in a 
--  single row. The value of all counters with the same [series_name] will be returned as a single row. 
--  The average of all values in the time window is returned
-- Parameters: 
--    @ServerName - SQL Server instance name
--    @EndTime - End of selected time window end (UTC)
--    @WindowSize - Number of minutes in the time window
--    @DataGroupID - Name of the calling report (used to retrieve the correct counters)
--    @CollectionSetUid - GUID identifier for the target collection set
--    @interval_count - Number of time intervals to divide the time window into (default 40)
--
IF (NOT OBJECT_ID(N'snapshots.rpt_generic_perfmon_pivot', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_generic_perfmon_pivot] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_generic_perfmon_pivot]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_generic_perfmon_pivot] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_generic_perfmon_pivot]
    @ServerName sysname, 
    @EndTime datetime, 
    @WindowSize int, 
    @DataGroupID nvarchar(128), 
    @CollectionSetUid varchar(64), 
    @interval_count int = 1
AS
BEGIN
    SET NOCOUNT ON;

    CREATE TABLE #countervalues (
        counter_subgroup_id nvarchar(512),          -- name of the data subgroup (e.g. chart or table) that requested the counter (used by a chart to filter out its rows from the larger resultset)
        series_name nvarchar(1024),                 -- chart series label 
        interval_id int,                            -- not used here
        interval_end_time datetime,                 -- not used here
        performance_counter_name nvarchar(2048),    -- perfmon counter name
        avg_formatted_value bigint,                 -- avg perfmon counter value over the time period
        max_formatted_value bigint,                 -- max perfmon counter value over the time period
        min_formatted_value bigint,                 -- min perfmon counter value over the time period
        multi_instance_avg_formatted_value bigint   -- simulated "_Total" instance value for multi-instance counters that lack _Total
    )
    
    SET @DataGroupID = @DataGroupID + 'Pivot'
    INSERT INTO #countervalues
    EXEC [snapshots].[rpt_generic_perfmon] 
        @ServerName, 
        @EndTime, 
        @WindowSize, 
        @DataGroupID, 
        @CollectionSetUid, 
        @interval_count

    IF EXISTS (SELECT * FROM #countervalues)
    BEGIN
        -- @counterlist looks like "[Counter 1], [Counter 2]"
        DECLARE @counterlist nvarchar(max)
        -- @columnlist_min_inner looks like "[Counter 1] AS [Counter 1_min], [Counter 2] AS [Counter 2_min]"
        DECLARE @columnlist_min_inner nvarchar(max)
        -- @columnlist_max_inner looks like "[Counter 1] AS [Counter 1_max], [Counter 2] AS [Counter 2_max]"
        DECLARE @columnlist_max_inner nvarchar(max)
        -- @columnlist_min_outer looks like "[Counter 1_min], [Counter 2_min]"
        DECLARE @columnlist_min_outer nvarchar(max)
        -- @columnlist_max_outer looks like "[Counter 1_max], [Counter 2_max]"
        DECLARE @columnlist_max_outer nvarchar(max)
        SET @counterlist = ''
        SET @columnlist_min_inner = ''
        SET @columnlist_min_outer = ''
        SET @columnlist_max_inner = ''
        SET @columnlist_max_outer = ''
        
        -- Build counter lists
        SELECT 
            @counterlist = @counterlist 
                -- Escape any embedded ']' chars (we can't use QUOTENAME because it can't handle strings > 128 chars)
                + ', [' + REPLACE (performance_counter_name, ']', ']]') + ']'
            , @columnlist_min_outer = @columnlist_min_outer + ', [' + REPLACE (performance_counter_name, ']', ']]') + '_min]'
            , @columnlist_min_inner = @columnlist_min_inner + ', [' + REPLACE (performance_counter_name, ']', ']]') + ']'
                + ' AS [' + REPLACE (performance_counter_name, ']', ']]') + '_min]'
            , @columnlist_max_outer = @columnlist_max_outer + ', [' + REPLACE (performance_counter_name, ']', ']]') + '_max]'
            , @columnlist_max_inner = @columnlist_max_inner + ', [' + REPLACE (performance_counter_name, ']', ']]') + ']'
                + ' AS [' + REPLACE (performance_counter_name, ']', ']]') + '_max]'
        FROM (SELECT DISTINCT performance_counter_name FROM #countervalues) AS t
        GROUP BY performance_counter_name 
        OPTION (MAXDOP 1)
        -- Remove the leading comma
        SET @counterlist = SUBSTRING (@counterlist, 3, LEN (@counterlist)-2)
        SET @columnlist_min_inner = SUBSTRING (@columnlist_min_inner, 3, LEN (@columnlist_min_inner)-2)
        SET @columnlist_min_outer = SUBSTRING (@columnlist_min_outer, 3, LEN (@columnlist_min_outer)-2)
        SET @columnlist_max_inner = SUBSTRING (@columnlist_max_inner, 3, LEN (@columnlist_max_inner)-2)
        SET @columnlist_max_outer = SUBSTRING (@columnlist_max_outer, 3, LEN (@columnlist_max_outer)-2)

        -- We have to use three PIVOTs here because SQL only allows one aggregate function 
        -- per PIVOT, and we need AVG, MIN, and MAX.  They are over a very small temp table, 
        -- though (by default just one row per counter), so the execution cost isn't 
        -- excessive. 
        DECLARE @sql nvarchar(max)
        SET @sql = '
            SELECT avg_pivot.*, ' + @columnlist_min_outer + ', ' + @columnlist_max_outer + '
            FROM 
            (
                SELECT series_name, interval_end_time, ' + @counterlist + '
                FROM 
                (
                    SELECT series_name, interval_end_time, performance_counter_name, avg_formatted_value
                    FROM #countervalues
                ) AS SourceTable
                PIVOT 
                (
                    AVG (avg_formatted_value) 
                    FOR performance_counter_name IN (' + @counterlist + ')
                ) AS PivotTable
            ) AS avg_pivot
            
            INNER JOIN 
            (
                SELECT series_name, interval_end_time, ' + @columnlist_min_inner + '
                FROM 
                (
                    SELECT series_name, interval_end_time, performance_counter_name, min_formatted_value
                    FROM #countervalues
                ) AS SourceTable
                PIVOT 
                (
                    MIN (min_formatted_value) 
                    FOR performance_counter_name IN (' + @counterlist + ')
                ) AS PivotTable
            ) AS min_pivot ON min_pivot.series_name = avg_pivot.series_name AND min_pivot.interval_end_time = avg_pivot.interval_end_time
            
            INNER JOIN 
            (
                SELECT series_name, interval_end_time, ' + @columnlist_max_inner + '
                FROM 
                (
                    SELECT series_name, interval_end_time, performance_counter_name, max_formatted_value
                    FROM #countervalues
                ) AS SourceTable
                PIVOT 
                (
                    MAX (max_formatted_value) 
                    FOR performance_counter_name IN (' + @counterlist + ')
                ) AS PivotTable
            ) AS max_pivot ON max_pivot.series_name = avg_pivot.series_name AND max_pivot.interval_end_time = avg_pivot.interval_end_time
            '
        EXEC sp_executesql @sql
    END
END;
GO


--
-- snapshots.rpt_wait_stats
--  Returns wait time per wait type over a time interval
-- Parameters: 
--    @ServerName - SQL Server instance name
--    @EndTime - End of the user-selected time window (UTC)
--    @WindowSize - Number of minutes in the time window 
--    @CategoryName - (Optional) Name of wait category to filter on (all categories if NULL)
--    @WaitType - (Optional) Name of wait type to filter on (all wait types if NULL)
--
IF (NOT OBJECT_ID(N'snapshots.rpt_wait_stats', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_wait_stats] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_wait_stats]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_wait_stats] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_wait_stats]
    @ServerName sysname,
    @EndTime datetime = NULL,
    @WindowSize int,
    @CategoryName nvarchar(20) = NULL, 
    @WaitType nvarchar(45) = NULL
AS
BEGIN
    SET NOCOUNT ON;

    -- Clean string params (on drillthrough, RS may pass in empty string instead of NULL)
    IF @CategoryName = '' SET @CategoryName = NULL
    IF @WaitType = '' SET @WaitType = NULL

    -- Divide our time window up into 40 evenly-sized time intervals, and find the last collection_time within each of these intervals
    CREATE TABLE #intervals (
        interval_time_id        int, 
        interval_start_time     datetimeoffset(7),
        interval_end_time       datetimeoffset(7),
        interval_id             int, 
        first_collection_time   datetimeoffset(7), 
        last_collection_time    datetimeoffset(7), 
        first_snapshot_id       int,
        last_snapshot_id        int, 
        source_id               int,
        snapshot_id             int, 
        collection_time         datetimeoffset(7), 
        collection_time_id      int
    )
    -- GUID 49268954-... is Server Activity
    INSERT INTO #intervals
    EXEC [snapshots].[rpt_interval_collection_times] 
        @ServerName, @EndTime, @WindowSize, 'snapshots.os_wait_stats', '49268954-4FD4-4EB6-AA04-CD59D9BB5714', 40, 0

    -- Get the earliest and latest snapshot_id values that contain data for the selected time interval. 
    -- This will allow a more efficient query plan. 
    DECLARE @start_snapshot_id int;
    DECLARE @end_snapshot_id int;
    SELECT @start_snapshot_id = MIN (first_snapshot_id)
    FROM #intervals
    SELECT @end_snapshot_id = MAX (last_snapshot_id)
    FROM #intervals
    
    -- Get the wait stats for these collection times
    SELECT 
        coll.interval_time_id, coll.interval_id, 
        last_collection_time AS collection_time, 
        coll.interval_start_time, 
        coll.interval_end_time, 
        coll.last_snapshot_id, 
        wt.category_name, ws.wait_type, ws.waiting_tasks_count, 
        ISNULL (ws.signal_wait_time_ms, 0) AS signal_wait_time_ms, 
        ISNULL (ws.wait_time_ms, 0) - ISNULL (ws.signal_wait_time_ms, 0) AS wait_time_ms, 
        wait_time_ms AS raw_wait_time_ms, 
        ISNULL (ws.wait_time_ms, 0) AS wait_time_ms_cumulative 
    INTO #wait_stats
    FROM snapshots.os_wait_stats AS ws
    INNER JOIN #intervals AS coll ON coll.last_snapshot_id = ws.snapshot_id AND coll.last_collection_time = ws.collection_time 
    INNER JOIN core.wait_types_categorized AS wt ON wt.wait_type = ws.wait_type
    WHERE wt.category_name = ISNULL (@CategoryName, wt.category_name)
        AND wt.wait_type = ISNULL (@WaitType, wt.wait_type)
        AND wt.ignore != 1

    -- Get wait times by waittype for each interval (plus CPU time, modeled as a waittype)
    ---- First get resource wait stats for this interval. We must convert all datetimeoffset values 
    ---- to UTC datetime values before returning to Reporting Services
    SELECT 
        CONVERT (datetime, SWITCHOFFSET (CAST (w1.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        CONVERT (datetime, SWITCHOFFSET (CAST (w1.interval_start_time AS datetimeoffset(7)), '+00:00')) AS interval_start, 
        CONVERT (datetime, SWITCHOFFSET (CAST (w2.interval_start_time AS datetimeoffset(7)), '+00:00')) AS interval_end, 
        w2.category_name, w2.wait_type, 
        -- All wait stats will be reset to zero by a service cycle, which will cause 
        -- (snapshot2waittime-snapshot1waittime) calculations to produce an incorrect 
        -- negative wait time for the interval.  Detect this and avoid calculating 
        -- negative wait time/wait count/signal time deltas. 
        CASE 
            WHEN (w2.waiting_tasks_count - w1.waiting_tasks_count) < 0 THEN w2.waiting_tasks_count 
            ELSE (w2.waiting_tasks_count - w1.waiting_tasks_count) 
        END AS waiting_tasks_count_delta, 
        CASE 
            WHEN (w2.raw_wait_time_ms - w1.raw_wait_time_ms) < 0 THEN w2.wait_time_ms
            ELSE (w2.wait_time_ms - w1.wait_time_ms)
        END AS resource_wait_time_delta, -- / CAST (DATEDIFF (second, w1.collection_time, w2.collection_time) AS decimal) 
        CASE 
            WHEN (w2.signal_wait_time_ms - w1.signal_wait_time_ms) < 0 THEN w2.signal_wait_time_ms 
            ELSE (w2.signal_wait_time_ms - w1.signal_wait_time_ms) 
        END AS resource_signal_time_delta, -- / CAST (DATEDIFF (second, w1.collection_time, w2.collection_time) AS decimal) 
        DATEDIFF (second, w1.collection_time, w2.collection_time) AS interval_sec, 
        w2.wait_time_ms_cumulative
    -- Self-join - w1 represents wait stats at the beginning of the sample interval, while w2 
    -- shows the wait stats at the end of the interval. 
    FROM #wait_stats AS w1 
    INNER JOIN #wait_stats AS w2 ON w1.wait_type = w2.wait_type AND w1.interval_id = w2.interval_id-1 

    UNION ALL 

    ---- Treat the sum of all signal waits as CPU "wait time"
    SELECT 
        MAX (CONVERT (datetime, SWITCHOFFSET (CAST (w1.collection_time AS datetimeoffset(7)), '+00:00'))) AS collection_time, 
        MIN (CONVERT (datetime, SWITCHOFFSET (CAST (w1.interval_start_time AS datetimeoffset(7)), '+00:00'))) AS interval_start, 
        MAX (CONVERT (datetime, SWITCHOFFSET (CAST (w2.interval_start_time AS datetimeoffset(7)), '+00:00'))) AS interval_end, 
        'CPU' AS category_name, 
        'CPU (Signal Wait)' AS wait_type, 
        0 AS waiting_tasks_count_delta, 
        -- Handle wait stats resets, as in the previous query
        SUM (
            CASE 
                WHEN (w2.signal_wait_time_ms - w1.signal_wait_time_ms) < 0 THEN w2.signal_wait_time_ms
                ELSE (w2.signal_wait_time_ms - w1.signal_wait_time_ms) 
            END -- / CAST (DATEDIFF (second, w1.collection_time, w2.collection_time) AS decimal)
        ) AS resource_wait_time_delta, 
        0 AS resource_signal_time_delta, 
        DATEDIFF (second, w1.collection_time, w2.collection_time) AS interval_sec, 
        NULL AS wait_time_ms_cumulative
    FROM #wait_stats AS w1
    INNER JOIN #wait_stats AS w2 ON w1.wait_type = w2.wait_type AND w1.interval_id = w2.interval_id-1
    -- Only return CPU stats if we were told to return the 'CPU' category or all categories
    WHERE (@CategoryName IS NULL OR @CategoryName = 'CPU')
    GROUP BY 
        w1.interval_start_time, w2.interval_start_time, w1.interval_end_time, w2.interval_end_time, w1.collection_time, w2.collection_time

    UNION ALL 

    -- Get actual used CPU time from perfmon data (average across the whole snapshot_time_id interval, 
    -- and use this average for each sample time in this interval).  Note that the "% Processor Time" 
    -- counter in the "Process" object can exceed 100% (for example, it can range from 0-800% on an 
    -- 8 CPU server). 
    SELECT
        CONVERT (datetime, SWITCHOFFSET (CAST (w1.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        CONVERT (datetime, SWITCHOFFSET (CAST (w1.interval_start_time AS datetimeoffset(7)), '+00:00')) AS interval_start, 
        CONVERT (datetime, SWITCHOFFSET (CAST (w2.interval_start_time AS datetimeoffset(7)), '+00:00')) AS interval_end, 
        'CPU' AS category_name, 
        'CPU (Consumed)' AS wait_type, 
        0 AS waiting_tasks_count_delta, 
        -- Get sqlservr %CPU usage for the perfmon sample that immediately precedes each wait stats sample.  
        -- Multiply by 10 to convert "% CPU" to "ms CPU/sec". This works because (for example) on an 8 proc 
        -- server, Process(...)\% Processor Time ranges from 0 to 800, not 0 to 100.  Multiply again by 
        -- the duration of interval in seconds to get the total ms of CPU time used in the interval. 
        DATEDIFF (second, w1.collection_time, w2.collection_time) * 10 * ( 
            SELECT TOP 1 formatted_value
            FROM snapshots.performance_counters AS pc
            INNER JOIN core.snapshots s ON pc.snapshot_id = s.snapshot_id
            WHERE pc.performance_object_name = 'Process' AND pc.performance_counter_name = '% Processor Time' 
                AND pc.performance_instance_name = '$(TARGETPROCESS)'
                AND pc.collection_time <= w2.collection_time
                AND s.instance_name = @ServerName AND s.collection_set_uid = '49268954-4FD4-4EB6-AA04-CD59D9BB5714' -- Server Activity CS
                AND s.snapshot_id BETWEEN @start_snapshot_id AND @end_snapshot_id 
            ORDER BY pc.collection_time DESC
        ) AS resource_wait_time_delta, 
        0 AS resource_signal_time_delta, 
        DATEDIFF (second, w1.collection_time, w2.collection_time) AS interval_sec, 
        NULL AS wait_time_ms_cumulative
    FROM #wait_stats AS w1
    INNER JOIN #wait_stats AS w2 ON w1.wait_type = w2.wait_type AND w1.interval_id = w2.interval_id-1
    -- Only return CPU stats if we weren't passed in a specific wait category
    WHERE (@CategoryName IS NULL OR @CategoryName = 'CPU')
    GROUP BY 
        w1.interval_start_time, w2.interval_start_time, w1.interval_end_time, w2.interval_end_time, w1.collection_time, w2.collection_time

    ORDER BY category_name, collection_time, wait_type
    -- These trace flags are necessary for a good plan, due to the join on ascending core.snapshot PK
    OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390);
    
END
GO


--
-- snapshots.fn_hexstrtovarbin
--  Converts a hex string into a varbinary(max). Reporting Services does not support binary 
--  form parameters, so sql_handle and plan_handle values must be passed to and from RS as strings. 
--  For conversion in the opposite direction, use master.dbo.fn_varbintohexstr. 
-- Parameters: 
--    @hexStr - a string representation of a binary value (e.g. "0x123C2F")
-- Returns: the input value converted to a true varbinary
--
IF (NOT OBJECT_ID (N'[snapshots].[fn_hexstrtovarbin]', 'FN') IS NULL)
BEGIN
    RAISERROR('Dropping function [snapshots].[fn_hexstrtovarbin] ...', 0, 1)  WITH NOWAIT;
    DROP FUNCTION [snapshots].[fn_hexstrtovarbin]
END
GO 

RAISERROR('Creating function [snapshots].[fn_hexstrtovarbin] ...', 0, 1)  WITH NOWAIT;
GO
CREATE FUNCTION [snapshots].[fn_hexstrtovarbin]
(
    @hexStr varchar(max) 
)
RETURNS varbinary(max)
AS
BEGIN
    DECLARE @ret varbinary(max)
    DECLARE @len int

    SET @ret = 0x;
    SET @len = LEN (@hexStr)-2;

    IF (@len >= 0) AND (LEFT (@hexStr, 2) = '0x')
        SET @hexStr = SUBSTRING (@hexStr, 3, @len);
    ELSE
        RETURN NULL;

    DECLARE @leftNibbleChar char(1), @rightNibbleChar char(1), @hexCharStr varchar(2)
    DECLARE @leftNibble int, @rightNibble int
    DECLARE @i int;
    SET @i = 1;
    WHILE (@i <= @len)
    BEGIN
        SET @hexCharStr = SUBSTRING (@hexStr, @i, 2)
        IF LEN (@hexCharStr) = 1 SET @hexCharStr = '0' + @hexCharStr
        SET @leftNibbleChar = LOWER (LEFT (@hexCharStr, 1))
        SET @rightNibbleChar = LOWER (RIGHT (@hexCharStr, 1))

        IF @leftNibbleChar BETWEEN 'a' AND 'f' COLLATE Latin1_General_BIN 
           SET @leftNibble = (CONVERT (int, CONVERT (binary(1), @leftNibbleChar)) - CONVERT (int, CONVERT (binary(1), 'a')) + 10) * 16;
        ELSE IF @leftNibbleChar BETWEEN '0' AND '9' COLLATE Latin1_General_BIN 
           SET @leftNibble = (CONVERT (int, CONVERT (binary(1), @leftNibbleChar)) - CONVERT (int, CONVERT (binary(1), '0'))) * 16;
        ELSE
            RETURN NULL;

        IF @rightNibbleChar BETWEEN 'a' AND 'f' COLLATE Latin1_General_BIN 
           SET @rightNibble = (CONVERT (int, CONVERT (binary(1), @rightNibbleChar)) - CONVERT (int, CONVERT (binary(1), 'a')) + 10);
        ELSE IF @rightNibbleChar  BETWEEN '0' AND '9' COLLATE Latin1_General_BIN 
           SET @rightNibble = (CONVERT (int, CONVERT (binary(1), @rightNibbleChar)) - CONVERT (int, CONVERT (binary(1), '0')));
        ELSE
            RETURN NULL;

        SET @ret = @ret + CONVERT (binary(1), @leftNibble + @rightNibble)
        SET @i = @i + 2
    END

    RETURN @ret
END
GO


--
-- snapshots.rpt_top_query_stats
--  Returns aggregate query stats for the most expensive notable queries observed 
--  over the specified time interval.
--  Returns the top 10 most expensive plans (ranking criteria is specified via @order_by_criteria). 
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @start_time - (Optional) time window start (UTC)
--    @end_time - time window end (UTC)
--    @time_window_size - Number of intervals in the time window (provide if @start_time is NULL)
--    @time_interval_min - Number of minutes in each interval (provide if @start_time is NULL)
--    @order_by_criteria - (Optional) 'CPU' (default), 'Physical Reads', 'Logical Writes', 'I/O' (reads + writes), or 'Duration'
--    @database_name - Optional filter criteria: Only queries within a particular database
--
IF (NOT OBJECT_ID(N'snapshots.rpt_top_query_stats', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_top_query_stats] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_top_query_stats]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_top_query_stats] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_top_query_stats]
    @instance_name sysname,
    @start_time datetime = NULL,
    @end_time datetime = NULL,
    @time_window_size smallint = NULL,
    @time_interval_min smallint = 1, 
    @order_by_criteria varchar(30) = 'CPU', 
    @database_name nvarchar(255) = NULL
AS
BEGIN
    SET NOCOUNT ON;

    -- Clean string params (on drillthrough, RS may pass in an empty string instead of NULL)
    IF @database_name = '' SET @database_name = NULL

    -- @end_time should never be NULL when we are called from the Query Stats report
    -- Convert snapshot_time (datetimeoffset) to a UTC datetime
    IF (@end_time IS NULL)
        SET @end_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MAX(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));

    IF (@start_time IS NULL)
    BEGIN
        -- If time_window_size and time_interval_min are set use them
        -- to determine the start time
        -- Otherwise use the earliest available snapshot_time
        IF @time_window_size IS NOT NULL AND @time_interval_min IS NOT NULL
        BEGIN
            SET @start_time = DATEADD(minute, @time_window_size * @time_interval_min * -1.0, @end_time);
        END
        ELSE
        BEGIN
            -- Convert min snapshot_time (datetimeoffset) to a UTC datetime
            SET @start_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MIN(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));
        END
    END

    DECLARE @end_snapshot_time_id int;
    SELECT @end_snapshot_time_id = MAX(snapshot_time_id) FROM core.snapshots WHERE snapshot_time <= @end_time;

    DECLARE @start_snapshot_time_id int;
    SELECT @start_snapshot_time_id = MIN(snapshot_time_id) FROM core.snapshots WHERE snapshot_time >= @start_time;

    DECLARE @interval_sec int;
    SET @interval_sec = DATEDIFF (s, @start_time, @end_time)

    SELECT 
        REPLACE (REPLACE (REPLACE (REPLACE (REPLACE (REPLACE (
            LEFT (LTRIM (stmtsql.query_text), 100)
            , CHAR(9), ' '), CHAR(10), ' '), CHAR(13), ' '), '   ', ' '), '  ', ' '), '  ', ' ') AS flat_query_text, 
        t.*, 
        master.dbo.fn_varbintohexstr (t.sql_handle) AS sql_handle_str, 
        stmtsql.*
    FROM 
    (
        SELECT TOP 10 
            stat.sql_handle, stat.statement_start_offset, stat.statement_end_offset, snap.source_id, 
            SUM (stat.snapshot_execution_count) AS execution_count, 
            SUM (stat.snapshot_execution_count) / (@interval_sec / 60) AS executions_per_min, 
            SUM (stat.snapshot_worker_time / 1000) AS total_cpu, 
            SUM (stat.snapshot_worker_time / 1000) / @interval_sec AS avg_cpu_per_sec, 
            SUM (stat.snapshot_worker_time / 1000.0) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_cpu_per_exec, 
            SUM (stat.snapshot_physical_reads) AS total_physical_reads, 
            SUM (stat.snapshot_physical_reads) / @interval_sec AS avg_physical_reads_per_sec, 
            SUM (stat.snapshot_physical_reads) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_physical_reads_per_exec, 
            SUM (stat.snapshot_logical_writes) AS total_logical_writes, 
            SUM (stat.snapshot_logical_writes) / @interval_sec AS avg_logical_writes_per_sec, 
            SUM (stat.snapshot_logical_writes) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_logical_writes_per_exec, 
            SUM (stat.snapshot_elapsed_time / 1000) AS total_elapsed_time, 
            SUM (stat.snapshot_elapsed_time / 1000) / @interval_sec AS avg_elapsed_time_per_sec, 
            SUM (stat.snapshot_elapsed_time / 1000.0) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_elapsed_time_per_exec, 
            COUNT(*) AS row_count, COUNT (DISTINCT stat.creation_time) AS plan_count, 
            SUM (CASE @order_by_criteria WHEN 'Duration' THEN charted_value / 1000 ELSE charted_value END) AS charted_value, 
            -- TODO: Make this "sql.database_name" once database name is available in notable_query_text (VSTS #121662)
            CONVERT (nvarchar(255), '') AS database_name, 
            ROW_NUMBER() OVER (ORDER BY SUM (charted_value) DESC) as query_rank
        FROM 
        (
            SELECT *, 
                CASE @order_by_criteria 
                    WHEN 'CPU' THEN (snapshot_worker_time / 1000.0) / @interval_sec
                    WHEN 'Physical Reads' THEN 1.0 * snapshot_physical_reads / @interval_sec
                    WHEN 'Logical Writes' THEN 1.0 * snapshot_logical_writes / @interval_sec
                    WHEN 'I/O' THEN 1.0 * (snapshot_physical_reads + snapshot_logical_writes) / @interval_sec
                    WHEN 'Duration' THEN snapshot_elapsed_time / 1000.0
                    ELSE (snapshot_worker_time / 1000.0) / @interval_sec
                END AS charted_value
            FROM snapshots.query_stats 
        ) AS stat
        INNER JOIN core.snapshots snap ON stat.snapshot_id = snap.snapshot_id
        -- TODO: Uncomment this and the line in the WHERE clause once database name is available in notable_query_text (VSTS #121662)
        -- LEFT OUTER JOIN snapshots.notable_query_text AS sql ON t.sql_handle = sql.sql_handle and sql.source_id = t.source_id
        WHERE
            snap.instance_name = @instance_name 
            AND snap.snapshot_time_id BETWEEN @start_snapshot_time_id AND @end_snapshot_time_id
            --  AND ISNULL (sql.database_name = ISNULL (@database_name, stat.database_name)
        GROUP BY stat.sql_handle, stat.statement_start_offset, stat.statement_end_offset, snap.source_id
        ORDER BY ROW_NUMBER() OVER (ORDER BY SUM (charted_value) DESC) ASC
    ) AS t
    LEFT OUTER JOIN snapshots.notable_query_text sql ON t.sql_handle = sql.sql_handle and sql.source_id = t.source_id
    OUTER APPLY snapshots.fn_get_query_text (t.source_id, t.sql_handle, t.statement_start_offset, t.statement_end_offset) AS stmtsql
    ORDER BY query_rank ASC
    -- These trace flags are necessary for a good plan, due to the join on ascending PK w/range filter
    OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390)
END
GO


--
-- snapshots.rpt_query_stats
--  Returns aggregate query stats for all executions of a particular query within a specified time interval
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @start_time - (Optional) time window start (UTC)
--    @end_time - time window end (UTC)
--    @time_window_size - Number of intervals in the time window (provide if @start_time is NULL)
--    @time_interval_min - Number of minutes in each interval (provide if @start_time is NULL)
--    @sql_handle_str - String representation of a SQL handle (e.g. "0x1F27BC...")
--    @statement_start_offset - Start offset (byte count) for the statement within the batch identified by @sql_handle_str
--    @statement_start_offset - End offset (byte count) for the statement within the batch identified by @sql_handle_str
--
IF (NOT OBJECT_ID(N'snapshots.rpt_query_stats', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_query_stats] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_query_stats]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_query_stats] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_query_stats]
    @instance_name sysname,
    @start_time datetime = NULL,
    @end_time datetime = NULL,
    @time_window_size smallint,
    @time_interval_min smallint = 1, 
    @sql_handle_str varchar(130), 
    @statement_start_offset int, 
    @statement_end_offset int
AS
BEGIN
    SET NOCOUNT ON;

    -- @end_time should never be NULL when we are called from the Query Stats report
    -- Convert snapshot_time (datetimeoffset) to a UTC datetime
    IF (@end_time IS NULL)
        SET @end_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MAX(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));

    IF (@start_time IS NULL)
    BEGIN
        -- If time_window_size and time_interval_min are set use them
        -- to determine the start time
        -- Otherwise use the earliest available snapshot_time
        IF @time_window_size IS NOT NULL AND @time_interval_min IS NOT NULL
        BEGIN
            SET @start_time = DATEADD(minute, @time_window_size * @time_interval_min * -1.0, @end_time);
        END
        ELSE
        BEGIN
            -- Convert min snapshot_time (datetimeoffset) to a UTC datetime
            SET @start_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MIN(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));
        END
    END

    DECLARE @end_snapshot_time_id int;
    SELECT @end_snapshot_time_id = MAX(snapshot_time_id) FROM core.snapshots WHERE snapshot_time <= @end_time;

    DECLARE @start_snapshot_time_id int;
    SELECT @start_snapshot_time_id = MIN(snapshot_time_id) FROM core.snapshots WHERE snapshot_time >= @start_time;

    DECLARE @interval_sec int;
    SET @interval_sec = DATEDIFF (s, @start_time, @end_time);

    DECLARE @sql_handle varbinary(64)
    SET @sql_handle = snapshots.fn_hexstrtovarbin (@sql_handle_str)

    SELECT 
        REPLACE (REPLACE (REPLACE (REPLACE (REPLACE (REPLACE (
            LEFT (LTRIM (stmtsql.query_text), 100)
            , CHAR(9), ' '), CHAR(10), ' '), CHAR(13), ' '), '   ', ' '), '  ', ' '), '  ', ' ') AS flat_query_text, 
        t.*, 
        master.dbo.fn_varbintohexstr (t.sql_handle) AS sql_handle_str, 
        stmtsql.*
    FROM 
    (
        SELECT 
            stat.sql_handle, stat.statement_start_offset, stat.statement_end_offset, snap.source_id, 
            SUM (stat.snapshot_execution_count) AS execution_count, 
            SUM (stat.snapshot_execution_count) / (@interval_sec / 60) AS executions_per_min, 
            SUM (stat.snapshot_worker_time / 1000) AS total_cpu, 
            SUM (stat.snapshot_worker_time / 1000) / @interval_sec AS avg_cpu_per_sec, 
            SUM (stat.snapshot_worker_time / 1000.0) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_cpu_per_exec, 
            SUM (stat.snapshot_physical_reads) AS total_physical_reads, 
            SUM (stat.snapshot_physical_reads) / @interval_sec AS avg_physical_reads_per_sec, 
            SUM (stat.snapshot_physical_reads) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_physical_reads_per_exec, 
            SUM (stat.snapshot_logical_writes) AS total_logical_writes, 
            SUM (stat.snapshot_logical_writes) / @interval_sec AS avg_logical_writes_per_sec, 
            SUM (stat.snapshot_logical_writes) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_logical_writes_per_exec, 
            SUM (stat.snapshot_elapsed_time / 1000) AS total_elapsed_time, 
            SUM (stat.snapshot_elapsed_time / 1000) / @interval_sec AS avg_elapsed_time_per_sec, 
            SUM (stat.snapshot_elapsed_time / 1000.0) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_elapsed_time_per_exec, 
            COUNT(*) AS row_count, COUNT(DISTINCT plan_number) AS plan_count
        FROM
        (
            SELECT *, DENSE_RANK() OVER (ORDER BY plan_handle, creation_time) AS plan_number
            FROM snapshots.query_stats
        ) AS stat
        INNER JOIN core.snapshots snap ON stat.snapshot_id = snap.snapshot_id
        WHERE
            snap.instance_name = @instance_name 
            AND stat.sql_handle = @sql_handle 
            AND stat.statement_start_offset = @statement_start_offset 
            AND stat.statement_end_offset = @statement_end_offset
            AND snap.snapshot_time_id BETWEEN @start_snapshot_time_id AND @end_snapshot_time_id
        GROUP BY stat.sql_handle, stat.statement_start_offset, stat.statement_end_offset, snap.source_id
    ) t
    LEFT OUTER JOIN snapshots.notable_query_text sql ON t.sql_handle = sql.sql_handle and sql.source_id = t.source_id
    OUTER APPLY snapshots.fn_get_query_text (t.source_id, t.sql_handle, t.statement_start_offset, t.statement_end_offset) AS stmtsql
    -- These trace flags are necessary for a good plan, due to the join on ascending PK w/range filter
    OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390)
END
GO


--
-- snapshots.rpt_query_plan_stats
--  Returns aggregate stats for the all plans observed for a query within a specified time interval. 
--  Returns the top 10 most expensive plans (ranking criteria is specified via @order_by_criteria). 
--    @instance_name - SQL Server instance name
--    @start_time - (Optional) time window start (UTC)
--    @end_time - time window end (UTC)
--    @time_window_size - Number of intervals in the time window (provide if @start_time is NULL)
--    @time_interval_min - Number of minutes in each interval (provide if @start_time is NULL)
--    @sql_handle_str - String representation of a SQL handle (e.g. "0x1F27BC...")
--    @plan_handle_str - String representation of a plan handle (e.g. "0x1F27BC...")
--    @plan_creation_time - (Optional) Plan creation time
--    @statement_start_offset - Start offset (byte count) for the statement within the batch identified by @sql_handle_str
--    @statement_start_offset - End offset (byte count) for the statement within the batch identified by @sql_handle_str
--    @order_by_criteria - (Optional) 'CPU' (default), 'Physical Reads', 'Logical Writes', 'I/O' (reads+writes), or 'Duration'
--
IF (NOT OBJECT_ID(N'snapshots.rpt_query_plan_stats', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_query_plan_stats] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_query_plan_stats]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_query_plan_stats] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_query_plan_stats]
    @instance_name sysname,
    @start_time datetime = NULL,
    @end_time datetime = NULL,
    @time_window_size smallint,
    @time_interval_min smallint = 1, 
    @sql_handle_str varchar(130), 
    @plan_handle_str varchar(130) = NULL, 
    @plan_creation_time datetime = NULL, 
    @statement_start_offset int, 
    @statement_end_offset int, 
    @order_by_criteria varchar(30) = 'CPU'
AS
BEGIN
    SET NOCOUNT ON;

    -- @end_time should never be NULL when we are called from the Query Stats report
    -- Convert snapshot_time (datetimeoffset) to a UTC datetime
    IF (@end_time IS NULL)
        SET @end_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MAX(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));

    IF (@start_time IS NULL)
    BEGIN
        -- If time_window_size and time_interval_min are set use them
        -- to determine the start time
        -- Otherwise use the earliest available snapshot_time
        IF @time_window_size IS NOT NULL AND @time_interval_min IS NOT NULL
        BEGIN
            SET @start_time = DATEADD(minute, @time_window_size * @time_interval_min * -1.0, @end_time);
        END
        ELSE
        BEGIN
            -- Convert min snapshot_time (datetimeoffset) to a UTC datetime
            SET @start_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MIN(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));
        END
    END

    DECLARE @end_snapshot_time_id int;
    SELECT @end_snapshot_time_id = MAX(snapshot_time_id) FROM core.snapshots WHERE snapshot_time <= @end_time;

    DECLARE @start_snapshot_time_id int;
    SELECT @start_snapshot_time_id = MIN(snapshot_time_id) FROM core.snapshots WHERE snapshot_time >= @start_time;

    DECLARE @interval_sec int;
    SET @interval_sec = DATEDIFF (s, @start_time, @end_time);

    -- SQL and plan handles are passed in as a hex-formatted string. Convert to varbinary. 
    DECLARE @sql_handle varbinary(64), @plan_handle varbinary(64)
    SET @sql_handle = snapshots.fn_hexstrtovarbin (@sql_handle_str)
    IF LEN (@plan_handle_str) > 0
    BEGIN
        SET @plan_handle = snapshots.fn_hexstrtovarbin (@plan_handle_str)
    END

    SELECT 
        t.*, 
        master.dbo.fn_varbintohexstr (t.sql_handle) AS sql_handle_str, 
        master.dbo.fn_varbintohexstr (t.plan_handle) AS plan_handle_str
    FROM 
    (
        SELECT 
            stat.sql_handle, stat.statement_start_offset, stat.statement_end_offset, stat.plan_handle, 
            CONVERT (datetime, SWITCHOFFSET (CAST (stat.creation_time AS datetimeoffset(7)), '+00:00')) AS creation_time, 
            CONVERT (varchar, CONVERT (datetime, SWITCHOFFSET (CAST (MAX (stat.creation_time) AS datetimeoffset(7)), '+00:00')), 126) AS creation_time_str, 
            CONVERT (datetime, SWITCHOFFSET (CAST (MAX (stat.last_execution_time) AS datetimeoffset(7)), '+00:00')) AS last_execution_time, 
            snap.source_id, 
            SUM (stat.snapshot_execution_count) AS execution_count, 
            SUM (stat.snapshot_execution_count) / (@interval_sec / 60) AS executions_per_min, 
            SUM (stat.snapshot_worker_time / 1000) AS total_cpu, 
            SUM (stat.snapshot_worker_time / 1000) / @interval_sec AS avg_cpu_per_sec, 
            SUM (stat.snapshot_worker_time / 1000.0) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_cpu_per_exec, 
            SUM (stat.snapshot_physical_reads) AS total_physical_reads, 
            SUM (stat.snapshot_physical_reads) / @interval_sec AS avg_physical_reads_per_sec, 
            SUM (stat.snapshot_physical_reads) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_physical_reads_per_exec, 
            SUM (stat.snapshot_logical_writes) AS total_logical_writes, 
            SUM (stat.snapshot_logical_writes) / @interval_sec AS avg_logical_writes_per_sec, 
            SUM (stat.snapshot_logical_writes) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_logical_writes_per_exec, 
            SUM (stat.snapshot_elapsed_time / 1000) AS total_elapsed_time, 
            SUM (stat.snapshot_elapsed_time / 1000) / @interval_sec AS avg_elapsed_time_per_sec, 
            SUM (stat.snapshot_elapsed_time / 1000.0) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_elapsed_time_per_exec, 
            COUNT(*) AS row_count, COUNT (DISTINCT stat.creation_time) AS plan_count, 
            SUM (charted_value) AS charted_value, 
            ROW_NUMBER() OVER (ORDER BY SUM (charted_value) DESC) as query_rank
        FROM 
        (
            SELECT *, 
                -- This is the criteria used to rank the returned rowset and determine the order within Top-N plans
                -- returned from here. It is important that this part of the query stays in sync with a similar
                -- part of the query in snapshots.rpt_query_plan_stats_timeline procedure
                CASE @order_by_criteria     
                    WHEN 'CPU' THEN ((snapshot_worker_time / 1000.0) / CASE snapshot_execution_count WHEN 0 THEN 1 ELSE snapshot_execution_count END)
                    WHEN 'Physical Reads' THEN 1.0 * (snapshot_physical_reads / CASE snapshot_execution_count WHEN 0 THEN 1 ELSE snapshot_execution_count END)
                    WHEN 'Logical Writes' THEN 1.0 * (snapshot_logical_writes / CASE snapshot_execution_count WHEN 0 THEN 1 ELSE snapshot_execution_count END)
                    WHEN 'I/O' THEN 1.0 * ((snapshot_physical_reads + snapshot_logical_writes) / CASE snapshot_execution_count WHEN 0 THEN 1 ELSE snapshot_execution_count END)
                    WHEN 'Duration' THEN ((snapshot_elapsed_time / 1000.0) / CASE snapshot_execution_count WHEN 0 THEN 1 ELSE snapshot_execution_count END)
                    ELSE ((snapshot_worker_time / 1000.0) / CASE snapshot_execution_count WHEN 0 THEN 1 ELSE snapshot_execution_count END)
                END AS charted_value
            FROM snapshots.query_stats 
            WHERE 
                sql_handle = @sql_handle 
                AND (@plan_handle IS NULL OR plan_handle = @plan_handle) 
                AND (@plan_creation_time IS NULL OR creation_time = @plan_creation_time) 
                AND statement_start_offset = @statement_start_offset 
                AND statement_end_offset = @statement_end_offset
        ) stat
        INNER JOIN core.snapshots snap ON stat.snapshot_id = snap.snapshot_id
        WHERE
            snap.instance_name = @instance_name 
            AND snap.snapshot_time_id BETWEEN @start_snapshot_time_id AND @end_snapshot_time_id
        GROUP BY stat.sql_handle, stat.statement_start_offset, stat.statement_end_offset, stat.plan_handle, 
                stat.creation_time, snap.source_id
    ) t
    WHERE 
        (query_rank <= 10)
    ORDER BY query_rank ASC
    -- These trace flags are necessary for a good plan, due to the join on ascending PK w/range filter
    OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390)
END
GO



--
-- snapshots.rpt_query_plan_stats_timeline
--  Returns stats for the top 10 plans observed for a query within a specified time interval. 
--  Output is intended for plotting this over a time window. 
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @start_time - (Optional) time window start (UTC)
--    @end_time - time window end (UTC)
--    @time_window_size - Number of intervals in the time window (provide if @start_time is NULL)
--    @time_interval_min - Number of minutes in each interval (provide if @start_time is NULL)
--    @sql_handle_str - String representation of a SQL handle (e.g. "0x1F27BC...")
--    @plan_handle_str - (Optional) String representation of a plan handle (e.g. "0x1F27BC..."). Omit to see stats for all plans
--    @plan_creation_time - (Optional) Plan creation time
--    @statement_start_offset - Start offset (byte count) for the statement within the batch identified by @sql_handle_str
--    @statement_start_offset - End offset (byte count) for the statement within the batch identified by @sql_handle_str
--    @order_by_criteria - (Optional) 'CPU' (default), 'Physical Reads', 'Logical Writes', 'I/O' (reads+writes), or 'Duration'
--
IF (NOT OBJECT_ID(N'snapshots.rpt_query_plan_stats_timeline', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_query_plan_stats_timeline] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_query_plan_stats_timeline]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_query_plan_stats_timeline] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_query_plan_stats_timeline]
    @instance_name sysname,
    @start_time datetime = NULL,
    @end_time datetime = NULL,
    @time_window_size smallint,
    @time_interval_min smallint = 1, 
    @sql_handle_str varchar(130), 
    @plan_handle_str varchar(130) = NULL, 
    @plan_creation_time datetime = NULL, 
    @statement_start_offset int, 
    @statement_end_offset int, 
    @order_by_criteria varchar(30) = 'CPU'
AS
BEGIN
    SET NOCOUNT ON;

    -- @end_time should never be NULL when we are called from the Query Stats report
    -- Convert snapshot_time (datetimeoffset) to a UTC datetime
    IF (@end_time IS NULL)
        SET @end_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MAX(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));

    IF (@start_time IS NULL)
    BEGIN
        -- If time_window_size and time_interval_min are set use them
        -- to determine the start time
        -- Otherwise use the earliest available snapshot_time
        IF @time_window_size IS NOT NULL AND @time_interval_min IS NOT NULL
        BEGIN
            SET @start_time = DATEADD(minute, @time_window_size * @time_interval_min * -1.0, @end_time);
        END
        ELSE
        BEGIN
            -- Convert min snapshot_time (datetimeoffset) to a UTC datetime
            SET @start_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MIN(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));
        END
    END

    DECLARE @end_snapshot_time_id int;
    SELECT @end_snapshot_time_id = MAX(snapshot_time_id) FROM core.snapshots WHERE snapshot_time <= @end_time;

    DECLARE @start_snapshot_time_id int;
    SELECT @start_snapshot_time_id = MIN(snapshot_time_id) FROM core.snapshots WHERE snapshot_time >= @start_time;

    -- SQL and plan handles are passed in as a hex-formatted string. Convert to varbinary. 
    DECLARE @sql_handle varbinary(64), @plan_handle varbinary(64)
    SET @sql_handle = snapshots.fn_hexstrtovarbin (@sql_handle_str)
    IF LEN (ISNULL (@plan_handle_str, '')) > 0
    BEGIN
        SET @plan_handle = snapshots.fn_hexstrtovarbin (@plan_handle_str)
    END


    CREATE TABLE #top_plans (
        plan_handle varbinary(64), 
        creation_time datetimeoffset(7), 
        plan_rank int 
    )

    -- If we weren't told to focus on a particular plan...
    IF (@plan_handle IS NULL)
    BEGIN
        -- Get the top 10 most expensive plans for this query during the specified 
        -- time window. 
        INSERT INTO #top_plans
        SELECT * FROM
        (
            SELECT 
                plan_handle, 
                creation_time, 
                ROW_NUMBER() OVER (ORDER BY SUM (ranking_value) DESC) AS plan_rank 
            FROM 
            (
                SELECT *, 
                -- This is the criteria used to rank the returned rowset and determine the order within Top-N plans
                -- returned from here. It is important that this part of the query stays in sync with a similar
                -- part of the query in snapshots.rpt_query_plan_stats procedure
                CASE @order_by_criteria 
                    WHEN 'CPU' THEN ((snapshot_worker_time / 1000.0) / CASE snapshot_execution_count WHEN 0 THEN 1 ELSE snapshot_execution_count END)
                    WHEN 'Physical Reads' THEN 1.0 * (snapshot_physical_reads / CASE snapshot_execution_count WHEN 0 THEN 1 ELSE snapshot_execution_count END)
                    WHEN 'Logical Writes' THEN 1.0 * (snapshot_logical_writes / CASE snapshot_execution_count WHEN 0 THEN 1 ELSE snapshot_execution_count END)
                    WHEN 'I/O' THEN 1.0 * ((snapshot_physical_reads + snapshot_logical_writes) / CASE snapshot_execution_count WHEN 0 THEN 1 ELSE snapshot_execution_count END)
                    WHEN 'Duration' THEN ((snapshot_elapsed_time / 1000.0) / CASE snapshot_execution_count WHEN 0 THEN 1 ELSE snapshot_execution_count END)
                    ELSE ((snapshot_worker_time / 1000.0) / CASE snapshot_execution_count WHEN 0 THEN 1 ELSE snapshot_execution_count END)
                END AS ranking_value
            FROM snapshots.query_stats 
            WHERE 
                sql_handle = @sql_handle 
                AND statement_start_offset = @statement_start_offset 
                AND statement_end_offset = @statement_end_offset
        ) AS stat
        INNER JOIN core.snapshots snap ON stat.snapshot_id = snap.snapshot_id
        WHERE
            snap.instance_name = @instance_name 
            AND snap.snapshot_time_id BETWEEN @start_snapshot_time_id AND @end_snapshot_time_id
        GROUP BY plan_handle, creation_time
    ) AS t
    WHERE t.plan_rank <= 10
    -- These trace flags are necessary for a good plan, due to the join on ascending PK w/range filter
    OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390);
    END
    ELSE 
    BEGIN
        -- @plan_handle is not NULL; we have been told to focus on a particular plan. 
        INSERT INTO #top_plans 
        VALUES (@plan_handle, @plan_creation_time, 1)
    END;

    -- Get statistics for these 10 plans for each collection point in the time window
    WITH raw_stat AS 
    (
        SELECT *, 
            CASE @order_by_criteria 
                WHEN 'CPU' THEN snapshot_worker_time / 1000.0
                WHEN 'Physical Reads' THEN snapshot_physical_reads 
                WHEN 'Logical Writes' THEN snapshot_logical_writes 
                WHEN 'I/O' THEN (snapshot_logical_writes + snapshot_physical_reads)
                WHEN 'Duration' THEN snapshot_elapsed_time / 1000.0
                ELSE snapshot_worker_time / 1000.0
            END AS charted_value
        FROM snapshots.query_stats AS stat
    )
    SELECT 
        t.*, 
        master.dbo.fn_varbintohexstr (t.sql_handle) AS sql_handle_str, 
        master.dbo.fn_varbintohexstr (t.plan_handle) AS plan_handle_str
    FROM 
    (
        SELECT 
            stat.sql_handle, stat.statement_start_offset, stat.statement_end_offset, stat.plan_handle, 
            CONVERT (datetime, SWITCHOFFSET (CAST (stat.creation_time AS datetimeoffset(7)), '+00:00')) AS creation_time, 
            CONVERT (varchar, CONVERT (datetime, SWITCHOFFSET (CAST (MAX (stat.creation_time) AS datetimeoffset(7)), '+00:00')), 126) AS creation_time_str,             
            CONVERT (datetime, SWITCHOFFSET (CAST (MAX (stat.last_execution_time) AS datetimeoffset(7)), '+00:00')) AS last_execution_time, 
            CONVERT (datetime, SWITCHOFFSET (CAST (stat.collection_time_chart AS datetimeoffset(7)), '+00:00')) AS collection_time, 
            snap.source_id, 
            SUM (stat.snapshot_execution_count) AS execution_count, 
            SUM (stat.snapshot_worker_time / 1000) AS total_cpu, 
            SUM (stat.snapshot_worker_time / 1000) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_cpu_per_exec, 
            SUM (stat.snapshot_physical_reads) AS total_physical_reads, 
            SUM (stat.snapshot_physical_reads) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_physical_reads_per_exec, 
            SUM (stat.snapshot_logical_writes) AS total_logical_writes, 
            SUM (stat.snapshot_logical_writes) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_logical_writes_per_exec, 
            SUM (stat.snapshot_elapsed_time / 1000) AS total_elapsed_time, 
            SUM (stat.snapshot_elapsed_time / 1000) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS avg_elapsed_time_per_exec, 
            COUNT(*) AS row_count, COUNT (DISTINCT stat.creation_time) AS plan_count, 
            SUM (stat.charted_value) AS charted_value, 
            SUM (stat.charted_value) / CASE SUM (stat.snapshot_execution_count) WHEN 0 THEN 1 ELSE SUM (stat.snapshot_execution_count) END AS charted_value_per_exec, 
            MAX (topN.plan_rank) AS plan_rank -- same value for all rows within a group
        FROM (
            -- Work around a RS chart limitation (single data points do not plot on line charts). 
            -- Fake a second data point shortly after the first so even short-lived plans will 
            -- gets plotted. 
            SELECT *, collection_time AS collection_time_chart FROM raw_stat
            UNION ALL
            SELECT *, DATEADD (mi, 1, collection_time) AS collection_time_chart FROM raw_stat
        ) AS stat
        INNER JOIN #top_plans AS topN 
            ON topN.plan_handle = stat.plan_handle AND topN.creation_time = stat.creation_time
        INNER JOIN core.snapshots snap ON stat.snapshot_id = snap.snapshot_id
        WHERE
            stat.sql_handle = @sql_handle 
            AND statement_start_offset = @statement_start_offset 
            AND statement_end_offset = @statement_end_offset
            AND snap.instance_name = @instance_name 
            AND snap.snapshot_time_id BETWEEN @start_snapshot_time_id AND @end_snapshot_time_id
        GROUP BY stat.sql_handle, stat.statement_start_offset, stat.statement_end_offset, stat.plan_handle, 
                stat.creation_time, snap.source_id, stat.collection_time_chart
    ) AS t
    WHERE 
        (plan_rank <= 10)
    ORDER BY plan_rank ASC, collection_time ASC
    -- These trace flags are necessary for a good plan, due to the join on ascending PK w/range filter
    OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390)
END
GO


--
-- snapshots.rpt_query_plan_missing_indexes
--  Returns any missing indexes recorded in a query plan, formatted as CREATE INDEX statements. 
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @plan_handle_str - String representation of a plan handle (e.g. "0x1F27BC...")
--    @statement_start_offset - Start offset (byte count) for the statement within the plan identified by @plan_handle_str
--    @statement_start_offset - End offset (byte count) for the statement within the plan identified by @plan_handle_str
--
IF (NOT OBJECT_ID(N'snapshots.rpt_query_plan_missing_indexes', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_query_plan_missing_indexes] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_query_plan_missing_indexes]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_query_plan_missing_indexes] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_query_plan_missing_indexes]
    @instance_name sysname, 
    @plan_handle_str varchar(130), 
    @statement_start_offset int, 
    @statement_end_offset int 
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @showplan nvarchar(max)
    DECLARE @plan_handle varbinary(64)
    DECLARE @element nvarchar(512), @element_start nvarchar(512), @element_end nvarchar(512)
    DECLARE @xml_fragment xml
    DECLARE @start_offset int

    -- We may be invoked with a NULL/empty string plan handle if the user has not yet drilled down into 
    -- a specific query plan on the query_stats_detail report. 
    IF ISNULL (@plan_handle_str, '') = '' 
    BEGIN
        RETURN
    END

    SET @plan_handle = snapshots.fn_hexstrtovarbin (@plan_handle_str)

    SELECT TOP 1 
        @showplan = CONVERT (nvarchar(max), qp.query_plan) 
    FROM snapshots.notable_query_plan qp
    INNER JOIN core.snapshots snap ON snap.source_id = qp.source_id 
    WHERE plan_handle = @plan_handle 
        AND statement_start_offset = @statement_start_offset AND statement_end_offset = @statement_end_offset
        AND snap.instance_name = @instance_name
        -- Get sql_handle to enable a clustered index seek on notable_query_plans
        AND qp.sql_handle = 
        (
            SELECT TOP 1 sql_handle 
            FROM snapshots.notable_query_plan AS qp
            INNER JOIN core.snapshots snap ON snap.source_id = qp.source_id 
            WHERE plan_handle = @plan_handle
                AND statement_start_offset = @statement_start_offset AND statement_end_offset = @statement_end_offset 
                AND snap.instance_name = @instance_name
        );

    SET @element = 'MissingIndexes'
    -- Use non-XML methods to extract the <MissingIndexes> fragment.  Some query plans may be too complex 
    -- to represent in the T-SQL xml data type, but the <MissingIndexes> node will always be simple enough. 
    -- Doing this ensures that we will always be able to extract any missing index information from the 
    -- plan even if the plan itself is too complex for the T-SQL xml type. 
    SET @element_start = '<' + @element + '>'
    SET @element_end = '</' + @element + '>'
    SET @start_offset = ISNULL (PATINDEX ('%' + @element_start + '%', @showplan), 0)
    IF @start_offset > 0 
    BEGIN
        SET @xml_fragment = SUBSTRING (@showplan, @start_offset, PATINDEX ('%' + @element_end + '%', @showplan) - @start_offset + LEN (@element_end)); 

        --  Sample <MissingIndexes> fragment from an XML query plan: 
        --    <MissingIndexes>
        --      <MissingIndexGroup Impact="26.4126">
        --        <MissingIndex Database="[AdventureWorks]" Schema="[Sales]" Table="[CustomerAddress]">
        --          <ColumnGroup Usage="EQUALITY">
        --            <Column Name="[AddressTypeID]" ColumnId="3"/>
        --          </ColumnGroup>
        --          <ColumnGroup Usage="INCLUDE">
        --          <Column Name="[CustomerID]" ColumnId="1"/>
        --            <Column Name="[AddressID]" ColumnId="2"/>
        --          </ColumnGroup>
        --        </MissingIndex>
        --      </MissingIndexGroup>
        --    </MissingIndexes>
        SELECT 
            'CREATE INDEX [ncidx_mdw_' 
                + LEFT (target_object_name, 20)
                -- Random component to make name conflicts less likely
                + '_' + CONVERT (varchar(30), ABS (CONVERT (binary(6), NEWID()) % 1000))
                + '] ON ' + target_object_fullname 
                + ' (' + ISNULL (equality_columns, '')
                + CASE WHEN ISNULL (equality_columns, '') != '' AND ISNULL (inequality_columns, '') != '' THEN ',' ELSE '' END + ISNULL (inequality_columns, '')
                + ')'
                + CASE WHEN ISNULL (included_columns, '') != '' THEN ' INCLUDE (' + included_columns + ')' ELSE '' END
                AS create_idx_statement, 
            *
        FROM 
        (
            SELECT 
                index_node.value('(../@Impact)[1]', 'float') as index_impact,
                REPLACE (REPLACE (index_node.value('(./@Table)[1]', 'nvarchar(512)'), '[', ''), ']', '') AS target_object_name,
                CONVERT (nvarchar(1024), index_node.query('concat(
                        string((./@Database)[1]), 
                        ".",
                        string((./@Schema)[1]),
                        ".",
                        string((./@Table)[1])
                    )')) AS target_object_fullname,
                REPLACE (CONVERT (nvarchar(max), index_node.query('for $colgroup in ./ColumnGroup,
                        $col in $colgroup/Column
                        where $colgroup/@Usage = "EQUALITY"
                        return string($col/@Name)')), '] [', '],[') AS equality_columns,
                REPLACE (CONVERT (nvarchar(max), index_node.query('for $colgroup in ./ColumnGroup,
                        $col in $colgroup/Column
                        where $colgroup/@Usage = "INEQUALITY"
                        return string($col/@Name)')), '] [', '],[') AS inequality_columns,
                REPLACE (CONVERT (nvarchar(max), index_node.query('for $colgroup in .//ColumnGroup,
                        $col in $colgroup/Column
                        where $colgroup/@Usage = "INCLUDE"
                        return string($col/@Name)')), '] [', '],[') AS included_columns
            FROM (SELECT @xml_fragment AS fragment) AS missing_indexes_fragment
            CROSS APPLY missing_indexes_fragment.fragment.nodes('/MissingIndexes/MissingIndexGroup/MissingIndex') AS missing_indexes (index_node)
        ) AS t
    END
END
GO

--
-- snapshots.rpt_query_plan_parameters
--  Returns the compile-time parameters that a query plan was optimized for
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @plan_handle_str - String representation of a plan handle (e.g. "0x1F27BC...")
--    @statement_start_offset - Start offset (byte count) for the statement within the plan identified by @plan_handle_str
--    @statement_start_offset - End offset (byte count) for the statement within the plan identified by @plan_handle_str
--
IF (NOT OBJECT_ID(N'snapshots.rpt_query_plan_parameters', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_query_plan_parameters] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].rpt_query_plan_parameters
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_query_plan_parameters] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].rpt_query_plan_parameters
    @instance_name sysname, 
    @plan_handle_str varchar(130), 
    @statement_start_offset int, 
    @statement_end_offset int 
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @showplan xml
    DECLARE @plan_handle varbinary(64)

    -- We may be invoked with a NULL/empty string plan handle if the user has not yet drilled down into 
    -- a specific query plan on the query_stats_detail report. 
    IF ISNULL (@plan_handle_str, '') = '' 
    BEGIN
        RETURN
    END

    SET @plan_handle = snapshots.fn_hexstrtovarbin (@plan_handle_str)

    BEGIN TRY
        SELECT TOP 1 
            @showplan = CONVERT (xml, qp.query_plan) 
        FROM snapshots.notable_query_plan qp
        INNER JOIN core.snapshots snap ON snap.source_id = qp.source_id 
        WHERE plan_handle = @plan_handle 
            AND statement_start_offset = @statement_start_offset AND statement_end_offset = @statement_end_offset 
            AND snap.instance_name = @instance_name
            -- Get sql_handle to enable a clustered index seek on notable_query_plans
            AND qp.sql_handle = 
            (
                SELECT TOP 1 sql_handle 
                FROM snapshots.notable_query_plan AS qp
                INNER JOIN core.snapshots snap ON snap.source_id = qp.source_id 
                WHERE plan_handle = @plan_handle
                    AND statement_start_offset = @statement_start_offset AND statement_end_offset = @statement_end_offset 
                    AND snap.instance_name = @instance_name
            );
    END TRY
    BEGIN CATCH
        -- It is expected that we may end up here even under normal circumstances.  Some plans are simply too 
        -- complex to represent using T-SQL's xml datatype. Raise a low-severity message with the error details. 
        DECLARE @ErrorMessage   NVARCHAR(4000);
        DECLARE @ErrorNumber    INT;
        DECLARE @ErrorLine      INT;
        SELECT @ErrorLine = ERROR_LINE(),
               @ErrorNumber = ERROR_NUMBER(),
               @ErrorMessage = ERROR_MESSAGE();
        -- "Unable to convert showplan to XML.  Error #%d on Line %d: %s"
        RAISERROR (14697, 0, 1, @ErrorNumber, @ErrorLine, @ErrorMessage);
    END CATCH;

    WITH XMLNAMESPACES ('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS sp)
    SELECT 
        param_list.param_node.value('(./@Column)[1]', 'nvarchar(512)') AS param_name,
        param_list.param_node.value('(./@ParameterCompiledValue)[1]', 'nvarchar(max)') AS param_compiled_value 
    FROM (SELECT @showplan AS query_plan) AS p
    CROSS APPLY p.query_plan.nodes ('/sp:ShowPlanXML/sp:BatchSequence/sp:Batch/sp:Statements/sp:StmtSimple/sp:QueryPlan[1]/sp:ParameterList[1]/sp:ColumnReference') as param_list (param_node)
END
GO


--
-- snapshots.rpt_query_plan_details
--  Returns details of a query plan (estimated cost, compile-time CPU, etc)
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @plan_handle_str - String representation of a plan handle (e.g. "0x1F27BC...")
--    @statement_start_offset - Start offset (byte count) for the statement within the plan identified by @plan_handle_str
--    @statement_start_offset - End offset (byte count) for the statement within the plan identified by @plan_handle_str
--
IF (NOT OBJECT_ID(N'snapshots.rpt_query_plan_details', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_query_plan_details] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].rpt_query_plan_details
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_query_plan_details] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].rpt_query_plan_details
    @instance_name sysname, 
    @plan_handle_str varchar(130), 
    @statement_start_offset int, 
    @statement_end_offset int 
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @showplan xml
    DECLARE @plan_handle varbinary(64)

    -- We may be invoked with a NULL/empty string plan handle if the user has not yet drilled down into 
    -- a specific query plan on the query_stats_detail report. 
    IF ISNULL (@plan_handle_str, '') = '' 
    BEGIN
        RETURN
    END

    SET @plan_handle = snapshots.fn_hexstrtovarbin (@plan_handle_str)

    BEGIN TRY
        SELECT TOP 1 
            @showplan = CONVERT (xml, qp.query_plan) 
        FROM snapshots.notable_query_plan AS qp 
        INNER JOIN core.snapshots snap ON snap.source_id = qp.source_id 
        WHERE plan_handle = @plan_handle 
            AND statement_start_offset = @statement_start_offset AND statement_end_offset = @statement_end_offset 
            AND snap.instance_name = @instance_name 
            -- Get sql_handle to enable a clustered index seek on notable_query_plans
            AND qp.sql_handle = 
            (
                SELECT TOP 1 sql_handle 
                FROM snapshots.notable_query_plan AS qp
                INNER JOIN core.snapshots snap ON snap.source_id = qp.source_id 
                WHERE plan_handle = @plan_handle
                    AND statement_start_offset = @statement_start_offset AND statement_end_offset = @statement_end_offset 
                    AND snap.instance_name = @instance_name
            );
    END TRY
    BEGIN CATCH
        -- It is expected that we may end up here even under normal circumstances.  Some plans are simply too 
        -- complex to represent using T-SQL's xml datatype. Raise a low-severity message with the error details. 
        DECLARE @ErrorMessage   NVARCHAR(4000);
        DECLARE @ErrorNumber    INT;
        DECLARE @ErrorLine      INT;
        SELECT @ErrorLine = ERROR_LINE(),
               @ErrorNumber = ERROR_NUMBER(),
               @ErrorMessage = ERROR_MESSAGE();
        -- "Unable to convert showplan to XML.  Error #%d on Line %d: %s"
        RAISERROR (14697, 0, 1, @ErrorNumber, @ErrorLine, @ErrorMessage);
        RETURN
    END CATCH;

    WITH XMLNAMESPACES ('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS sp)
    SELECT TOP 10 
        CONVERT (bigint, stmt_simple.stmt_node.value('(./@StatementEstRows)[1]', 'decimal(28,10)')) AS stmt_est_rows, 
        stmt_simple.stmt_node.value('(./@StatementOptmLevel)[1]', 'varchar(30)') AS stmt_optimization_level, 
        stmt_simple.stmt_node.value('(./@StatementOptmEarlyAbortReason)[1]', 'varchar(30)') AS stmt_optimization_early_abort_reason, 
        stmt_simple.stmt_node.value('(./@StatementSubTreeCost)[1]', 'float') AS stmt_est_subtree_cost, 
        stmt_simple.stmt_node.value('(./@StatementText)[1]', 'nvarchar(max)') AS stmt_text, 
        stmt_simple.stmt_node.value('(./@ParameterizedText)[1]', 'nvarchar(max)') AS stmt_parameterized_text, 
        stmt_simple.stmt_node.value('(./@StatementType)[1]', 'varchar(30)') AS stmt_type, 
        stmt_simple.stmt_node.value('(./@PlanGuideName)[1]', 'varchar(30)') AS plan_guide_name, 
        stmt_simple.stmt_node.value('(./sp:QueryPlan/@CachedPlanSize)[1]', 'int') AS plan_size, 
        stmt_simple.stmt_node.value('(./sp:QueryPlan/@CompileTime)[1]', 'bigint') AS plan_compile_time, 
        stmt_simple.stmt_node.value('(./sp:QueryPlan/@CompileCPU)[1]', 'bigint') AS plan_compile_cpu, 
        stmt_simple.stmt_node.value('(./sp:QueryPlan/@CompileMemory)[1]', 'int') AS plan_compile_memory 
    FROM (SELECT @showplan AS query_plan) AS p
    CROSS APPLY p.query_plan.nodes ('/sp:ShowPlanXML/sp:BatchSequence/sp:Batch/sp:Statements/sp:StmtSimple') as stmt_simple (stmt_node)
END
GO


--
-- snapshots.rpt_blocking_chains
--  Returns summary of blocking chains that existed in a specified time window
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @start_time - time window start (UTC)
--    @end_time - time window end (UTC)
--
IF (NOT OBJECT_ID(N'snapshots.rpt_blocking_chains', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_blocking_chains] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_blocking_chains]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_blocking_chains] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_blocking_chains]
    @instance_name sysname,
    @start_time datetime = NULL, 
    @end_time datetime, 
    @WindowSize int = NULL
AS
BEGIN
SET NOCOUNT ON;

    -- Compensate for RS truncation of fractional seconds
    SET @end_time = DATEADD (second, 1, @end_time) 

    -- If @start_time is NULL, calc it using @end_time and @WindowSize
    IF @start_time IS NULL SET @start_time = DATEADD (minute, -1 * @WindowSize, @end_time)

    -- Get all collection times for the "Active Sessions and Requests" collection item
    SELECT DISTINCT r1.collection_time, r1.snapshot_id, 
        DENSE_RANK() OVER (ORDER BY r1.collection_time) AS collection_time_id
    INTO #collection_times
    FROM snapshots.active_sessions_and_requests AS r1 
    INNER JOIN core.snapshots s ON s.snapshot_id = r1.snapshot_id 
    WHERE 
        s.instance_name = @instance_name 
        AND r1.collection_time BETWEEN @start_time AND @end_time

    DECLARE @max_collection_time datetimeoffset(7)
    SELECT @max_collection_time = MAX (collection_time) FROM #collection_times

    -- Get all head blockers during the selected time window
    SELECT r1.*, times.collection_time_id
    INTO #blocking_participants 
    FROM #collection_times AS times
    LEFT OUTER JOIN snapshots.active_sessions_and_requests AS r1 
        ON r1.collection_time = times.collection_time AND r1.snapshot_id = times.snapshot_id
    WHERE r1.blocking_session_id = 0
        AND session_id IN (
            SELECT DISTINCT blocking_session_id 
            FROM snapshots.active_sessions_and_requests AS r2 
            WHERE r2.blocking_session_id != 0 AND r2.collection_time = r1.collection_time
                AND r2.snapshot_id = r1.snapshot_id
        )
    CREATE NONCLUSTERED INDEX IDX1_blocking_participants 
    ON #blocking_participants 
    (
        session_id, collection_time_id, blocking_session_id
    )

    -- List all blocking chains during this time window. 
    -- For the purposes of this overview, we define a blocking chain as a contiguous series 
    -- of samples where the same spid remains the head blocker.  Rolling blocking will be viewed 
    -- as multiple discrete chains. 
    SELECT 
        MIN (head_blockers.collection_time) AS blocking_start_time, 
        -- We know when the blocking ended within a (roughly) 10 second window. Assume it stopped approximately 
        -- at the midpoint. 
        DATEADD (second, 5, ISNULL (MAX (blocking_end_times.collection_time), @max_collection_time)) AS blocking_end_time, 
        DATEDIFF (second, MIN (head_blockers.collection_time), ISNULL (MAX (blocking_end_times.collection_time), @max_collection_time)) 
            AS blocking_duration_sec,
        head_blockers.session_id AS head_blocker_session_id, 
        MIN (head_blockers.[program_name]) AS [program_name], 
        MIN (head_blockers.[database_name]) AS [database_name], 
        COUNT(*) AS observed_sample_count,  -- Number of times we saw this blocking chain
        CASE WHEN MAX (blocking_end_times.collection_time) IS NULL THEN 1 ELSE 0 END AS still_active
    INTO #blocking_chains
    FROM 
    (
        SELECT 
            (   -- Find the end time for this blocking incident
                SELECT MIN (collection_time_id)
                FROM #collection_times AS times1
                WHERE times1.collection_time_id > blockers_start.collection_time_id 
                    AND times1.collection_time_id <= @end_time 
                    AND NOT EXISTS (
                        SELECT * FROM #blocking_participants AS blk1
                        WHERE blk1.session_id = blockers_start.session_id 
                            AND blk1.[program_name] = blockers_start.[program_name] 
                            AND blk1.login_time = blockers_start.login_time 
                            AND blk1.collection_time_id = times1.collection_time_id
                            AND blk1.blocking_session_id = 0
                    )
            ) AS blocking_end_collection_time_id, 
            *
        FROM #blocking_participants AS blockers_start 
        WHERE blockers_start.blocking_session_id = 0 
    ) AS head_blockers
    LEFT OUTER JOIN #collection_times AS blocking_end_times
        ON blocking_end_times.collection_time_id = head_blockers.blocking_end_collection_time_id - 1
    GROUP BY head_blockers.session_id, head_blockers.blocking_end_collection_time_id 
    ORDER BY MIN (head_blockers.collection_time)

    -- This proc supports two different chart elements: a table, with one row per blocking 
    -- chain, and a timeline chart, with one series per blocking chain.  The chart must 
    -- return two data points per series in order to correctly plot the blocking chain's 
    -- beginning and end times.  The chart uses the output of both of the following UNIONed 
    -- SELECT statements, while the table filters out the second resultset 
    -- ([chart_data_only]=0). Doing this avoids the need to waste time running two procs 
    -- that are almost identical.  
    SELECT 
        blocking_chain_number, 
        CONVERT (datetime, SWITCHOFFSET (CAST (blocking_start_time AS datetimeoffset(7)), '+00:00')) AS blocking_start_time, 
        CONVERT (datetime, SWITCHOFFSET (CAST (blocking_end_time AS datetimeoffset(7)), '+00:00')) AS blocking_end_time, 
        blocking_duration_sec, 
        head_blocker_session_id, 
        [program_name], 
        [database_name], 
        observed_sample_count, 
        still_active, 
        -- Represent this time as a string to avoid RS datetime truncation when the report passes it back to us on drillthrough
        CONVERT (varchar(40), SWITCHOFFSET (CAST (blocking_start_time AS datetimeoffset(7)), '+00:00'), 126) AS blocking_start_time_str, 
        CONVERT (datetime, SWITCHOFFSET (CAST (chart_time AS datetimeoffset(7)), '+00:00')) AS chart_time, 
        chart_data_only
    FROM 
    (
        SELECT TOP 10 ROW_NUMBER() OVER (ORDER BY blocking_duration_sec DESC) AS blocking_chain_number, 
            *, blocking_start_time AS chart_time, 
            0 AS chart_data_only 
        FROM #blocking_chains
        ORDER BY blocking_duration_sec DESC
        UNION ALL 
        SELECT TOP 10 ROW_NUMBER() OVER (ORDER BY blocking_duration_sec DESC) AS blocking_chain_number, 
            *, blocking_end_time AS chart_time, 
            1 AS chart_data_only 
        FROM #blocking_chains
        ORDER BY blocking_duration_sec DESC
    ) AS t
    ORDER BY blocking_chain_number, chart_data_only
END
GO


--
-- snapshots.rpt_blocking_chain_detail
--  Returns details about a blocking chain
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @blocking_time_str - a string representation of a datetimeoffset value during the 
--        period where @head_blocker_session_id was the cause of blocking (UTC)
--    @head_blocker_session_id - session ID (SPID) of the head of the blocker chain
--
IF (NOT OBJECT_ID(N'snapshots.rpt_blocking_chain_detail', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_blocking_chain_detail] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_blocking_chain_detail]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_blocking_chain_detail] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_blocking_chain_detail]
    @instance_name sysname, 
    @blocking_time_str varchar(40), 
    @head_blocker_session_id int
AS
BEGIN
SET NOCOUNT ON;
    DECLARE @blocking_start_time datetimeoffset(7)
    DECLARE @blocking_end_time datetimeoffset(7)
    DECLARE @blocking_time datetimeoffset(7)
    -- The report passed in the blocking time as a string to avoid RS date truncation. 
    -- Convert this to a datetimeoffset value in UTC time.
    SET @blocking_time = SWITCHOFFSET (CAST (@blocking_time_str AS datetimeoffset(7)), '+00:00')

    -- The time that we were passed in may have been in the middle of the blocking incident. 
    -- Find the true start time for this blocking chain. This might be 10 seconds prior, or 
    -- might be days prior.  For perf reasons, search backwards in time one hour at a time 
    -- until we find the start of the blocking incident. 
    DECLARE @blocking_end_snapshot_id int
    DECLARE @blocking_end_source_id int
    DECLARE @hour_count int
    SET @hour_count = -1
    WHILE (@blocking_start_time IS NULL)
    BEGIN
        -- Only if we have more rows left to search for the blocking end time
        IF EXISTS (
            SELECT * 
            FROM snapshots.active_sessions_and_requests AS r 
            INNER JOIN core.snapshots AS s ON s.snapshot_id = r.snapshot_id
            WHERE s.instance_name = @instance_name 
                AND r.collection_time < DATEADD (hour, @hour_count+1, @blocking_time)
        )
        BEGIN
            SELECT TOP 1 @blocking_start_time = r.collection_time 
            FROM snapshots.active_sessions_and_requests AS r 
            INNER JOIN core.snapshots AS s ON s.snapshot_id = r.snapshot_id
            WHERE s.instance_name = @instance_name
                AND r.collection_time BETWEEN DATEADD (hour, @hour_count, @blocking_time) AND @blocking_time 
                AND NOT EXISTS 
                ( 
                    SELECT * 
                    FROM snapshots.active_sessions_and_requests AS r2 
                    WHERE r.snapshot_id = r2.snapshot_id AND r.collection_time = r2.collection_time 
                        AND r2.blocking_session_id = @head_blocker_session_id 
                ) 
            ORDER BY r.collection_time DESC
        END
        ELSE
        BEGIN
            -- We've reached the beginning of the data in the warehouse, and the blocking incident was already 
            -- in-progress at that time. Use the earliest collection time as the approx blocking start time. 
            SELECT @blocking_start_time = ISNULL (DATEADD (second, -10, MIN (r.collection_time)), GETUTCDATE())
            FROM snapshots.active_sessions_and_requests AS r 
            INNER JOIN core.snapshots AS s ON s.snapshot_id = r.snapshot_id
            WHERE s.instance_name = @instance_name
        END
        SET @hour_count = @hour_count - 1
    END
    -- We've found the collection_time just before the blocking began. Get the next collection time, 
    -- which is the first collection time where this blocking incident was detected. 
    SELECT TOP 1 @blocking_start_time = r.collection_time
    FROM snapshots.active_sessions_and_requests AS r 
    INNER JOIN core.snapshots AS s ON s.snapshot_id = r.snapshot_id
    WHERE s.instance_name = @instance_name AND r.collection_time > @blocking_start_time
    ORDER BY r.collection_time ASC

    -- Now find the end of the blocking incident.  Here, again, do an optimistic search in 1-hour blocks. 
    SET @hour_count = 1
    WHILE (@blocking_end_time IS NULL)
    BEGIN
        -- Only if we have more rows left to search for the blocking end time
        IF EXISTS (
            SELECT * 
            FROM snapshots.active_sessions_and_requests AS r 
            INNER JOIN core.snapshots AS s ON s.snapshot_id = r.snapshot_id
            WHERE s.instance_name = @instance_name 
                AND r.collection_time > DATEADD (hour, @hour_count-1, @blocking_time)
        )
        BEGIN
            SELECT TOP 1 @blocking_end_time = r.collection_time 
            FROM snapshots.active_sessions_and_requests AS r 
            INNER JOIN core.snapshots AS s ON s.snapshot_id = r.snapshot_id
            WHERE s.instance_name = @instance_name
                AND r.collection_time BETWEEN DATEADD (hour, @hour_count-1, @blocking_time) AND DATEADD (hour, @hour_count, @blocking_time)
                AND NOT EXISTS 
                ( 
                    SELECT * 
                    FROM snapshots.active_sessions_and_requests AS r2 
                    WHERE r.snapshot_id = r2.snapshot_id AND r.collection_time = r2.collection_time 
                        AND r2.blocking_session_id = @head_blocker_session_id 
                ) 
            ORDER BY r.collection_time ASC
        END
        ELSE
        BEGIN
            -- We've reached the end of the data in the warehouse, and the blocking incident is still 
            -- in-progress. Use the last collection time as the approx blocking end time. 
            SELECT @blocking_end_time = ISNULL (DATEADD (second, 10, MAX (r.collection_time)), GETUTCDATE())
            FROM snapshots.active_sessions_and_requests AS r 
            INNER JOIN core.snapshots AS s ON s.snapshot_id = r.snapshot_id
            WHERE s.instance_name = @instance_name
        END
        SET @hour_count = @hour_count + 1
    END
    -- We've found the collection_time just after before the blocking ended. Get the prior collection time, 
    -- which is the last collection time where the blocking incident was detected. 
    SELECT TOP 1 @blocking_end_time = r.collection_time, @blocking_end_snapshot_id = r.snapshot_id, @blocking_end_source_id = s.source_id
    FROM snapshots.active_sessions_and_requests AS r 
    INNER JOIN core.snapshots AS s ON s.snapshot_id = r.snapshot_id
    WHERE s.instance_name = @instance_name AND r.collection_time < @blocking_end_time
    ORDER BY r.collection_time DESC

    -- DC captures a snapshot of session state every few seconds, which would mean hundreds of samples for 
    -- moderately long-lived blocking chains.  It would be too expensive to summarize the state of the blocking 
    -- chain at every one of these points.  Instead, select 10 evenly-spaced intervals during the blocking 
    -- incident to characterize the changes in the head blocker's state over the blocking period. 
    DECLARE @interval_sec int
    SET @interval_sec = DATEDIFF (second, @blocking_start_time, @blocking_end_time) / 10
    CREATE TABLE #sample_collection_times (snapshot_id int, source_id int, collection_time datetimeoffset(7))
    DECLARE @i int
    SET @i = 0
    WHILE (@i < 9)
    BEGIN
        INSERT INTO #sample_collection_times 
        SELECT TOP 1 r.snapshot_id, s.source_id, r.collection_time
        FROM snapshots.active_sessions_and_requests AS r 
        INNER JOIN core.snapshots AS s ON s.snapshot_id = r.snapshot_id
        WHERE s.instance_name = @instance_name 
            AND r.collection_time BETWEEN DATEADD (second, @interval_sec * @i, @blocking_start_time)
                AND DATEADD (second, @interval_sec * (@i+1)-1, @blocking_start_time)
            -- Only choose collection times where we have info for the head blocker
            AND r.session_id = @head_blocker_session_id 
        ORDER BY r.collection_time ASC
        SET @i = @i + 1
    END
    -- The 10th sample time is always the blocking incident's final collection time
    INSERT INTO #sample_collection_times VALUES (@blocking_end_snapshot_id, @blocking_end_source_id, @blocking_end_time);

    -- Use a recursive CTE to walk the tree of the blocking chain at each of these collection times
    -- and get the state of all the sessions that were part of the tree
    WITH blocking_hierarchy AS  
    (
        -- Head blocker at each of the selected sample times
        SELECT t.collection_time, t.snapshot_id, t.source_id, 0 AS [level], 
            r.session_id, r.request_id, r.exec_context_id, r.request_status, r.command, 
            r.blocking_session_id, r.blocking_exec_context_id, 
            r.wait_type, r.wait_duration_ms, r.wait_resource, r.resource_description, 
            r.login_name, r.login_time, r.[program_name], r.[host_name], r.database_name, 
            r.open_transaction_count, r.transaction_isolation_level, 
            r.request_cpu_time, r.request_total_elapsed_time, r.request_start_time, r.memory_usage, 
            r.session_cpu_time, r.session_total_scheduled_time, r.session_row_count, r.pending_io_count, r.prev_error, 
            r.session_last_request_start_time, r.session_last_request_end_time, r.open_resultsets, 
            r.plan_handle, r.sql_handle, r.statement_start_offset, r.statement_end_offset 
        FROM #sample_collection_times AS t
        INNER JOIN snapshots.active_sessions_and_requests AS r
            ON t.snapshot_id = r.snapshot_id AND t.collection_time = r.collection_time
        WHERE r.session_id = @head_blocker_session_id 
            AND ISNULL (r.exec_context_id, 0) IN (-1, 0) -- for the head blocker, only return the main worker's state
        UNION ALL 
        -- Tasks blocked by the head blocker at the same times
        SELECT r2.collection_time, r2.snapshot_id, parent.source_id, parent.[level] + 1 AS [level], 
            r2.session_id, r2.request_id, r2.exec_context_id, r2.request_status, r2.command, 
            r2.blocking_session_id, r2.blocking_exec_context_id, 
            r2.wait_type, r2.wait_duration_ms, r2.wait_resource, r2.resource_description, 
            r2.login_name, r2.login_time, r2.[program_name], r2.[host_name], r2.database_name, 
            r2.open_transaction_count, r2.transaction_isolation_level, 
            r2.request_cpu_time, r2.request_total_elapsed_time, r2.request_start_time, r2.memory_usage, 
            r2.session_cpu_time, r2.session_total_scheduled_time, r2.session_row_count, r2.pending_io_count, r2.prev_error, 
            r2.session_last_request_start_time, r2.session_last_request_end_time, r2.open_resultsets, 
            r2.plan_handle, r2.sql_handle, r2.statement_start_offset, r2.statement_end_offset 
        FROM snapshots.active_sessions_and_requests AS r2
        INNER JOIN blocking_hierarchy AS parent 
            ON parent.snapshot_id = r2.snapshot_id AND parent.collection_time = r2.collection_time
                AND parent.session_id = r2.blocking_session_id
    )
    SELECT * INTO #blocking_hierarchy 
    FROM blocking_hierarchy 
    
    -- Summarize the state of the head blocker at each of the sample times, along with 
    -- some aggregate stats (# of blocked sessions, etc) describing the blocked sessions
    SELECT 
        CONVERT (datetime, SWITCHOFFSET (CAST (@blocking_start_time AS datetimeoffset(7)), '+00:00')) AS blocking_start_time, 
        CONVERT (datetime, SWITCHOFFSET (CAST (@blocking_end_time AS datetimeoffset(7)), '+00:00')) AS blocking_end_time, 
        DATEDIFF (second, @blocking_start_time, @blocking_end_time) AS blocking_duration, 
        -- Return these dates as strings to avoid RS date truncation
        CONVERT (varchar(40), SWITCHOFFSET (CAST (@blocking_start_time AS datetimeoffset(7)), '+00:00'), 126) AS blocking_start_time_str, 
        CONVERT (varchar(40), SWITCHOFFSET (CAST (@blocking_end_time AS datetimeoffset(7)), '+00:00'), 126) AS blocking_end_time_str, 
        CONVERT (datetime, SWITCHOFFSET (CAST (blocked.chart_collection_time AS datetimeoffset(7)), '+00:00')) AS chart_collection_time, 
        blocked.chart_only, 
        CONVERT (datetime, SWITCHOFFSET (CAST (blocked.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        blocked.snapshot_id, 
        CONVERT (varchar(40), SWITCHOFFSET (CAST (blocked.collection_time AS datetimeoffset(7)), '+00:00'), 126) AS collection_time_str, 
        @head_blocker_session_id AS head_blocker_session_id, 
        COUNT(DISTINCT blocked.session_id) AS blocked_session_count, 
        SUM (blocked.wait_duration_ms) AS total_wait_time, 
        AVG (blocked.wait_duration_ms) AS avg_wait_time, 
        (
            -- Get the description of the resource owned by the head blocker that 
            -- has caused the most wait time in this blocking chain
            SELECT TOP 1 resource_description
            FROM #blocking_hierarchy bh
            WHERE bh.collection_time = blocked.collection_time AND bh.snapshot_id = blocked.snapshot_id
                AND bh.blocking_session_id = @head_blocker_session_id
            GROUP BY resource_description
            ORDER BY SUM (wait_duration_ms) DESC
        ) AS primary_wait_resource_description, 
        ISNULL (blocker.command, 'AWAITING COMMAND') AS command, blocker.request_status, 
        wt.category_name, blocker.wait_type, blocker.wait_duration_ms, blocker.wait_resource, blocker.resource_description, 
        blocker.[program_name], blocker.[host_name], blocker.login_name, 
        CONVERT (datetime, SWITCHOFFSET (CAST (blocker.login_time AS datetimeoffset(7)), '+00:00')) AS login_time, 
        blocker.database_name, 
        MAX (blocker.open_transaction_count) AS open_transaction_count, MAX (blocker.transaction_isolation_level) AS transaction_isolation_level, 
        MAX (blocker.request_cpu_time) AS request_cpu_time, MAX (blocker.request_total_elapsed_time) AS request_total_elapsed_time, 
        MIN (blocker.request_start_time) AS request_start_time, MAX (blocker.memory_usage) AS memory_usage, 
        MAX (blocker.session_cpu_time) AS session_cpu_time, MAX (blocker.session_total_scheduled_time) AS session_total_scheduled_time, 
        MAX (blocker.session_row_count) AS session_row_count, MAX (blocker.pending_io_count) AS pending_io_count, 
        MAX (blocker.prev_error) AS prev_error, 
        CONVERT (datetime, SWITCHOFFSET (CAST (MAX (blocker.session_last_request_start_time) AS datetimeoffset(7)), '+00:00')) AS session_last_request_start_time, 
        CONVERT (datetime, SWITCHOFFSET (CAST (MAX (blocker.session_last_request_end_time) AS datetimeoffset(7)), '+00:00')) AS session_last_request_end_time, 
        MAX (blocker.open_resultsets) AS open_resultsets, 
        blocker.plan_handle, blocker.sql_handle, blocker.statement_start_offset, blocker.statement_end_offset, 
        -- RS can't handle binary values as parameters -- convert plan and sql handles to string types
        master.dbo.fn_varbintohexstr (blocker.plan_handle) AS plan_handle_str, 
        master.dbo.fn_varbintohexstr (blocker.sql_handle) AS sql_handle_str, 
        sql_text.[object_id], sql_text.[object_name], sql_text.query_text, 
        REPLACE (REPLACE (REPLACE (REPLACE (REPLACE (REPLACE (
            LEFT (LTRIM (sql_text.query_text), 100), 
            CHAR(9), ' '), CHAR(10), ' '), CHAR(13), ' '), '   ', ' '), '  ', ' '), '  ', ' ') AS flat_query_text
    -- RS won't plot series that only have a single data point on a line graph.  Duplicate each data 
    -- point with a slight time shift, which will allow the chart to provide some visualization even if 
    -- the blocking was transient (only seen during a single sample). For performance reasons, this 
    -- same resultset feeds both a chart and a table.  The duplicate records are flagged with 
    -- chart_only=1 so that they can be filtered out in the table. 
    FROM (
        SELECT *, collection_time AS chart_collection_time, 0 AS chart_only
        FROM #blocking_hierarchy 
        WHERE blocking_session_id != 0 
        UNION ALL 
        SELECT *, DATEADD (second, 10, collection_time) AS chart_collection_time, 1 AS chart_only 
        FROM #blocking_hierarchy 
        WHERE blocking_session_id != 0 
    ) AS blocked
    INNER JOIN (
        SELECT *, collection_time AS chart_collection_time, 0 AS chart_only
        FROM #blocking_hierarchy 
        WHERE blocking_session_id = 0 
        UNION ALL 
        SELECT *, DATEADD (second, 10, collection_time) AS chart_collection_time, 1 AS chart_only
        FROM #blocking_hierarchy 
        WHERE blocking_session_id = 0 
    ) AS blocker
        ON blocked.collection_time = blocker.collection_time AND blocked.snapshot_id = blocker.snapshot_id
            AND blocked.chart_collection_time = blocker.chart_collection_time 
    OUTER APPLY [snapshots].[fn_get_query_text] (blocker.source_id, blocker.sql_handle, blocker.statement_start_offset, blocker.statement_end_offset) AS sql_text
    LEFT OUTER JOIN core.wait_types_categorized AS wt ON blocker.wait_type = wt.wait_type
    WHERE 
        blocker.blocking_session_id = 0 AND blocked.blocking_session_id != 0
    GROUP BY blocked.chart_collection_time, blocked.chart_only, blocked.collection_time, blocked.snapshot_id, 
        blocker.command, blocker.request_status, 
        wt.category_name, blocker.wait_type, blocker.wait_duration_ms, blocker.wait_resource, blocker.resource_description, 
        blocker.[program_name], blocker.[host_name], blocker.login_name, blocker.login_time, blocker.database_name, 
        blocker.plan_handle, blocker.sql_handle, blocker.statement_start_offset, blocker.statement_end_offset, 
        sql_text.[object_id], sql_text.[object_name], sql_text.query_text  
    ORDER BY blocked.collection_time ASC

END 
GO

--
-- snapshots.rpt_active_sessions_and_requests
--  Returns all the active sessions/requests at a particular moment in time
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @collection_time - a time that corresponds to a specific [collection_time] in snapshots.active_sessions_and_requests (UTC)
--
IF (NOT OBJECT_ID(N'snapshots.rpt_active_sessions_and_requests', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_active_sessions_and_requests] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_active_sessions_and_requests]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_active_sessions_and_requests] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_active_sessions_and_requests]
    @instance_name sysname, 
    @collection_time datetime
AS
BEGIN
SET NOCOUNT ON;
    -- Find the nearest collection time on or before the user-specified time
    DECLARE @current_collection_time datetimeoffset(7)
    DECLARE @current_snapshot_id int
    DECLARE @query_stats_source_id int

    -- Compensate for RS truncation of fractional seconds 
    SET @current_collection_time = DATEADD(second, 1, @collection_time)

    SELECT TOP 1 @current_collection_time = r.collection_time, @current_snapshot_id = r.snapshot_id
    FROM core.snapshots AS s
    INNER JOIN snapshots.active_sessions_and_requests AS r ON s.snapshot_id = r.snapshot_id
    WHERE s.instance_name = @instance_name
      AND r.collection_time <= @current_collection_time
    ORDER BY collection_time DESC

    -- Get the source_id for the Query Stats collection set on this server
    SELECT @query_stats_source_id = s.source_id
    FROM core.snapshots AS s
    WHERE s.instance_name = @instance_name AND s.collection_set_uid = '2DC02BD6-E230-4C05-8516-4E8C0EF21F95'

    -- Get all active sessions/requests at that time
    SELECT 
        r.session_id, r.request_id, r.exec_context_id, ISNULL (r.blocking_session_id, 0) AS blocking_session_id, r.blocking_exec_context_id, 
        r.scheduler_id, r.database_name, r.[user_id], r.task_state, r.request_status, r.session_status, 
        r.executing_managed_code, 
        CONVERT (datetime, SWITCHOFFSET (CAST (r.login_time AS datetimeoffset(7)), '+00:00')) AS login_time, 
        r.is_user_process, r.[host_name], r.[program_name], r.login_name, r.wait_type, r.last_wait_type, 
        r.wait_duration_ms, r.wait_resource, r.resource_description, r.transaction_id, 
        r.open_transaction_count, r.transaction_isolation_level, r.request_cpu_time, 
        r.request_logical_reads, r.request_reads, r.request_writes, r.request_total_elapsed_time, 
        CONVERT (datetime, SWITCHOFFSET (CAST (r.request_start_time AS datetimeoffset(7)), '+00:00')) AS request_start_time, 
        r.memory_usage, r.session_cpu_time, r.session_reads, r.session_writes, 
        r.session_logical_reads, r.session_total_scheduled_time, r.session_total_elapsed_time, 
        CONVERT (datetime, SWITCHOFFSET (CAST (r.session_last_request_start_time AS datetimeoffset(7)), '+00:00')) AS session_last_request_start_time, 
        CONVERT (datetime, SWITCHOFFSET (CAST (r.session_last_request_end_time AS datetimeoffset(7)), '+00:00')) AS session_last_request_end_time, 
        r.open_resultsets, r.session_row_count, r.prev_error, r.pending_io_count, ISNULL (r.command, 'AWAITING COMMAND') AS command, 
        r.plan_handle, r.sql_handle, r.statement_start_offset, r.statement_end_offset, 
        CONVERT (datetime, SWITCHOFFSET (CAST (r.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        r.snapshot_id, 
        wt.category_name AS wait_category, 
        sql.*, 
        master.dbo.fn_varbintohexstr (r.sql_handle) AS sql_handle_str, 
        master.dbo.fn_varbintohexstr (r.plan_handle) AS plan_handle_str, 
        REPLACE (REPLACE (REPLACE (REPLACE (REPLACE (REPLACE (
            LEFT (LTRIM (sql.query_text), 100)
            , CHAR(9), ' '), CHAR(10), ' '), CHAR(13), ' '), '   ', ' '), '  ', ' '), '  ', ' ') AS flat_query_text 
    FROM snapshots.active_sessions_and_requests AS r
    LEFT OUTER JOIN core.wait_types_categorized AS wt ON r.wait_type = wt.wait_type
    OUTER APPLY snapshots.fn_get_query_text (@query_stats_source_id, r.sql_handle, r.statement_start_offset, r.statement_end_offset) AS sql
    WHERE r.snapshot_id = @current_snapshot_id AND r.collection_time = @current_collection_time
END
GO


--
-- snapshots.rpt_sql_memory_clerks
--  Returns data from os_memory_clerks table
-- Parameters: 
--    @ServerName - SQL Server instance name
--    @EndTime - End of the user-selected time window (UTC)
--    @WindowSize - Number of minutes in the time window 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_sql_memory_clerks', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_sql_memory_clerks] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_sql_memory_clerks]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_sql_memory_clerks] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_sql_memory_clerks]
    @ServerName sysname,
    @EndTime datetime = NULL,
    @WindowSize smallint = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Divide our time window up into 40 evenly-sized time intervals, and find the last collection_time within each of these intervals
    CREATE TABLE #intervals (
        interval_time_id        int, 
        interval_start_time     datetimeoffset(7),
        interval_end_time       datetimeoffset(7),
        interval_id             int, 
        first_collection_time   datetimeoffset(7), 
        last_collection_time    datetimeoffset(7), 
        first_snapshot_id       int,
        last_snapshot_id        int, 
        source_id               int, 
        snapshot_id             int, 
        collection_time         datetimeoffset(7), 
        collection_time_id      int
    )
    -- GUID 49268954-... is the Server Activity CS
    INSERT INTO #intervals
    EXEC [snapshots].[rpt_interval_collection_times] 
        @ServerName, @EndTime, @WindowSize, 'snapshots.os_memory_clerks', '49268954-4FD4-4EB6-AA04-CD59D9BB5714', 40, 0
    
    -- Get memory clerk stats for these collection times
    SELECT 
        coll.interval_time_id, 
        -- Convert datetimeoffsets to UTC datetimes
        CONVERT (datetime, SWITCHOFFSET (coll.interval_start_time, '+00:00')) AS interval_start_time, 
        CONVERT (datetime, SWITCHOFFSET (coll.interval_end_time,  '+00:00')) AS interval_end_time, 
        coll.interval_id, 
        CONVERT (datetime, SWITCHOFFSET (coll.first_collection_time, '+00:00')) AS first_collection_time, 
        CONVERT (datetime, SWITCHOFFSET (coll.last_collection_time, '+00:00')) AS last_collection_time, 
        coll.first_snapshot_id, coll.last_snapshot_id, 
        mc.[type], mc.memory_node_id, mc.single_pages_kb, mc.multi_pages_kb, mc.virtual_memory_reserved_kb, 
        mc.virtual_memory_committed_kb, mc.awe_allocated_kb, mc.shared_memory_reserved_kb, mc.shared_memory_committed_kb, 
        CONVERT (datetime, SWITCHOFFSET (mc.collection_time, '+00:00')) AS collection_time, 
        CAST (mc.single_pages_kb AS bigint) 
            + mc.multi_pages_kb 
            + (CASE WHEN type <> 'MEMORYCLERK_SQLBUFFERPOOL' THEN mc.virtual_memory_committed_kb ELSE 0 END) 
            + mc.shared_memory_committed_kb AS total_kb
    INTO #memory_clerks
    FROM snapshots.os_memory_clerks AS mc
    INNER JOIN #intervals AS coll ON coll.last_snapshot_id = mc.snapshot_id AND coll.last_collection_time = mc.collection_time 

    -- Return memory stats to the caller
    SELECT 
        mc.*, 
        mc.single_pages_kb + mc.multi_pages_kb as allocated_kb,
        ta.total_kb_all_clerks, 
        mc.total_kb / CONVERT(decimal, ta.total_kb_all_clerks) AS percent_total_kb,
        -- There are many memory clerks. We'll chart any that make up 5% of SQL memory or more; less significant clerks will be lumped into an "Other" bucket
        CASE
            WHEN mc.total_kb / CONVERT(decimal, ta.total_kb_all_clerks) > 0.05 THEN mc.[type]
            ELSE N'Other'
        END AS graph_type
    FROM #memory_clerks AS mc
    -- Use a self-join to calculate the total memory allocated for each time interval
    JOIN 
    (
        SELECT 
            mc_ta.collection_time, 
            SUM (mc_ta.total_kb) AS total_kb_all_clerks
        FROM #memory_clerks AS mc_ta
        GROUP BY mc_ta.collection_time
    ) AS ta ON (mc.collection_time = ta.collection_time)
    ORDER BY collection_time
END;
GO


--
-- snapshots.rpt_sql_process_and_system_memory
--  Returns system and SQL process memory details over a time interval
-- Parameters: 
--    @ServerName - SQL Server instance name
--    @EndTime - End of the user-selected time window (UTC)
--    @WindowSize - Number of minutes in the time window 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_sql_process_and_system_memory', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_sql_process_and_system_memory] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_sql_process_and_system_memory]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_sql_process_and_system_memory] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_sql_process_and_system_memory]
    @ServerName sysname,
    @EndTime datetime = NULL,
    @WindowSize int
AS
BEGIN
    SET NOCOUNT ON;

    -- Divide our time window up into 40 evenly-sized time intervals, and find the last collection_time within each of these intervals
    CREATE TABLE #intervals (
        interval_time_id        int, 
        interval_start_time     datetimeoffset(7),
        interval_end_time       datetimeoffset(7),
        interval_id             int, 
        first_collection_time   datetimeoffset(7), 
        last_collection_time    datetimeoffset(7), 
        first_snapshot_id       int,
        last_snapshot_id        int, 
        source_id               int,
        snapshot_id             int, 
        collection_time         datetimeoffset(7), 
        collection_time_id      int
    )
    -- GUID 49268954-... is Server Activity
    INSERT INTO #intervals
    EXEC [snapshots].[rpt_interval_collection_times] 
        @ServerName, @EndTime, @WindowSize, 'snapshots.sql_process_and_system_memory', '49268954-4FD4-4EB6-AA04-CD59D9BB5714', 40, 0

    -- Get the earliest and latest snapshot_id values that contain data for the selected time interval. 
    -- This will allow a more efficient query plan. 
    DECLARE @start_snapshot_id int;
    DECLARE @end_snapshot_id int;
    SELECT @start_snapshot_id = MIN (first_snapshot_id)
    FROM #intervals
    SELECT @end_snapshot_id = MAX (last_snapshot_id)
    FROM #intervals
    
    -- Get sys.dm_os_process_memory for these intervals
    SELECT 
        coll.interval_time_id, coll.interval_id, 
        CONVERT (datetime, SWITCHOFFSET (CAST (coll.last_collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        CONVERT (datetime, SWITCHOFFSET (CAST (coll.interval_start_time AS datetimeoffset(7)), '+00:00')) AS interval_start_time, 
        CONVERT (datetime, SWITCHOFFSET (CAST (coll.interval_end_time AS datetimeoffset(7)), '+00:00')) AS interval_end_time, 
        coll.last_snapshot_id, 
        AVG (sql_physical_memory_in_use_kb)             AS avg_sql_physical_memory_in_use_kb, 
        MAX (sql_physical_memory_in_use_kb)             AS max_sql_physical_memory_in_use_kb, 
        MIN (sql_physical_memory_in_use_kb)             AS min_sql_physical_memory_in_use_kb, 
        AVG (sql_total_virtual_address_space_kb)        AS avg_sql_total_virtual_address_space_kb, 
        AVG (sql_virtual_address_space_reserved_kb)     AS avg_sql_virtual_address_space_reserved_kb, 
        AVG (sql_virtual_address_space_committed_kb)    AS avg_sql_virtual_address_space_committed_kb, 
        AVG (sql_virtual_address_space_available_kb)    AS avg_sql_virtual_address_space_available_kb, 
        MAX (sql_virtual_address_space_available_kb)    AS max_sql_virtual_address_space_available_kb, 
        MIN (sql_virtual_address_space_available_kb)    AS min_sql_virtual_address_space_available_kb, 
        AVG (sql_memory_utilization_percentage)         AS avg_sql_memory_utilization_percentage, 
        MIN (sql_memory_utilization_percentage)         AS min_sql_memory_utilization_percentage, 
        AVG (sql_available_commit_limit_kb)             AS avg_sql_available_commit_limit_kb, 
        MIN (sql_available_commit_limit_kb)             AS min_sql_available_commit_limit_kb, 
        AVG (sql_large_page_allocations_kb)             AS avg_sql_large_page_allocations_kb, 
        AVG (sql_locked_page_allocations_kb)            AS avg_sql_locked_page_allocations_kb, 
        SUM (CAST (sql_process_physical_memory_low AS int)) AS sql_process_physical_memory_low_count, 
        SUM (CAST (sql_process_virtual_memory_low AS int))  AS sql_process_virtual_memory_low_count, 
        MAX (sql_page_fault_count) - MIN (sql_page_fault_count) AS interval_sql_page_fault_count, 
        AVG (system_total_physical_memory_kb)           AS system_total_physical_memory_kb, 
        AVG (system_available_physical_memory_kb)       AS avg_system_available_physical_memory_kb, 
        MAX (system_available_physical_memory_kb)       AS max_system_available_physical_memory_kb, 
        MIN (system_available_physical_memory_kb)       AS min_system_available_physical_memory_kb, 
        AVG (system_total_page_file_kb)                 AS avg_system_total_page_file_kb, 
        AVG (system_available_page_file_kb)             AS avg_system_available_page_file_kb, 
        MIN (system_available_page_file_kb)             AS min_system_available_page_file_kb, 
        AVG (system_cache_kb)                           AS avg_system_cache_kb, 
        AVG (system_kernel_paged_pool_kb)               AS avg_system_kernel_paged_pool_kb, 
        AVG (system_kernel_nonpaged_pool_kb)            AS avg_system_kernel_nonpaged_pool_kb, 
        SUM (CAST (system_high_memory_signal_state AS int)) AS system_high_memory_signal_state_count, 
        SUM (CAST (system_low_memory_signal_state AS int))  AS system_low_memory_signal_state_count, 
        AVG (bpool_commit_target)                       AS avg_bpool_commit_target, 
        MAX (bpool_commit_target)                       AS max_bpool_commit_target,  
        MIN (bpool_commit_target)                       AS min_bpool_commit_target, 
        AVG (bpool_committed)                           AS avg_bpool_committed, 
        MAX (bpool_committed)                           AS max_bpool_committed,  
        MIN (bpool_committed)                           AS min_bpool_committed, 
        AVG (bpool_visible)                             AS avg_bpool_visible, 
        MAX (bpool_visible)                             AS max_bpool_visible,  
        MIN (bpool_visible)                             AS min_bpool_visible 
    FROM snapshots.sql_process_and_system_memory AS pm
    INNER JOIN #intervals AS coll ON coll.last_snapshot_id = pm.snapshot_id AND coll.last_collection_time = pm.collection_time     
    GROUP BY 
        coll.interval_start_time, coll.interval_end_time, coll.interval_time_id, coll.last_collection_time, coll.interval_id, coll.last_snapshot_id
    ORDER BY coll.last_collection_time ASC;
END
GO


--
-- snapshots.rpt_sampled_waits
--  Returns a list of the apps and queries that spent the most time waiting for the specified wait category. 
--  Note that this is based off data collected from regular samples of sys.dm_exec_requests, 
--  sys.dm_os_waiting_tasks, and related DMVs.  Because these are just samples, it is not expected that 
--  all waits will be captured; at best, a representative sampling will be returned.  
-- 
-- Parameters: 
--    @ServerName - SQL Server instance name
--    @EndTime - end of the time window (UTC)
--    @WindowSize - number of minutes in the time window
--    @CategoryName - Optional filter criteria: Name of wait category
--    @WaitType - Optional filter criteria: Name of wait type
--    @ProgramName - Optional filter criteria: Application name
--    @SqlHandleStr - Optional filter criteria: Handle to a particular query
--    @StatementStartOffset - Start offset for a particular statement within the batch/proc specified by @SqlHandleStr
--    @StatementEndOffset - End offset for a particular statement within the batch/proc specified by @SqlHandleStr
--    @SessionID - Optional filter criteria: Specific SPID
--    @Database - Optional filter criteria: Waits within a particular database
--
IF (NOT OBJECT_ID(N'snapshots.rpt_sampled_waits', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_sampled_waits] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_sampled_waits]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_sampled_waits] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_sampled_waits]
    @ServerName sysname, 
    @EndTime datetime, 
    @WindowSize int, 
    @CategoryName nvarchar(20) = NULL, 
    @WaitType nvarchar(45) = NULL, 
    @ProgramName nvarchar(50) = NULL, 
    @SqlHandleStr varchar(130) = NULL, 
    @StatementStartOffset int = NULL, 
    @StatementEndOffset int = NULL, 
    @SessionID int = NULL, 
    @Database nvarchar(255) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @start_time_internal datetimeoffset(7);
    DECLARE @end_time_internal datetimeoffset(7);

    -- Clean string params (on drillthrough, RS may pass in an empty string instead of NULL)
    IF @CategoryName = '' SET @CategoryName = NULL
    IF @WaitType = '' SET @WaitType = NULL
    IF @ProgramName = '' SET @ProgramName = NULL
    IF @SqlHandleStr = '' SET @SqlHandleStr = NULL
    IF @Database = '' SET @Database = NULL
    -- -1 is a potentially valid offset, but anything less is invalid. RS can't represent NULL in some places, so 
    -- translate int values that are out of range to NULL. 
    IF @StatementStartOffset < -1 SET @StatementStartOffset = NULL
    IF @StatementEndOffset < -1 SET @StatementStartOffset = NULL
    IF @SessionID < -1 SET @SessionID = NULL

    -- NOTE: The logic below is duplicated in snapshots.rpt_sampled_waits_longest.  It cannot be moved to a shared 
    -- child proc because of SQL restrictions (no nested INSERT EXECs, and table params are read only).  
    -- Also update snapshots.rpt_sampled_waits_longest if a change to this section is required. 

    /*** BEGIN DUPLICATED CODE SECTION ***/
        -- Start time should be passed in as a UTC datetime
        IF (@EndTime IS NOT NULL)
        BEGIN
            -- Assumed time zone offset for this conversion is +00:00 (datetime must be passed in as UTC)
            SET @end_time_internal = CAST (@EndTime AS datetimeoffset(7));
        END
        ELSE BEGIN
            SELECT @end_time_internal = MAX(ar.collection_time)
            FROM core.snapshots AS s
            INNER JOIN snapshots.active_sessions_and_requests AS ar ON s.snapshot_id = ar.snapshot_id
            WHERE s.instance_name = @ServerName -- AND collection_set_uid = '49268954-4FD4-4EB6-AA04-CD59D9BB5714' -- Server Activity CS
        END
        SET @start_time_internal = DATEADD (minute, -1 * @WindowSize, @end_time_internal);

        DECLARE @sql_handle varbinary(64)
        IF LEN (@SqlHandleStr) > 0
        BEGIN
            SET @sql_handle = snapshots.fn_hexstrtovarbin (@SqlHandleStr)
        END
        
        -- Divide our time window up into 40 evenly-sized time intervals, and find the first and last collection_time within each of these intervals
        CREATE TABLE #intervals (
            interval_time_id        int, 
            interval_start_time     datetimeoffset(7),
            interval_end_time       datetimeoffset(7),
            interval_id             int, 
            first_collection_time   datetimeoffset(7), 
            last_collection_time    datetimeoffset(7), 
            first_snapshot_id       int,
            last_snapshot_id        int, 
            source_id               int, 
            snapshot_id             int, 
            collection_time         datetimeoffset(7), 
            collection_time_id      int
        )
        INSERT INTO #intervals
        EXEC [snapshots].[rpt_interval_collection_times] 
            @ServerName, @EndTime, @WindowSize, 'snapshots.active_sessions_and_requests', '2dc02bd6-e230-4c05-8516-4e8c0ef21f95', 40, 1

        SELECT 
            ti.interval_id, ti.interval_time_id, ti.interval_start_time, ti.interval_end_time, 
            ti.collection_time, ti.collection_time_id, r.row_id, 
            r.snapshot_id, r.session_id, r.request_id, r.exec_context_id, r.wait_duration_ms, r.wait_resource, 
            r.login_time, r.program_name, r.sql_handle, r.statement_start_offset, r.statement_end_offset, r.plan_handle, 
            r.database_name, r.task_state, ti.source_id, wt.ignore, 
            -- Model CPU as a "wait type" in the sampling results.  Any active request without a wait type is assumed to be CPU-bound. 
            CASE -- same expression here as in the GROUP BY
                WHEN r.wait_type = '' AND r.task_state IS NOT NULL THEN 'CPU'
                ELSE wt.category_name 
            END AS category_name, 
            -- "Running" tasks are actively using the CPU.  A "runnable" task is able to run, but is momentarily waiting for the active 
            -- task to yield so the next runnable task can get scheduled. Map runnable to the SOS_SCHEDULER_YIELD wait type.
            CASE 
                WHEN r.wait_type = '' AND r.task_state = 'RUNNING' THEN 'CPU (Consumed)'
                WHEN r.wait_type = '' AND r.task_state != 'RUNNING' THEN 'SOS_SCHEDULER_YIELD'
                ELSE r.wait_type
            END AS wait_type
        INTO #waiting_tasks
        FROM snapshots.active_sessions_and_requests AS r
        LEFT OUTER JOIN core.wait_types_categorized AS wt ON wt.wait_type = r.wait_type 
        INNER JOIN #intervals AS ti ON r.collection_time = ti.collection_time AND r.snapshot_id = ti.snapshot_id 
        WHERE r.collection_time BETWEEN @start_time_internal AND @end_time_internal 
            AND r.command != 'AWAITING COMMAND' AND r.request_id != -1 -- exclude idle spids (e.g. head blockers)
            AND (r.program_name = @ProgramName OR @ProgramName IS NULL)
            AND (r.sql_handle = @sql_handle OR @SqlHandleStr IS NULL)
            AND (r.database_name = @Database OR @Database IS NULL)
            AND (r.statement_start_offset = @StatementStartOffset OR @SqlHandleStr IS NULL)
            AND (r.statement_end_offset = @StatementEndOffset OR @SqlHandleStr IS NULL)
            AND (r.session_id = @SessionID OR @SessionID IS NULL)
            AND 
            ( -- ... and wait category either matches the user-specified parameter ...
                (@CategoryName = 
                    CASE -- same expression here as in the select column list
                        WHEN r.wait_type = '' AND r.task_state IS NOT NULL THEN 'CPU'
                        ELSE wt.category_name 
                    END
                ) 
                -- ... or a filter parameter for wait category was not provided (return all categories that aren't marked as ignorable). 
                OR (@CategoryName IS NULL AND ISNULL (wt.ignore, 0) = 0)
            )
            AND 
            ( -- ... and wait type either matches the user-specified parameter ...
                (@WaitType = 
                    CASE 
                        WHEN r.wait_type = '' AND r.task_state = 'RUNNING' THEN 'CPU (Consumed)'
                        WHEN r.wait_type = '' AND r.task_state != 'RUNNING' THEN 'SOS_SCHEDULER_YIELD'
                        ELSE r.wait_type
                    END 
                )
                -- ... or a filter parameter for wait category was not provided 
                OR @WaitType IS NULL
            )            
        -- Force a recompile of this statement to take into account the actual values of @start_time_internal and 
        -- @end_time_internal, which were not available at the time of proc compilation. 
        OPTION (RECOMPILE);
        
    /*** END DUPLICATED CODE SECTION ***/
    
    -- Get query text (do this here instead of in the following query so that we don't waste time retrieving the 
    -- same query's text more than once). 
    SELECT 
        r.sql_handle, r.statement_start_offset, r.statement_end_offset, 
        REPLACE (REPLACE (REPLACE (REPLACE (REPLACE (REPLACE (
            LEFT (LTRIM (qt.query_text), 100)
            , CHAR(9), ' '), CHAR(10), ' '), CHAR(13), ' '), '   ', ' '), '  ', ' '), '  ', ' ') AS flat_query_text
    INTO #queries
    FROM 
    (
        SELECT DISTINCT source_id, sql_handle, statement_start_offset, statement_end_offset
        FROM #waiting_tasks
        WHERE category_name IS NOT NULL 
    ) AS r
    OUTER APPLY snapshots.fn_get_query_text(r.source_id, r.sql_handle, r.statement_start_offset, r.statement_end_offset) AS qt

    -- Within each time interval, group the wait counts by waittype, app, db, and query. 
    SELECT 
        CONVERT (datetime, SWITCHOFFSET (r.interval_start_time, '+00:00')) AS interval_start_time, 
        CONVERT (datetime, SWITCHOFFSET (r.interval_end_time, '+00:00')) AS interval_end_time, 
        r.interval_id, r.interval_time_id, 
        r.source_id, r.category_name, r.wait_type, r.[program_name], r.database_name, 
        COUNT (*) AS wait_count, r.sql_handle, r.statement_start_offset, r.statement_end_offset,
        master.dbo.fn_varbintohexstr (r.sql_handle) AS sql_handle_str, 
        q.flat_query_text
    FROM #waiting_tasks AS r 
    LEFT OUTER JOIN #queries AS q ON r.sql_handle = q.sql_handle AND r.statement_start_offset = q.statement_start_offset 
        AND r.statement_end_offset = q.statement_end_offset 
    WHERE r.category_name IS NOT NULL 
    GROUP BY r.interval_start_time, r.interval_end_time, r.interval_id, r.interval_time_id,
        r.category_name, r.wait_type, r.[program_name], r.database_name, r.[sql_handle], 
        r.source_id, r.statement_start_offset, r.statement_end_offset, q.flat_query_text
    ORDER BY r.category_name, r.interval_id, COUNT(*) DESC

END 
GO


--
-- snapshots.rpt_sampled_waits_longest
--  Returns list of the N longest waits that meet user-specified filter criteria.  
--  Note that this is based off data collected from regular samples of sys.dm_exec_requests, 
--  sys.dm_os_waiting_tasks, and related DMVs.  Because these are just samples, it is not expected that 
--  all waits will be captured; at best, a representative sampling will be returned.  
-- 
-- Parameters: 
--    @ServerName - SQL Server instance name
--    @EndTime - end of the time window (UTC)
--    @WindowSize - number of minutes in the time window
--    @CategoryName - Optional filter criteria: Name of wait category
--    @WaitType - Optional filter criteria: Name of wait type
--    @ProgramName - Optional filter criteria: Application name
--    @SqlHandleStr - Optional filter criteria: Name of the query to filter on
--    @StatementStartOffset - Start offset for a particular statement within the batch/proc specified by @SqlHandleStr
--    @StatementEndOffset - End offset for a particular statement within the batch/proc specified by @SqlHandleStr
--    @SessionID - Optional filter criteria: Specific SPID
--    @Database - Optional filter criteria: Waits within a particular database
--
IF (NOT OBJECT_ID(N'snapshots.rpt_sampled_waits_longest', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_sampled_waits_longest] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_sampled_waits_longest]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_sampled_waits_longest] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_sampled_waits_longest]
    @ServerName sysname, 
    @EndTime datetime, 
    @WindowSize int, 
    @CategoryName nvarchar(20) = NULL, 
    @WaitType nvarchar(45) = NULL, 
    @ProgramName nvarchar(50) = NULL, 
    @SqlHandleStr varchar(130) = NULL, 
    @StatementStartOffset int = NULL, 
    @StatementEndOffset int = NULL, 
    @SessionID int = NULL, 
    @Database nvarchar(255) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @start_time_internal datetimeoffset(7);
    DECLARE @end_time_internal datetimeoffset(7);

    -- Clean string params (on drillthrough, RS may pass in an empty string instead of NULL)
    IF @CategoryName = '' SET @CategoryName = NULL
    IF @WaitType = '' SET @WaitType = NULL
    IF @ProgramName = '' SET @ProgramName = NULL
    IF @SqlHandleStr = '' SET @SqlHandleStr = NULL
    IF @Database = '' SET @Database = NULL
    -- -1 is a potentially valid offset, but anything less is invalid. RS can't represent NULL in some places, so 
    -- translate int values that are out of range to NULL. 
    IF @StatementStartOffset < -1 SET @StatementStartOffset = NULL
    IF @StatementEndOffset < -1 SET @StatementStartOffset = NULL
    IF @SessionID < -1 SET @SessionID = NULL

    -- NOTE: The logic below is duplicated in snapshots.rpt_sampled_waits.  It cannot be moved to a shared 
    -- child proc because of SQL restrictions (no nested INSERT EXECs, and table params are read only).  
    -- Also update snapshots.rpt_sampled_waits if a change to this section is required. 

    /*** BEGIN DUPLICATED CODE SECTION ***/
        -- Start time should be passed in as a UTC datetime
        IF (@EndTime IS NOT NULL)
        BEGIN
            -- Assumed time zone offset for this conversion is +00:00 (datetime must be passed in as UTC)
            SET @end_time_internal = CAST (@EndTime AS datetimeoffset(7));
        END
        ELSE BEGIN
            SELECT @end_time_internal = MAX(ar.collection_time)
            FROM core.snapshots AS s
            INNER JOIN snapshots.active_sessions_and_requests AS ar ON s.snapshot_id = ar.snapshot_id
            WHERE s.instance_name = @ServerName -- AND collection_set_uid = '49268954-4FD4-4EB6-AA04-CD59D9BB5714' -- Server Activity CS
        END
        SET @start_time_internal = DATEADD (minute, -1 * @WindowSize, @end_time_internal);

        DECLARE @sql_handle varbinary(64)
        IF LEN (@SqlHandleStr) > 0
        BEGIN
            SET @sql_handle = snapshots.fn_hexstrtovarbin (@SqlHandleStr)
        END
        
        -- Divide our time window up into 40 evenly-sized time intervals, and find the first and last collection_time within each of these intervals
        CREATE TABLE #intervals (
            interval_time_id        int, 
            interval_start_time     datetimeoffset(7),
            interval_end_time       datetimeoffset(7),
            interval_id             int, 
            first_collection_time   datetimeoffset(7), 
            last_collection_time    datetimeoffset(7), 
            first_snapshot_id       int,
            last_snapshot_id        int, 
            source_id               int, 
            snapshot_id             int, 
            collection_time         datetimeoffset(7), 
            collection_time_id      int
        )
        INSERT INTO #intervals
        EXEC [snapshots].[rpt_interval_collection_times] 
            @ServerName, @EndTime, @WindowSize, 'snapshots.active_sessions_and_requests', '2dc02bd6-e230-4c05-8516-4e8c0ef21f95', 40, 1

        SELECT 
            ti.interval_id, ti.interval_time_id, ti.interval_start_time, ti.interval_end_time, 
            ti.collection_time, ti.collection_time_id, r.row_id, 
            r.snapshot_id, r.session_id, r.request_id, r.exec_context_id, r.wait_duration_ms, r.wait_resource, 
            r.login_time, r.program_name, r.sql_handle, r.statement_start_offset, r.statement_end_offset, r.plan_handle, 
            r.database_name, r.task_state, ti.source_id, wt.ignore, 
            -- Model CPU as a "wait type" in the sampling results.  Any active request without a wait type is assumed to be CPU-bound. 
            CASE -- same expression here as in the GROUP BY
                WHEN r.wait_type = '' AND r.task_state IS NOT NULL THEN 'CPU'
                ELSE wt.category_name 
            END AS category_name, 
            -- "Running" tasks are actively using the CPU.  A "runnable" task is able to run, but is momentarily waiting for the active 
            -- task to yield so the next runnable task can get scheduled. Map runnable to the SOS_SCHEDULER_YIELD wait type.
            CASE 
                WHEN r.wait_type = '' AND r.task_state = 'RUNNING' THEN 'CPU (Consumed)'
                WHEN r.wait_type = '' AND r.task_state != 'RUNNING' THEN 'SOS_SCHEDULER_YIELD'
                ELSE r.wait_type
            END AS wait_type
        INTO #waiting_tasks
        FROM snapshots.active_sessions_and_requests AS r
        LEFT OUTER JOIN core.wait_types_categorized AS wt ON wt.wait_type = r.wait_type 
        INNER JOIN #intervals AS ti ON r.collection_time = ti.collection_time AND r.snapshot_id = ti.snapshot_id 
        WHERE r.collection_time BETWEEN @start_time_internal AND @end_time_internal 
            AND r.command != 'AWAITING COMMAND' AND r.request_id != -1 -- exclude idle spids (e.g. head blockers)
            AND (r.program_name = @ProgramName OR @ProgramName IS NULL)
            AND (r.sql_handle = @sql_handle OR @SqlHandleStr IS NULL)
            AND (r.database_name = @Database OR @Database IS NULL)
            AND (r.statement_start_offset = @StatementStartOffset OR @SqlHandleStr IS NULL)
            AND (r.statement_end_offset = @StatementEndOffset OR @SqlHandleStr IS NULL)
            AND (r.session_id = @SessionID OR @SessionID IS NULL)
            AND 
            ( -- ... and wait category either matches the user-specified parameter ...
                (@CategoryName = 
                    CASE -- same expression here as in the select column list
                        WHEN r.wait_type = '' AND r.task_state IS NOT NULL THEN 'CPU'
                        ELSE wt.category_name 
                    END
                ) 
                -- ... or a filter parameter for wait category was not provided (return all categories that aren't marked as ignorable). 
                OR (@CategoryName IS NULL AND ISNULL (wt.ignore, 0) = 0)
            )
            AND 
            ( -- ... and wait type either matches the user-specified parameter ...
                (@WaitType = 
                    CASE 
                        WHEN r.wait_type = '' AND r.task_state = 'RUNNING' THEN 'CPU (Consumed)'
                        WHEN r.wait_type = '' AND r.task_state != 'RUNNING' THEN 'SOS_SCHEDULER_YIELD'
                        ELSE r.wait_type
                    END 
                )
                -- ... or a filter parameter for wait category was not provided 
                OR @WaitType IS NULL
            )
        -- Force a recompile of this statement to take into account the actual values of @start_time_internal and 
        -- @end_time_internal, which were not available at the time of proc compilation. 
        OPTION (RECOMPILE);
        
    /*** END DUPLICATED CODE SECTION ***/

    CREATE INDEX idx1 ON #waiting_tasks (collection_time_id, session_id)

    -- The same long-lived wait may be captured multiple times.  For a given wait, we only care about the max 
    -- wait time within the target time window. If we see that the immediately following sample shows the same 
    -- spid waiting with a wait time that spans both samples, we discard the preceding sample and keep only 
    -- the max wait time.  For example, in the data below, spid 54 was stuck in the same long-lived lock wait 
    -- from row 1 through row 3.  From this data, we would only keep rows #3 and #4. Rows #1 and #2 are 
    -- discarded to avoid reporting the same long wait several times. 
    -- 
    --     (row#)   collection_time     session_id  wait_type    wait_duration_ms  
    --              ------------------- ----------- ------------ -----------------
    --       1      2007-10-25 13:01:00          53 LCK_M_S                   8141
    --       2      2007-10-25 13:01:10          53 LCK_M_S                  18278
    --       3      2007-10-25 13:01:20          53 LCK_M_S                  28318
    --       4      2007-10-25 13:01:30          54 LCK_M_X                    755
    --
    -- Also, if there was one collection time where everyone was blocked momentarily, we want to avoid 
    -- reporting that collection time over and over w/only the spid # varying; that's not the most interesting 
    -- sampling.  Instead, report the longest wait in each of the top 10 collection times (top 10 times by max 
    -- wait duration at the collection time).  
    SELECT TOP 10
        CONVERT (datetime, SWITCHOFFSET (r1.collection_time, '+00:00')) AS collection_time, 
        CONVERT (varchar(30), CONVERT (datetime, SWITCHOFFSET (r1.collection_time, '+00:00')), 126) AS collection_time_str, 
        r1.snapshot_id, r1.row_id, 
        r1.session_id, r1.request_id, r1.exec_context_id, r1.wait_resource, 
        r1.source_id, r1.category_name, r1.wait_type, r1.wait_duration_ms, 
        r1.database_name, r1.[program_name], r1.[sql_handle], r1.statement_start_offset, r1.statement_end_offset, r1.plan_handle
    FROM #waiting_tasks AS r1
    -- Find the same spid in the next collection time
    LEFT OUTER JOIN #waiting_tasks AS r2 
        ON r2.collection_time_id = r1.collection_time_id + 1 AND r2.session_id = r1.session_id 
            AND r2.request_id = r1.request_id AND r2.exec_context_id = r1.exec_context_id AND r2.login_time = r1.login_time 
    WHERE 
        -- Prevent reporting the same wait spanning multiple collection times. 
        (   
            -- ... where the spid wasn't seen waiting at the next collection time
            r2.session_id IS NULL 
            -- ... or the wait at the next collection time is shorter than it would have been if the wait had spanned both collection times 
            OR r2.wait_duration_ms < (DATEDIFF (second, r1.collection_time, r2.collection_time) * 1000)
        )
        -- Exclude all but the longest wait in any given collection time
        AND NOT EXISTS 
        (
            SELECT * FROM #waiting_tasks AS r3
            WHERE r3.collection_time_id = r1.collection_time_id 
                AND r3.wait_duration_ms > r1.wait_duration_ms 
                OR (r3.wait_duration_ms = r1.wait_duration_ms AND r3.row_id > r1.row_id)
        )
    ORDER BY r1.wait_duration_ms DESC
   
END
GO


--
-- snapshots.rpt_sampled_waits_hottest_resources
--  Returns list of the N longest waits that meet user-specified filter criteria.  
--  Note that this is based off data collected from regular samples of sys.dm_exec_requests, 
--  sys.dm_os_waiting_tasks, and related DMVs.  Because these are just samples, it is not expected that 
--  all waits will be captured; at best, a representative sampling will be returned.  
-- 
-- Parameters: 
--    @ServerName - SQL Server instance name
--    @EndTime - end of the time window (UTC)
--    @WindowSize - number of minutes in the time window
--    @CategoryName - Optional filter criteria: Name of wait category
--    @WaitType - Optional filter criteria: Name of wait type
--    @ProgramName - Optional filter criteria: Application name
--    @SqlHandleStr - Optional filter criteria: Name of the query to filter on
--    @StatementStartOffset - Start offset for a particular statement within the batch/proc specified by @SqlHandleStr
--    @StatementEndOffset - End offset for a particular statement within the batch/proc specified by @SqlHandleStr
--    @SessionID - Option filter criteria: Specific SPID
--    @Database - Optional filter criteria: Waits within a particular database
--
IF (NOT OBJECT_ID(N'snapshots.rpt_sampled_waits_hottest_resources', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_sampled_waits_hottest_resources] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_sampled_waits_hottest_resources]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_sampled_waits_hottest_resources] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_sampled_waits_hottest_resources]
    @ServerName sysname, 
    @EndTime datetime, 
    @WindowSize int, 
    @CategoryName nvarchar(20) = NULL, 
    @WaitType nvarchar(45) = NULL, 
    @ProgramName nvarchar(50) = NULL, 
    @SqlHandleStr varchar(130) = NULL, 
    @StatementStartOffset int = NULL, 
    @StatementEndOffset int = NULL, 
    @SessionID int = NULL, 
    @Database nvarchar(255) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @start_time_internal datetimeoffset(7);
    DECLARE @end_time_internal datetimeoffset(7);

    -- Clean string params (on drillthrough, RS may pass in an empty string instead of NULL)
    IF @CategoryName = '' SET @CategoryName = NULL
    IF @WaitType = '' SET @WaitType = NULL
    IF @ProgramName = '' SET @ProgramName = NULL
    IF @SqlHandleStr = '' SET @SqlHandleStr = NULL
    IF @Database = '' SET @Database = NULL
    -- -1 is a potentially valid offset, but anything less is invalid. RS can't represent NULL in some places, so 
    -- translate int values that are out of range to NULL. 
    IF @StatementStartOffset < -1 SET @StatementStartOffset = NULL
    IF @StatementEndOffset < -1 SET @StatementStartOffset = NULL
    IF @SessionID < -1 SET @SessionID = NULL

    -- Start time should be passed in as a UTC datetime
    IF (@EndTime IS NOT NULL)
    BEGIN
        -- Assumed time zone offset for this conversion is +00:00 (datetime must be passed in as UTC)
        SET @end_time_internal = CAST (@EndTime AS datetimeoffset(7));
    END
    ELSE BEGIN
        SELECT @end_time_internal = MAX(ar.collection_time)
        FROM core.snapshots AS s
        INNER JOIN snapshots.active_sessions_and_requests AS ar ON s.snapshot_id = ar.snapshot_id
        WHERE s.instance_name = @ServerName -- AND collection_set_uid = '49268954-4FD4-4EB6-AA04-CD59D9BB5714' -- Server Activity CS
    END
    SET @start_time_internal = DATEADD (minute, -1 * @WindowSize, @end_time_internal);

    DECLARE @sql_handle varbinary(64)
    IF LEN (@SqlHandleStr) > 0
    BEGIN
        SET @sql_handle = snapshots.fn_hexstrtovarbin (@SqlHandleStr)
    END
    
    SELECT TOP 10 
        wt.category_name, r.wait_type, 
        CASE 
            WHEN LEN (ISNULL (r.wait_resource, '')) = 0 THEN 
                CASE 
                    WHEN LEN (r.resource_description) > 30 THEN LEFT (r.resource_description, 27) + '...'
                    ELSE LEFT (r.resource_description, 30)
                END
            ELSE r.wait_resource
        END AS wait_resource, 
        r.resource_description, 
        CONVERT (datetime, SWITCHOFFSET (MAX (r.collection_time), '+00:00')) AS example_collection_time, 
        CONVERT (varchar(30), CONVERT (datetime, SWITCHOFFSET (MAX (r.collection_time), '+00:00')), 126) AS example_collection_time_str, 
        COUNT(*) AS wait_count
    FROM snapshots.active_sessions_and_requests AS r 
    LEFT OUTER JOIN core.wait_types_categorized AS wt ON wt.wait_type = r.wait_type 
    WHERE r.collection_time BETWEEN @start_time_internal AND @end_time_internal 
        AND r.command != 'AWAITING COMMAND' AND r.request_id != -1 -- exclude idle spids (e.g. head blockers)
        AND (r.program_name = @ProgramName OR @ProgramName IS NULL)
        AND (r.sql_handle = @sql_handle OR @SqlHandleStr IS NULL)
        AND (r.database_name = @Database OR @Database IS NULL)
        AND (r.statement_start_offset = @StatementStartOffset OR @SqlHandleStr IS NULL)
        AND (r.statement_end_offset = @StatementEndOffset OR @SqlHandleStr IS NULL)
        AND (r.session_id = @SessionID OR @SessionID IS NULL)
        AND 
        ( -- ... and wait category either matches the user-specified parameter ...
            (@CategoryName = 
                CASE -- same expression here as in the select column list
                    WHEN r.wait_type = '' AND r.task_state IS NOT NULL THEN 'CPU'
                    ELSE wt.category_name 
                END
            ) 
            -- ... or a filter parameter for wait category was not provided (return all categories that aren't marked as ignorable). 
            OR (@CategoryName IS NULL AND ISNULL (wt.ignore, 0) = 0)
        )
        AND 
        ( -- ... and wait type either matches the user-specified parameter ...
            (@WaitType = 
                CASE 
                    WHEN r.wait_type = '' AND r.task_state = 'RUNNING' THEN 'CPU (Consumed)'
                    WHEN r.wait_type = '' AND r.task_state != 'RUNNING' THEN 'SOS_SCHEDULER_YIELD'
                    ELSE r.wait_type
                END 
            )
            -- ... or a filter parameter for wait category was not provided 
            OR @WaitType IS NULL
        )
        -- Exclude rows where there is no named resource
        AND (ISNULL (r.resource_description, '') != '' OR ISNULL (r.wait_resource, '') != '')
    GROUP BY wt.category_name, r.wait_type, r.wait_resource, r.resource_description
    ORDER BY COUNT(*) DESC
    -- Force a recompile of this statement to take into account the actual values of @start_time_internal and 
    -- @end_time_internal, which were not available at the time of proc compilation. 
    OPTION (RECOMPILE);
   
END
GO


--
-- snapshots.rpt_io_virtual_file_stats
--  Returns wait time per wait type over a time interval
-- Parameters: 
--    @ServerName - SQL Server instance name
--    @EndTime - End of the user-selected time window (UTC)
--    @WindowSize - Number of minutes in the time window 
--    @CategoryName - (Optional) Name of wait category to filter on (all categories if NULL)
--    @WaitType - (Optional) Name of wait type to filter on (all wait types if NULL)
--
IF (NOT OBJECT_ID(N'snapshots.rpt_io_virtual_file_stats', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_io_virtual_file_stats] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_io_virtual_file_stats]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_io_virtual_file_stats] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_io_virtual_file_stats]
    @ServerName sysname,
    @EndTime datetime = NULL,
    @WindowSize int,
    @LogicalDisk nvarchar(255) = NULL, 
    @Database nvarchar(255) = NULL 
AS
BEGIN
    SET NOCOUNT ON;

    -- Clean string params (on drillthrough, RS may pass in empty string instead of NULL)
    IF @LogicalDisk = '' SET @LogicalDisk = NULL
    IF @Database = '' SET @Database = NULL

    -- Divide our time window up into 40 evenly-sized time intervals, and find the last collection_time within each of these intervals
    CREATE TABLE #intervals (
        interval_time_id        int, 
        interval_start_time     datetimeoffset(7),
        interval_end_time       datetimeoffset(7),
        interval_id             int, 
        first_collection_time   datetimeoffset(7), 
        last_collection_time    datetimeoffset(7), 
        first_snapshot_id       int,
        last_snapshot_id        int, 
        source_id               int,
        snapshot_id             int, 
        collection_time         datetimeoffset(7), 
        collection_time_id      int
    )
    -- GUID 49268954-... is Server Activity
    INSERT INTO #intervals
    EXEC [snapshots].[rpt_interval_collection_times] 
        @ServerName, @EndTime, @WindowSize, 'snapshots.io_virtual_file_stats', '49268954-4FD4-4EB6-AA04-CD59D9BB5714', 40, 0

    -- Get the earliest and latest snapshot_id values that contain data for the selected time interval. 
    -- This will allow a more efficient query plan. 
    DECLARE @start_snapshot_id int;
    DECLARE @end_snapshot_id int;
    SELECT @start_snapshot_id = MIN (first_snapshot_id)
    FROM #intervals
    SELECT @end_snapshot_id = MAX (last_snapshot_id)
    FROM #intervals
    
    -- Get the file stats for these collection times
    SELECT 
        coll.interval_id, coll.interval_time_id, coll.interval_start_time, coll.interval_end_time, 
        coll.first_collection_time, coll.last_collection_time, coll.first_snapshot_id, coll.last_snapshot_id, 
        fs.*
    INTO #file_stats
    FROM snapshots.io_virtual_file_stats AS fs
    INNER JOIN #intervals AS coll ON coll.last_snapshot_id = fs.snapshot_id AND coll.last_collection_time = fs.collection_time 
    WHERE 
        fs.logical_disk = ISNULL (@LogicalDisk, fs.logical_disk)
        AND fs.database_name = ISNULL (@Database, fs.database_name)
    
    -- Get file stats deltas for each interval.  
    SELECT 
        t.*, 
        /**** Combined reads + write values ****/
        t.num_of_reads_delta + t.num_of_writes_delta                    AS num_of_transfers_delta, 
        t.num_of_reads_cumulative + t.num_of_writes_cumulative          AS num_of_transfers_cumulative,  
        t.num_of_mb_read_delta + t.num_of_mb_written_delta              AS num_of_mb_transferred_delta, 
        t.num_of_mb_read_cumulative + t.num_of_mb_written_cumulative    AS num_of_mb_transferred_cumulative, 
        /**** Calc "Disk sec/Transfer" type values ***/
        t.io_stall_read_ms_delta + t.io_stall_write_ms_delta            AS io_stall_ms_delta, 
        t.io_stall_read_ms_cumulative + t.io_stall_write_ms_cumulative  AS io_stall_ms_cumulative, 
        CASE 
            WHEN t.num_of_reads_delta = 0 THEN 0 
            ELSE t.io_stall_read_ms_delta / t.num_of_reads_delta
        END                                                             AS io_stall_ms_per_read_delta, 
        CASE 
            WHEN t.num_of_reads_cumulative = 0 THEN 0 
            ELSE t.io_stall_read_ms_cumulative / t.num_of_reads_cumulative
        END                                                             AS io_stall_ms_per_read_cumulative, 
        CASE
            WHEN t.num_of_writes_delta = 0 THEN 0
            ELSE t.io_stall_write_ms_delta / t.num_of_writes_delta
        END                                                             AS io_stall_ms_per_write_delta, 
        CASE
            WHEN t.num_of_writes_cumulative = 0 THEN 0
            ELSE t.io_stall_write_ms_cumulative / t.num_of_writes_cumulative
        END                                                             AS io_stall_ms_per_write_cumulative, 
        CASE
            WHEN (t.num_of_reads_delta + t.num_of_writes_delta) = 0 THEN 0
            ELSE (t.io_stall_read_ms_delta + t.io_stall_write_ms_delta) / (t.num_of_reads_delta + t.num_of_writes_delta)
        END                                                             AS io_stall_ms_per_transfer_delta, 
        CASE
            WHEN (t.num_of_reads_cumulative + t.num_of_writes_cumulative) = 0 THEN 0
            ELSE (t.io_stall_read_ms_cumulative + t.io_stall_write_ms_cumulative) / (t.num_of_reads_cumulative + t.num_of_writes_cumulative)
        END                                                             AS io_stall_ms_per_transfer_cumulative 
    FROM
    (
        SELECT 
            fs1.interval_id, fs1.interval_time_id, fs1.first_snapshot_id, fs1.last_snapshot_id, 
            -- Convert all datetimeoffset values to UTC datetime values before returning to Reporting Services
            CONVERT (datetime, SWITCHOFFSET (CAST (fs1.first_collection_time AS datetimeoffset(7)), '+00:00')) AS first_collection_time, 
            CONVERT (datetime, SWITCHOFFSET (CAST (fs2.first_collection_time AS datetimeoffset(7)), '+00:00')) AS last_collection_time, 
            CONVERT (datetime, SWITCHOFFSET (CAST (fs1.interval_start_time AS datetimeoffset(7)), '+00:00')) AS interval_start, 
            CONVERT (datetime, SWITCHOFFSET (CAST (fs2.interval_start_time AS datetimeoffset(7)), '+00:00')) AS interval_end, 
            fs2.database_name, fs2.database_id, fs2.logical_file_name, fs2.[file_id], fs2.type_desc, fs2.logical_disk, 
            -- All file stats will be reset to zero by a service cycle, which will cause 
            -- (snapshot2_io_time-snapshot1_io_time) calculations to produce an incorrect 
            -- negative wait time for the interval.  Detect this and avoid calculating 
            -- negative IO wait time deltas. 
            /***** READS ****/
            CASE 
                WHEN (fs2.num_of_reads - fs1.num_of_reads) < 0 THEN fs2.num_of_reads
                ELSE (fs2.num_of_reads - fs1.num_of_reads)
            END AS num_of_reads_delta,                                              -- num_of_reads_delta
            fs2.num_of_reads AS num_of_reads_cumulative,                            -- num_of_reads_cumulative
            CASE 
                WHEN (fs2.num_of_bytes_read - fs1.num_of_bytes_read) < 0 THEN fs2.num_of_bytes_read 
                ELSE (fs2.num_of_bytes_read - fs1.num_of_bytes_read)
            END / 1024 / 1024 AS num_of_mb_read_delta,                              -- num_of_mb_read_delta
            fs2.num_of_bytes_read /1024/1024 AS num_of_mb_read_cumulative,          -- num_of_mb_read_cumulative
            CASE
                WHEN (fs2.io_stall_read_ms - fs1.io_stall_read_ms) < 0 THEN fs2.io_stall_read_ms
                ELSE (fs2.io_stall_read_ms - fs1.io_stall_read_ms)
            END AS io_stall_read_ms_delta,                                          -- io_stall_read_ms_delta
            fs2.io_stall_read_ms AS io_stall_read_ms_cumulative,                    -- io_stall_read_ms_cumulative
            /**** WRITES ****/
            CASE
                WHEN (fs2.num_of_writes - fs1.num_of_writes) < 0 THEN fs2.num_of_writes
                ELSE (fs2.num_of_writes - fs1.num_of_writes)
            END AS num_of_writes_delta,                                             -- num_of_writes_delta
            fs2.num_of_writes AS num_of_writes_cumulative,                          -- num_of_writes_cumulative
            CASE
                WHEN (fs2.num_of_bytes_written - fs1.num_of_bytes_written) < 0 THEN fs2.num_of_bytes_written
                ELSE (fs2.num_of_bytes_written - fs1.num_of_bytes_written)
            END / 1024 / 1024 AS num_of_mb_written_delta,                           -- num_of_mb_written_delta
            fs2.num_of_bytes_written /1024/1024 AS num_of_mb_written_cumulative,    -- num_of_mb_written_cumulative
            CASE
                WHEN (fs2.io_stall_write_ms - fs1.io_stall_write_ms) < 0 THEN fs2.io_stall_write_ms
                ELSE (fs2.io_stall_write_ms - fs1.io_stall_write_ms)
            END AS io_stall_write_ms_delta,                                         -- io_stall_write_ms_delta
            fs2.io_stall_write_ms AS io_stall_write_ms_cumulative,                  -- io_stall_write_ms_cumulative
            fs1.size_on_disk_bytes / 1024 / 1024 AS size_on_disk_mb_interval_start, -- size_on_disk_mb_interval_start
            fs2.size_on_disk_bytes / 1024 / 1024 AS size_on_disk_mb_interval_end    -- size_on_disk_mb_interval_end
        FROM #file_stats AS fs1
        -- Self-join - fs1 represents IO stats at the beginning of the sample interval, while fs2 
        -- shows file stats at the end of the interval. 
        INNER JOIN #file_stats AS fs2 ON fs1.database_id = fs2.database_id AND fs1.[file_id] = fs2.[file_id] AND fs1.interval_id = fs2.interval_id-1 

    ) AS t
    ORDER BY t.database_name, t.logical_file_name, t.last_collection_time
END
GO


--
-- snapshots.rpt_list_all_servers
--  Shows a list of the servers that have data in this MDW database, plus information on how up-to-date 
--  the data is.  Used in the multi-server overview report. 
-- Parameters: 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_list_all_servers', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_list_all_servers] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_list_all_servers]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_list_all_servers] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_list_all_servers]
AS
BEGIN
    SET NOCOUNT ON;

    SELECT 
        instance_name, 
        query_statistics_last_upload, 
        disk_usage_last_upload, 
        server_activity_last_upload
    FROM 
    (
        SELECT 
            -- Name of the SQL Server instance
            instance_name, 
            -- Name of the collection set.  The names in the syscollector_collection_sets table can be localized. 
            -- We use well-known strings here because we want to defer selection of the appropriate localized 
            -- string to the client. 
            CASE 
                collection_set_uid
                WHEN '2DC02BD6-E230-4C05-8516-4E8C0EF21F95' THEN 'query_statistics_last_upload'
                WHEN '7B191952-8ECF-4E12-AEB2-EF646EF79FEF' THEN 'disk_usage_last_upload'
                WHEN '49268954-4FD4-4EB6-AA04-CD59D9BB5714' THEN 'server_activity_last_upload'
                ELSE NULL -- custom Collection set, not displayed on this report
            END AS top_level_report_name,
            -- Convert datetimeoffset to UTC datetime for RS 2005 compatibility 
            CONVERT (datetime, SWITCHOFFSET (MAX (snapshot_time), '+00:00')) AS latest_snapshot_time 
        FROM core.snapshots 
        -- For this report, only system collection sets matter
        WHERE collection_set_uid IN ('2DC02BD6-E230-4C05-8516-4E8C0EF21F95', '7B191952-8ECF-4E12-AEB2-EF646EF79FEF', '49268954-4FD4-4EB6-AA04-CD59D9BB5714')
        GROUP BY instance_name, collection_set_uid
    ) AS instance_report_list 
    PIVOT 
    (
        MAX (latest_snapshot_time)
        FOR top_level_report_name IN (query_statistics_last_upload, disk_usage_last_upload, server_activity_last_upload)
    ) AS pvt 
    ORDER BY instance_name;

END;
GO




/**********************************************************************/
/* CUSTOM_SNAPSHOTS SCHEMA                                            */
/**********************************************************************/
RAISERROR('', 0, 1)  WITH NOWAIT;
RAISERROR('Create schema custom_snpshots...', 0, 1)  WITH NOWAIT;
GO
IF (SCHEMA_ID('custom_snapshots') IS NULL)
BEGIN
    DECLARE @sql nvarchar(128)
    SET @sql = 'CREATE SCHEMA custom_snapshots'
    EXEC sp_executesql @sql
END
GO




/**********************************************************************/
/* Create MDW security roles                                          */
/**********************************************************************/
RAISERROR('', 0, 1)  WITH NOWAIT;
RAISERROR('Create mdw_admin role...', 0, 1)  WITH NOWAIT;
IF ( NOT EXISTS (SELECT * FROM sys.database_principals 
                    WHERE name = N'mdw_admin' AND type = 'R'))
BEGIN
    CREATE ROLE [mdw_admin]
END
ELSE -- if the role exists check to see if it has members
BEGIN
    IF NOT EXISTS (SELECT rm.member_principal_id
                FROM sys.database_principals dp 
                INNER JOIN sys.database_role_members rm ON rm.role_principal_id = dp.principal_id
                WHERE name = N'mdw_admin' AND type = 'R')
    BEGIN
        -- if the role has no members drop and recreate it
        DROP ROLE [mdw_admin]
        CREATE ROLE [mdw_admin]
    END
END
GO

RAISERROR('Create mdw_writer role...', 0, 1)  WITH NOWAIT;
IF ( NOT EXISTS (SELECT * FROM sys.database_principals 
                    WHERE name = N'mdw_writer' AND type = 'R'))
BEGIN
    CREATE ROLE [mdw_writer]
END
ELSE -- if the role exists check to see if it has members
BEGIN
    IF NOT EXISTS (SELECT rm.member_principal_id
                FROM sys.database_principals dp 
                INNER JOIN sys.database_role_members rm ON rm.role_principal_id = dp.principal_id
                WHERE name = N'mdw_writer' AND type = 'R')
    BEGIN
        -- if the role has no members drop and recreate it
        DROP ROLE [mdw_writer]
        CREATE ROLE [mdw_writer]
    END
END
GO

RAISERROR('Create mdw_reader role...', 0, 1)  WITH NOWAIT;
IF ( NOT EXISTS (SELECT * FROM sys.database_principals 
                    WHERE name = N'mdw_reader' AND type = 'R'))
BEGIN
    CREATE ROLE [mdw_reader]
END
ELSE -- if the role exists check to see if it has members
BEGIN
    IF NOT EXISTS (SELECT rm.member_principal_id
                FROM sys.database_principals dp 
                INNER JOIN sys.database_role_members rm ON rm.role_principal_id = dp.principal_id
                WHERE name = N'mdw_reader' AND type = 'R')
    BEGIN
        -- if the role has no members drop and recreate it
        DROP ROLE [mdw_reader]
        CREATE ROLE [mdw_reader]
    END
END
GO

-- permissions of mdw_writer and mdw_reader are inclusive to mdw_admin
EXECUTE sp_addrolemember @rolename = 'mdw_writer' , 
                   @membername = 'mdw_admin' 
GO

EXECUTE sp_addrolemember @rolename = 'mdw_reader' , 
                   @membername = 'mdw_admin' 
GO

RAISERROR('', 0, 1)  WITH NOWAIT;
RAISERROR('Create loginless user ...', 0, 1)  WITH NOWAIT;
IF (NOT EXISTS(SELECT * FROM sys.database_principals WHERE NAME = 'mdw_check_operator_admin'))
BEGIN
    CREATE USER [mdw_check_operator_admin] WITHOUT LOGIN
END
GO

EXECUTE sp_addrolemember @rolename = 'mdw_admin', @membername = 'mdw_check_operator_admin' 
GO



/**********************************************************************/
/* Setup permissions                                                  */
/**********************************************************************/
RAISERROR('', 0, 1)  WITH NOWAIT;
RAISERROR('Granting permissions to MDW security roles...', 0, 1)  WITH NOWAIT;

-- Database level permissions
GRANT VIEW DEFINITION                                           TO [mdw_writer]


-- Core schema permissions

-- custom_snaphots use the core.snapshots_internal.snapshot_id column as a FK, 
-- REFERENCES permission need to be granted to mdw_admin 
GRANT REFERENCES ON SCHEMA::[core]                              TO [mdw_admin] 

GRANT EXECUTE ON [core].[sp_create_snapshot]                    TO [mdw_writer]
GRANT EXECUTE ON [core].[sp_update_data_source]                 TO [mdw_writer]
GRANT EXECUTE ON [core].[sp_add_collector_type]                 TO [mdw_admin]
GRANT EXECUTE ON [core].[sp_remove_collector_type]              TO [mdw_admin]
GRANT EXECUTE ON [core].[sp_purge_data]                         TO [mdw_admin]
GRANT EXECUTE ON [core].[sp_stop_purge]                         TO [mdw_admin]

GRANT REFERENCES ON [core].[fn_check_operator]                  TO [mdw_check_operator_admin]

GRANT SELECT  ON [core].[snapshots]                             TO [mdw_admin]    
GRANT SELECT  ON [core].[snapshots]                             TO [mdw_writer]    
GRANT SELECT  ON [core].[snapshots]                             TO [mdw_reader]    

GRANT SELECT  ON [core].[supported_collector_types]             TO [mdw_admin]
GRANT SELECT  ON [core].[supported_collector_types]             TO [mdw_writer]
GRANT SELECT  ON [core].[supported_collector_types]             TO [mdw_reader]

GRANT SELECT  ON [core].[wait_categories]                       TO [mdw_reader]
GRANT SELECT  ON [core].[wait_types]                            TO [mdw_reader]
GRANT SELECT  ON [core].[wait_types_categorized]                TO [mdw_reader]

GRANT SELECT  ON [core].[snapshots_internal]                    TO [mdw_admin]
GRANT SELECT  ON [core].[source_info_internal]                  TO [mdw_admin]
GRANT SELECT  ON [core].[snapshot_timetable_internal]           TO [mdw_admin]
GRANT SELECT  ON [core].[supported_collector_types_internal]    TO [mdw_admin]
GRANT SELECT  ON [core].[fn_query_text_from_handle]             TO [mdw_reader]

GRANT SELECT  ON [core].[performance_counter_report_group_items] TO [mdw_reader]

-- Snapshot schema permissions
GRANT INSERT  ON SCHEMA :: [snapshots]                          TO [mdw_writer]
GRANT EXECUTE ON SCHEMA :: [snapshots]                          TO [mdw_writer]
GRANT DELETE  ON SCHEMA :: [snapshots]                          TO [mdw_admin]
GRANT SELECT  ON SCHEMA :: [snapshots]                          TO [mdw_reader]
-- mdw_writer needs SELECT permission to perform upload because bcp selects the table to verify its schema
GRANT SELECT  ON SCHEMA :: [snapshots]                          TO [mdw_writer]

GRANT EXECUTE ON [snapshots].[sp_get_unknown_query_plan]        TO [mdw_writer]
GRANT EXECUTE ON [snapshots].[sp_get_unknown_query_text]        TO [mdw_writer]
GRANT EXECUTE ON [snapshots].[sp_update_query_plan]             TO [mdw_writer]
GRANT EXECUTE ON [snapshots].[sp_update_query_text]             TO [mdw_writer]

-- Custom_snapshot schema permissions
GRANT CREATE TABLE                                              TO [mdw_writer]
GRANT ALTER   ON SCHEMA :: [custom_snapshots]                   TO [mdw_writer]
GRANT INSERT  ON SCHEMA :: [custom_snapshots]                   TO [mdw_writer]
GRANT DELETE  ON SCHEMA :: [custom_snapshots]                   TO [mdw_admin]
GRANT SELECT  ON SCHEMA :: [custom_snapshots]                   TO [mdw_reader]
-- mdw_writer needs SELECT permission to perform upload because bcp selects the table to verify its schema
GRANT SELECT  ON SCHEMA :: [custom_snapshots]                   TO [mdw_writer]

GRANT CREATE TABLE                                              TO [mdw_admin]
GRANT CONTROL ON SCHEMA :: [custom_snapshots]                   TO [mdw_admin]

-- Report procedures
GRANT EXECUTE ON [snapshots].[rpt_snapshot_times]                       TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_interval_collection_times]            TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_next_and_previous_collection_times]   TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_generic_perfmon]                      TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_generic_perfmon_pivot]                TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_wait_stats]                           TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[fn_hexstrtovarbin]                        TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_top_query_stats]                      TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_query_stats]                          TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_query_plan_stats]                     TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_query_plan_stats_timeline]            TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_query_plan_missing_indexes]           TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_query_plan_parameters]                TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_query_plan_details]                   TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_blocking_chains]                      TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_blocking_chain_detail]                TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_active_sessions_and_requests]         TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_sql_memory_clerks]                    TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_sql_process_and_system_memory]        TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_sampled_waits]                        TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_sampled_waits_longest]                TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_sampled_waits_hottest_resources]      TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_io_virtual_file_stats]                TO [mdw_reader]
GRANT EXECUTE ON [snapshots].[rpt_list_all_servers]                     TO [mdw_reader]

-- NOTE: In order to insert data into MDW the login used for uploading data
-- needs to have ADMINISTER BULK OPERATIONS server permission granted

GO

/**********************************************************************/
/* LEGACY OBJECTS - KEPT FOR OLDER CTPs BUT NO LONGER USED            */
/**********************************************************************/

/**********************************************************************/
/* Used by CTP5 (but not CTP6):                                       */
/**********************************************************************/

--
-- snapshots.rpt_waiting_sessions
--  Returns list of sessions waiting for specified wait type category
-- Parameters: 
--    @ServerName - SQL Server instance name
--    @EndTime - end of the time window (UTC)
--    @WindowSize - number of minutes in the time window
--    @CategoryName - Name of wait category (optional)
--
IF (NOT OBJECT_ID(N'snapshots.rpt_waiting_sessions', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_waiting_sessions] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_waiting_sessions]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_waiting_sessions] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_waiting_sessions]
    @ServerName sysname, 
    @EndTime datetime, 
    @WindowSize int, 
    @CategoryName nvarchar(20)
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @start_time_internal datetimeoffset(7);
    DECLARE @end_time_internal datetimeoffset(7);
    
    -- Start time should be passed in as a UTC datetime
    IF (@EndTime IS NOT NULL)
    BEGIN
        -- Assumed time zone offset for this conversion is +00:00 (datetime must be passed in as UTC)
        SET @end_time_internal = CAST (@EndTime AS datetimeoffset(7));
    END
    ELSE BEGIN
        SELECT @end_time_internal = MAX(ar.collection_time)
        FROM core.snapshots AS s
        INNER JOIN snapshots.active_sessions_and_requests AS ar ON s.snapshot_id = ar.snapshot_id
        WHERE s.instance_name = @ServerName -- AND collection_set_uid = '49268954-4FD4-4EB6-AA04-CD59D9BB5714' -- Server Activity CS
    END
    SET @start_time_internal = DATEADD (minute, -1 * @WindowSize, @end_time_internal);
    
    DECLARE @total_waits TABLE
    (
        wait_type nvarchar(45),
        wait_count bigint, 
        wait_duration_ms bigint
    )

    INSERT INTO @total_waits
    SELECT 
        ar.wait_type,
        COUNT(*) AS wait_count, 
        SUM (wait_duration_ms)
    FROM snapshots.active_sessions_and_requests ar
    JOIN core.snapshots s ON (s.snapshot_id = ar.snapshot_id)
    WHERE s.instance_name = @ServerName -- AND collection_set_uid = '49268954-4FD4-4EB6-AA04-CD59D9BB5714' -- Server Activity CS
        AND ar.collection_time BETWEEN @start_time_internal AND @end_time_internal
        AND ar.wait_type != ''
    GROUP BY ar.wait_type
    
    SELECT
        t.source_id,
        t.session_id,
        t.request_id,
        t.database_name,
        CONVERT (datetime, SWITCHOFFSET (CAST (t.login_time AS datetimeoffset(7)), '+00:00')) AS login_time, 
        t.login_name,
        t.[program_name],
        t.sql_handle,
        master.dbo.fn_varbintohexstr (t.sql_handle) AS sql_handle_str,
        t.statement_start_offset,
        t.statement_end_offset,
        t.plan_handle,
        master.dbo.fn_varbintohexstr (t.plan_handle) AS plan_handle_str,
        t.wait_type,
        t.wait_count,
        t.wait_total_percent,
        t.wait_type_wait_percent,
        qt.query_text, 
        REPLACE (REPLACE (REPLACE (REPLACE (REPLACE (REPLACE (
            LEFT (LTRIM (qt.query_text), 100)
            , CHAR(9), ' '), CHAR(10), ' '), CHAR(13), ' '), '   ', ' '), '  ', ' '), '  ', ' ') AS flat_query_text
    FROM
    (
        SELECT 
            s.source_id,
            ar.session_id,
            ar.request_id,
            ar.database_name,
            ar.login_time,
            ar.login_name,
            ar.program_name,
            ar.sql_handle,
            ar.statement_start_offset,
            ar.statement_end_offset,
            ar.plan_handle,
            ar.wait_type,
            COUNT(*) AS wait_count,
            (COUNT(*)) / CONVERT(decimal, (SELECT SUM(wait_count) FROM @total_waits)) AS wait_total_percent,
            (COUNT(*)) / CONVERT(decimal, (SELECT wait_count FROM @total_waits WHERE wait_type = ar.wait_type)) AS wait_type_wait_percent
        FROM snapshots.active_sessions_and_requests AS ar
        INNER JOIN core.snapshots AS s ON (s.snapshot_id = ar.snapshot_id)
        INNER JOIN core.wait_types_categorized AS wt on (wt.wait_type = ar.wait_type)
        WHERE s.instance_name = @ServerName -- AND s.collection_set_uid = '49268954-4FD4-4EB6-AA04-CD59D9BB5714' -- Server Activity CS
            AND ar.collection_time BETWEEN @start_time_internal AND @end_time_internal
            AND wt.category_name = @CategoryName
        GROUP BY s.source_id, ar.session_id, ar.request_id, ar.database_name, ar.login_time, ar.login_name, ar.program_name, 
            ar.wait_type, ar.sql_handle, ar.statement_start_offset, ar.statement_end_offset, ar.plan_handle
    ) AS t 
    OUTER APPLY snapshots.fn_get_query_text(t.source_id, t.sql_handle, t.statement_start_offset, t.statement_end_offset) AS qt

END;
GO
GRANT EXECUTE ON [snapshots].[rpt_waiting_sessions]                     TO [mdw_reader]
GO


-- This function returns a list of all the known query plans from a disctinct source 
IF (NOT OBJECT_ID(N'snapshots.fn_get_notable_query_plans', 'TF') IS NULL)
BEGIN
    RAISERROR('Dropping function [snapshots].[fn_get_notable_query_plans] ...', 0, 1)  WITH NOWAIT;
    DROP FUNCTION [snapshots].[fn_get_notable_query_plans]
END

RAISERROR('Creating function [snapshots].[fn_get_notable_query_plans]', 0, 1)  WITH NOWAIT;
Go
CREATE FUNCTION [snapshots].[fn_get_notable_query_plans](
    @source_id  int
)
RETURNS @notable_queries TABLE (sql_handle varbinary(64) , 
                                plan_handle varbinary(64), 
                                statement_start_offset int, 
                                statement_end_offset int,
                                creation_time datetime)
BEGIN
    INSERT INTO @notable_queries
    SELECT  [sql_handle], 
        [plan_handle],
        [statement_start_offset],
        [statement_end_offset],
            -- Convert datetimeoffset to datetime so that SSIS can easily join the output back 
            -- to the new sys.dm_exec_query_stats data
            CONVERT (datetime, [creation_time]) AS [creation_time]
    FROM    [snapshots].[notable_query_plan]
    WHERE   [source_id] = @source_id
    ORDER BY [sql_handle] ASC, [plan_handle], [statement_start_offset], [statement_end_offset], [creation_time] ASC

    RETURN
END
GO

GRANT SELECT  ON [snapshots].[fn_get_notable_query_plans]       TO [mdw_writer]
GO



-- This function returns a list of all the known sql_handles (i.e., query text) from a disctinct source 
IF (NOT OBJECT_ID(N'snapshots.fn_get_notable_query_text', 'TF') IS NULL)
BEGIN
    RAISERROR('Dropping function [snapshots].[fn_get_notable_query_text] ...', 0, 1)  WITH NOWAIT;
    DROP FUNCTION [snapshots].[fn_get_notable_query_text]
END

RAISERROR('Creating function [snapshots].[fn_get_notable_query_text]', 0, 1)  WITH NOWAIT;
Go
CREATE FUNCTION [snapshots].[fn_get_notable_query_text](
    @source_id int
)
RETURNS @notable_text TABLE (sql_handle varbinary(64))
BEGIN
    INSERT INTO @notable_text
    SELECT  [sql_handle]
    FROM    [snapshots].[notable_query_text]
    WHERE   [source_id] = @source_id
    ORDER BY [sql_handle] ASC

    RETURN
END
GO

GRANT SELECT  ON [snapshots].[fn_get_notable_query_text]        TO [mdw_writer]
GO


--
-- snapshots.rpt_sql_memory_counters_one_snapshot
--  Returns values for page life expectancy and other SQL Server memory counters
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_sql_memory_counters_one_snapshot', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_sql_memory_counters_one_snapshot] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_sql_memory_counters_one_snapshot]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_sql_memory_counters_one_snapshot] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_sql_memory_counters_one_snapshot]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        pc.performance_counter_name AS series,
        CONVERT (datetime, SWITCHOFFSET (CAST (pc.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        pc.formatted_value 
    FROM snapshots.performance_counters pc
    INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id = @snapshot_time_id
        AND pc.performance_object_name LIKE '%SQL%:Buffer Manager'
        AND pc.performance_counter_name = 'Page life expectancy'
    ORDER BY 
        pc.collection_time, pc.performance_counter_name
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_sql_memory_counters_one_snapshot]     TO [mdw_reader]
GO


--
-- snapshots.rpt_sql_memory_clerks_one_snapshot
--  Returns data from os_memory_clerks table
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_sql_memory_clerks_one_snapshot', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_sql_memory_clerks_one_snapshot] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_sql_memory_clerks_one_snapshot]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_sql_memory_clerks_one_snapshot] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_sql_memory_clerks_one_snapshot]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @total_allocated TABLE (
        collection_time datetimeoffset(7),
        total_kb bigint
    );

    INSERT INTO @total_allocated 
    SELECT 
        mc.collection_time,
        SUM(mc.single_pages_kb + 
            mc.multi_pages_kb + 
            (CASE WHEN type <> N'MEMORYCLERK_SQLBUFFERPOOL' THEN mc.virtual_memory_committed_kb 
            ELSE 0 END) + 
            mc.shared_memory_committed_kb)
    FROM snapshots.os_memory_clerks mc
    JOIN core.snapshots s ON (s.snapshot_id = mc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id = @snapshot_time_id
    GROUP BY mc.collection_time

    SELECT 
        CONVERT (datetime, SWITCHOFFSET (CAST (mc.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        mc.type,
        mc.single_pages_kb + mc.multi_pages_kb as allocated_kb,
        mc.virtual_memory_reserved_kb as virtual_reserved_kb,
        mc.virtual_memory_committed_kb as virtual_committed_kb,
        mc.awe_allocated_kb as awe_allocated_kb,
        mc.shared_memory_reserved_kb as shared_reserved_kb,
        mc.shared_memory_committed_kb as shared_committed_kb,
        (mc.single_pages_kb + mc.multi_pages_kb + (CASE WHEN type <> 'MEMORYCLERK_SQLBUFFERPOOL' THEN mc.virtual_memory_committed_kb ELSE 0 END) + mc.shared_memory_committed_kb) as total_kb,
        (mc.single_pages_kb + mc.multi_pages_kb + (CASE WHEN type <> 'MEMORYCLERK_SQLBUFFERPOOL' THEN mc.virtual_memory_committed_kb ELSE 0 END) + mc.shared_memory_committed_kb) / CONVERT(decimal, ta.total_kb) AS percent_total_kb,
        CASE
            WHEN (mc.single_pages_kb + mc.multi_pages_kb + (CASE WHEN type <> 'MEMORYCLERK_SQLBUFFERPOOL' THEN mc.virtual_memory_committed_kb ELSE 0 END) + mc.shared_memory_committed_kb) / CONVERT(decimal, ta.total_kb) > 0.05 THEN mc.type
            ELSE N'Other'
        END AS graph_type
    FROM snapshots.os_memory_clerks mc
    JOIN @total_allocated ta ON (mc.collection_time = ta.collection_time)
    ORDER BY collection_time
    
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_sql_memory_clerks_one_snapshot]       TO [mdw_reader]
GO


--
-- snapshots.rpt_sql_process_memory_one_snapshot
--  Returns details on sql process memory usage
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_sql_process_memory_one_snapshot', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_sql_process_memory_one_snapshot] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_sql_process_memory_one_snapshot]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_sql_process_memory_one_snapshot] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_sql_process_memory_one_snapshot]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;

    SELECT 
        CONVERT (datetime, SWITCHOFFSET (CAST (collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        CASE 
            WHEN series = 'virtual_address_space_reserved_kb' THEN 'Virtual Memory Reserved'
            WHEN series = 'virtual_address_space_committed_kb' THEN 'Virtual Memory Committed'
            WHEN series = 'physical_memory_in_use_kb' THEN 'Physical Memory In Use'
            WHEN series = 'process_physical_memory_low' THEN 'Physical Memory Low'
            WHEN series = 'process_virtual_memory_low' THEN 'Virtual Memory Low'
            ELSE series
        END AS series,
        [value] / 1024 AS [value] -- convert KB to MB
    FROM 
    (
        SELECT 
            pm.collection_time,
            pm.virtual_address_space_reserved_kb,
            pm.virtual_address_space_committed_kb,
            pm.physical_memory_in_use_kb,
            CONVERT (bigint, pm.process_physical_memory_low) AS process_physical_memory_low,
            CONVERT (bigint, pm.process_virtual_memory_low) AS process_virtual_memory_low
        FROM [snapshots].[os_process_memory] pm
        JOIN core.snapshots s ON (s.snapshot_id = pm.snapshot_id)
        WHERE s.instance_name = @instance_name
            AND s.snapshot_time_id = @snapshot_time_id
    ) AS pvt
    UNPIVOT
    (
        [value] for [series] in 
            (virtual_address_space_reserved_kb, virtual_address_space_committed_kb, 
            physical_memory_in_use_kb)
    ) AS unpvt
    UNION ALL 
    SELECT 
        CONVERT (datetime, SWITCHOFFSET (CAST (collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        'Stolen Buffer Pool' AS [series], 
        pc.formatted_value * 8 AS [value] -- Convert from pages to KB
    FROM snapshots.performance_counters AS pc
    INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id = @snapshot_time_id
        AND pc.performance_object_name LIKE '%SQL%:Buffer Manager'
        AND pc.performance_counter_name = 'Stolen pages'
    ORDER BY collection_time
END
GO

GRANT EXECUTE ON [snapshots].[rpt_sql_process_memory_one_snapshot]      TO [mdw_reader]
GO

--
-- snapshots.rpt_memory_counters_one_snapshot
--  Returns values for memory usage counters
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_memory_counters_one_snapshot', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_memory_counters_one_snapshot] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_memory_counters_one_snapshot]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_memory_counters_one_snapshot] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_memory_counters_one_snapshot]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        pc.performance_counter_name AS series,
        CONVERT (datetime, SWITCHOFFSET (CAST (pc.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        pc.collection_time,
        pc.formatted_value / (1024*1024) AS formatted_value
    FROM snapshots.performance_counters pc
    INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id = @snapshot_time_id
        AND 
        (
            (pc.performance_object_name = 'Memory' AND pc.performance_counter_name IN ('Cache Bytes', 'Pool Nonpaged Bytes'))
            OR (pc.performance_object_name = 'Process' AND pc.performance_counter_name = 'Working Set' AND pc.performance_instance_name = '_Total')
        )
    ORDER BY 
        pc.collection_time, series
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_memory_counters_one_snapshot]         TO [mdw_reader]
GO


--
-- snapshots.rpt_memory_rates_one_snapshot
--  Returns values for various memory ratios 
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_memory_rates_one_snapshot', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_memory_rates_one_snapshot] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_memory_rates_one_snapshot]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_memory_rates_one_snapshot] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_memory_rates_one_snapshot]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        pc.performance_counter_name AS series,
        CONVERT (datetime, SWITCHOFFSET (CAST (pc.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        pc.collection_time,
        pc.formatted_value
    FROM snapshots.performance_counters pc
    INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id = @snapshot_time_id
        AND pc.performance_object_name = 'Memory' 
        AND pc.performance_counter_name IN ('Page Faults/sec', 'Page Reads/sec', 'Page Writes/sec', 'Cache Faults/sec')
    ORDER BY 
        pc.collection_time, series
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_memory_rates_one_snapshot]            TO [mdw_reader]
GO

--
-- snapshots.rpt_cpu_counters_one_snapshot
--  Returns values for CPU usage counters per processor
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_cpu_counters_one_snapshot', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_cpu_counters_one_snapshot] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_cpu_counters_one_snapshot]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_cpu_counters_one_snapshot] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_cpu_counters_one_snapshot]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        N'CPU ' + CONVERT (nvarchar(10), ISNULL(pc.performance_instance_name, N'')) as series,
        CONVERT (datetime, SWITCHOFFSET (CAST (pc.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        pc.formatted_value
    FROM snapshots.performance_counters pc
    JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id = @snapshot_time_id
        AND pc.performance_object_name = 'Processor' AND pc.performance_counter_name = '% Processor Time' 
        AND pc.performance_instance_name != '_Total'
    ORDER BY 
        pc.collection_time, series
    -- These trace flags are necessary for a good plan, due to the join on ascending PK w/range filter
    OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390)
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_cpu_counters_one_snapshot]            TO [mdw_reader]
GO

--
-- snapshots.rpt_cpu_queues_one_snapshot
--  Returns values for queue length counter per processor 
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_cpu_queues_one_snapshot', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_cpu_queues_one_snapshot] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_cpu_queues_one_snapshot]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_cpu_queues_one_snapshot] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_cpu_queues_one_snapshot]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        CASE pc.performance_counter_name 
            WHEN 'Processor Queue Length' THEN N'Processor Queue Length'
            ELSE N'CPU ' + CONVERT (nvarchar(10), ISNULL(pc.performance_instance_name, N'')) 
        END AS series,
        CONVERT (datetime, SWITCHOFFSET (CAST (pc.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        pc.formatted_value
    FROM snapshots.performance_counters pc
    JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id = @snapshot_time_id
        AND 
        (
            (pc.performance_object_name = 'Server Work Queues' AND pc.performance_counter_name = 'Queue Length'
                AND pc.performance_instance_name != '_Total' AND ISNUMERIC (pc.performance_instance_name) = 1)
            OR (pc.performance_object_name = 'System' AND pc.performance_counter_name = 'Processor Queue Length')
        )
    ORDER BY 
        pc.collection_time, series
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_cpu_queues_one_snapshot]              TO [mdw_reader]
GO

--
-- snapshots.rpt_cpu_usage_per_process
--  Returns min, max, avg CPU usage and avg thread count for top 10 processes
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_cpu_usage_per_process', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_cpu_usage_per_process] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_cpu_usage_per_process]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_cpu_usage_per_process] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_cpu_usage_per_process]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Determine the CPU count on the target system by querying the number of "Processor" 
    -- counter instances we captured in a perfmon sample that was captured around the same 
    -- time. 
    DECLARE @cpu_count smallint
    SELECT @cpu_count = COUNT (DISTINCT pc.performance_instance_name)
    FROM snapshots.performance_counters AS pc
    INNER JOIN core.snapshots s ON s.snapshot_id = pc.snapshot_id 
    WHERE pc.performance_object_name = 'Processor' AND pc.performance_counter_name = '% Processor Time'
        AND pc.performance_instance_name != '_Total'
        AND s.snapshot_time_id = @snapshot_time_id
        AND s.instance_name = @instance_name
        AND pc.collection_time = 
            (SELECT TOP 1 collection_time FROM snapshots.performance_counter_values pcv2 WHERE pcv2.snapshot_id = s.snapshot_id)

    SELECT TOP 10
        cpu.process_name,
        cpu.minimum_value / @cpu_count AS cpu_minimum_value,
        cpu.maximum_value / @cpu_count AS cpu_maximum_value,
        cpu.average_value / @cpu_count AS cpu_average_value,
        tc.average_value AS  tc_average_value
    FROM
    (  SELECT
            ISNULL(pc.performance_instance_name, N'') AS process_name,
            MIN(pc.formatted_value)         AS minimum_value,
            MAX(pc.formatted_value)         AS maximum_value,
            AVG(pc.formatted_value)         AS average_value
        FROM [snapshots].[performance_counters] AS pc
        INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
        WHERE
            s.snapshot_time_id = @snapshot_time_id
            AND s.instance_name = @instance_name
            AND (pc.performance_object_name = 'Process' AND pc.performance_counter_name = '% Processor Time'
                AND pc.performance_instance_name NOT IN ('_Total', 'Idle'))
        GROUP BY pc.performance_instance_name
    ) AS cpu
    INNER JOIN
    (  SELECT
            ISNULL(pc.performance_instance_name, N'') AS process_name,
            MIN(pc.formatted_value)         AS minimum_value,
            MAX(pc.formatted_value)         AS maximum_value,
            AVG(pc.formatted_value)         AS average_value
        FROM [snapshots].[performance_counters] as pc
        INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
        WHERE
            s.snapshot_time_id = @snapshot_time_id
            AND s.instance_name = @instance_name
            AND (pc.performance_object_name = 'Process' AND pc.performance_counter_name = 'Thread Count'
                AND pc.performance_instance_name NOT IN ('_Total', 'Idle'))
        GROUP BY pc.performance_instance_name
    ) AS tc ON (tc.process_name = cpu.process_name)
    ORDER BY cpu_average_value DESC

END;
GO

GRANT EXECUTE ON [snapshots].[rpt_cpu_usage_per_process]                TO [mdw_reader]
GO

--
-- snapshots.rpt_sessions_and_connections
--  Returns counts for sessions and connections within the given snapshot
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_sessions_and_connections', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_sessions_and_connections] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_sessions_and_connections]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_sessions_and_connections] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_sessions_and_connections]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;


    SELECT 
        pc.performance_counter_name AS series,
        CONVERT (datetime, SWITCHOFFSET (CAST (pc.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        pc.formatted_value
    FROM snapshots.performance_counters pc
    INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id = @snapshot_time_id
        AND (pc.performance_object_name LIKE '%SQL%:General Statistics' OR pc.performance_object_name LIKE '%SQL%:Databases')
        AND pc.performance_counter_name IN ('User Connections', 'Active Transactions')

    UNION ALL 

    SELECT 
        N'Active sessions' AS series,
        ar.collection_time,
        COUNT(DISTINCT ar.session_id) AS formatted_value
    FROM snapshots.active_sessions_and_requests ar
    INNER JOIN core.snapshots s ON (s.snapshot_id = ar.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id = @snapshot_time_id
    GROUP BY ar.collection_time    

    ORDER BY collection_time
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_sessions_and_connections]             TO [mdw_reader]
GO


--
-- snapshots.rpt_requests_and_compilations
--  Returns counts for number of requests/sec and related compilations counters
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_requests_and_compilations', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_requests_and_compilations] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_requests_and_compilations]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_requests_and_compilations] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_requests_and_compilations]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;

    SELECT 
        pc.performance_counter_name AS series,
        CONVERT (datetime, SWITCHOFFSET (CAST (pc.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        pc.formatted_value
    FROM snapshots.performance_counters pc
    INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id = @snapshot_time_id
        AND pc.performance_object_name LIKE '%SQL%:SQL Statistics'
        AND pc.performance_counter_name IN ('Batch Requests/sec', 'SQL Compilations/sec', 
            'SQL Re-Compilations/sec', 'Auto-Param Attempts/sec', 'Failed Auto-Params/sec')
    ORDER BY pc.collection_time, pc.performance_counter_name
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_requests_and_compilations]            TO [mdw_reader]
GO

--
-- snapshots.rpt_plan_cache_hit_ratio
--  Returns details on plan cache hit ratio counter
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_plan_cache_hit_ratio', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_plan_cache_hit_ratio] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_plan_cache_hit_ratio]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_plan_cache_hit_ratio] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_plan_cache_hit_ratio]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;

    SELECT 
        ISNULL(pc.performance_instance_name, N'') AS series,
        CONVERT (datetime, SWITCHOFFSET (CAST (pc.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        pc.formatted_value
    FROM snapshots.performance_counters pc
    INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id = @snapshot_time_id
        AND pc.performance_object_name LIKE '%SQL%:Plan Cache'
        AND pc.performance_counter_name = 'Cache Hit Ratio'
        AND pc.performance_instance_name != '_Total'
    ORDER BY pc.collection_time, ISNULL(pc.performance_instance_name, N'')
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_plan_cache_hit_ratio]                 TO [mdw_reader]
GO


--
-- snapshots.rpt_tempdb
--  Returns counters related to tempdb
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_tempdb', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_tempdb] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_tempdb]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_tempdb] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_tempdb]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;

    SELECT 
        pc.performance_counter_name AS series,
        CONVERT (datetime, SWITCHOFFSET (CAST (pc.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        pc.formatted_value
    FROM snapshots.performance_counters pc
    INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id = @snapshot_time_id
        AND (pc.performance_object_name LIKE '%SQL%:Transactions' OR pc.performance_object_name LIKE '%SQL%:General Statistics')
        AND pc.performance_counter_name IN ('Free Space in tempdb (KB)', 'Active Temp Tables')
    ORDER BY pc.collection_time, pc.performance_counter_name
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_tempdb]                               TO [mdw_reader]
GO

--
-- snapshots.rpt_disk_speed_one_snapshot
--  Returns values for disk usage counters
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_disk_speed_one_snapshot', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_disk_speed_one_snapshot] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_disk_speed_one_snapshot]
END
GO

RAISERROR('Creating procedure [snapshots].[rpt_disk_speed_one_snapshot] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_disk_speed_one_snapshot]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        ISNULL(pc.performance_instance_name, N'') AS disk_letter,
        pc.performance_counter_name AS counter,
        CONVERT (datetime, SWITCHOFFSET (CAST (pc.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        pc.formatted_value AS formatted_value
    FROM snapshots.performance_counters pc
    INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id = @snapshot_time_id
        AND pc.performance_object_name = 'LogicalDisk' 
        AND pc.performance_counter_name IN ('Avg. Disk sec/Read', 'Avg. Disk sec/Write')
        AND pc.performance_instance_name != '_Total'
    ORDER BY 
        pc.collection_time, pc.performance_counter_name, ISNULL(pc.performance_instance_name, N'')
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_disk_speed_one_snapshot]              TO [mdw_reader]
GO


--
-- snapshots.rpt_disk_queues_one_snapshot
--  Returns values for disk queue counters
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_disk_queues_one_snapshot', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_disk_queues_one_snapshot] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_disk_queues_one_snapshot]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_disk_queues_one_snapshot] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_disk_queues_one_snapshot]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        ISNULL(pc.performance_instance_name, N'') AS disk_letter,
        pc.performance_counter_name AS counter,
        CONVERT (datetime, SWITCHOFFSET (CAST (pc.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        pc.formatted_value
    FROM snapshots.performance_counters pc
    INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id = @snapshot_time_id
        AND pc.performance_object_name = 'LogicalDisk' 
        AND pc.performance_counter_name IN ('Current Disk Queue Length', 'Avg. Disk Read Queue Length', 'Avg. Disk Write Queue Length')
        AND pc.performance_instance_name != '_Total'
    ORDER BY 
        pc.collection_time, pc.performance_counter_name, ISNULL(pc.performance_instance_name, N'')
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_disk_queues_one_snapshot]             TO [mdw_reader]
GO


--
-- snapshots.rpt_disk_ratios_one_snapshot
--  Returns values for disk ratio counters
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_disk_ratios_one_snapshot', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_disk_ratios_one_snapshot] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_disk_ratios_one_snapshot]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_disk_ratios_one_snapshot] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_disk_ratios_one_snapshot]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        ISNULL(pc.performance_instance_name, N'') AS disk_letter,
        pc.performance_counter_name AS counter,
        CONVERT (datetime, SWITCHOFFSET (CAST (pc.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        pc.formatted_value
    FROM snapshots.performance_counters pc
    INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id = @snapshot_time_id
        AND pc.performance_object_name = 'LogicalDisk' 
        AND pc.performance_counter_name IN ('Disk Reads/sec', 'Disk Writes/sec')
        AND pc.performance_instance_name != '_Total'
    ORDER BY 
        pc.collection_time, pc.performance_counter_name, ISNULL(pc.performance_instance_name, N'')
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_disk_ratios_one_snapshot]             TO [mdw_reader]
GO


--
-- snapshots.rpt_disk_usage_per_process
--  Returns min, max, avg CPU usage and avg thread count for top 10 processes
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_disk_usage_per_process', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_disk_usage_per_process] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_disk_usage_per_process]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_disk_usage_per_process] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_disk_usage_per_process]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT TOP 10
        wb.process_name,
        wb.minimum_value AS wb_minimum_value,
        wb.maximum_value AS wb_maximum_value,
        wb.average_value AS wb_average_value,
        rb.minimum_value AS rb_minimum_value,
        rb.maximum_value AS rb_maximum_value,
        rb.average_value AS rb_average_value
    FROM
    (  SELECT
            ISNULL(pc.performance_instance_name, N'') AS process_name,
            MIN(pc.formatted_value / (1024))        as minimum_value,
            MAX(pc.formatted_value / (1024))        as maximum_value,
            AVG(pc.formatted_value / (1024))        as average_value
        FROM [snapshots].[performance_counters] as pc
        INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
        WHERE
            s.snapshot_time_id = @snapshot_time_id
            AND s.instance_name = @instance_name
            AND pc.performance_object_name = 'Process' 
            AND pc.performance_counter_name = 'IO Read Bytes/sec'
            AND pc.performance_instance_name NOT IN ('_Total', 'Idle')
        GROUP BY pc.performance_instance_name
    ) AS rb
    INNER JOIN
    (  SELECT
            ISNULL(pc.performance_instance_name, N'') AS process_name,
            MIN(pc.formatted_value / (1024))        as minimum_value,
            MAX(pc.formatted_value / (1024))        as maximum_value,
            AVG(pc.formatted_value / (1024))        as average_value
        FROM [snapshots].[performance_counters] as pc
        INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
        WHERE
            s.snapshot_time_id = @snapshot_time_id
            AND s.instance_name = @instance_name
            AND pc.performance_object_name = 'Process' 
            AND pc.performance_counter_name = 'IO Write Bytes/sec'
            AND pc.performance_instance_name NOT IN ('_Total', 'Idle')
        GROUP BY pc.performance_instance_name
    ) AS wb ON (wb.process_name = rb.process_name)
    ORDER BY wb_average_value DESC, rb_average_value DESC
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_disk_usage_per_process]               TO [mdw_reader]
GO


-- snapshots.rpt_waiting_sessions_per_snapshot
--  Returns list of sessions waiting for specified wait type category
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--    @wait_category_name - Name of wait category to filter on 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_waiting_sessions_per_snapshot', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_waiting_sessions_per_snapshot] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_waiting_sessions_per_snapshot]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_waiting_sessions_per_snapshot] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_waiting_sessions_per_snapshot]
    @instance_name sysname,
    @snapshot_time_id int,
    @wait_category_name nvarchar(20)
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @total_waits TABLE
    (
        wait_type nvarchar(45),
        wait_count bigint
    )

    INSERT INTO @total_waits
    SELECT 
        ar.wait_type,
        COUNT(ar.wait_type) 
    FROM snapshots.active_sessions_and_requests ar
    JOIN core.snapshots s ON (s.snapshot_id = ar.snapshot_id)
    WHERE s.snapshot_time_id = @snapshot_time_id
      AND s.instance_name = @instance_name
    GROUP BY ar.wait_type
    
    SELECT
        t.source_id,
        t.session_id,
        t.request_id,
        t.database_name,
        CONVERT (datetime, SWITCHOFFSET (CAST (t.login_time AS datetimeoffset(7)), '+00:00')) AS login_time, 
        t.login_name,
        t.[program_name],
        t.sql_handle,
        master.dbo.fn_varbintohexstr (t.sql_handle) AS sql_handle_str,
        t.statement_start_offset,
        t.statement_end_offset,
        t.plan_handle,
        master.dbo.fn_varbintohexstr (t.plan_handle) AS plan_handle_str,
        t.wait_type,
        t.wait_count,
        t.wait_total_precent,
        t.wait_type_wait_precent,
        qt.query_text
    FROM
    (
        SELECT 
            s.source_id,
            ar.session_id,
            ar.request_id,
            ar.database_name,
            ar.login_time,
            ar.login_name,
            ar.program_name,
            ar.sql_handle,
            ar.statement_start_offset,
            ar.statement_end_offset,
            ar.plan_handle,
            ar.wait_type,
            COUNT(ar.wait_type) AS wait_count,
            (COUNT(ar.wait_type)) / CONVERT(decimal, (SELECT SUM(wait_count) FROM @total_waits)) AS wait_total_precent,
            (COUNT(ar.wait_type)) / CONVERT(decimal, (SELECT wait_count FROM @total_waits WHERE wait_type = ar.wait_type)) AS wait_type_wait_precent
        FROM snapshots.active_sessions_and_requests ar
        JOIN core.snapshots s ON (s.snapshot_id = ar.snapshot_id)
        JOIN core.wait_types ev on (ev.wait_type = ar.wait_type)
        JOIN core.wait_categories ct on (ct.category_id = ev.category_id)
        WHERE s.snapshot_time_id = @snapshot_time_id
          AND s.instance_name = @instance_name
          AND ct.category_name = @wait_category_name
        GROUP BY s.source_id, ar.session_id, ar.request_id, ar.database_name, ar.login_time, ar.login_name, ar.program_name, ar.wait_type, ar.sql_handle, ar.statement_start_offset, ar.statement_end_offset, ar.plan_handle
    ) t 
    OUTER APPLY snapshots.fn_get_query_text(t.source_id, t.sql_handle, t.statement_start_offset, t.statement_end_offset) AS qt

END;
GO
GRANT EXECUTE ON [snapshots].[rpt_waiting_sessions_per_snapshot]        TO [mdw_reader]
GO


--
-- snapshots.rpt_memory_usage_per_process
--  Returns min, max, avg CPU usage and avg thread count for top 10 processes
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_memory_usage_per_process', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_memory_usage_per_process] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_memory_usage_per_process]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_memory_usage_per_process] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_memory_usage_per_process]
    @instance_name sysname,
    @snapshot_time_id int
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT TOP 10
        ws.process_name,
        ws.minimum_value AS ws_minimum_value,
        ws.maximum_value AS ws_maximum_value,
        ws.average_value AS ws_average_value,
        vb.minimum_value AS vb_minimum_value,
        vb.maximum_value AS vb_maximum_value,
        vb.average_value AS vb_average_value
    FROM
    (  SELECT
            ISNULL(pc.performance_instance_name, N'') as process_name,
            MIN(pc.formatted_value / (1024*1024))        as minimum_value,
            MAX(pc.formatted_value / (1024*1024))        as maximum_value,
            AVG(pc.formatted_value / (1024*1024))        as average_value
        FROM [snapshots].[performance_counters] as pc
        INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
        WHERE
            s.snapshot_time_id = @snapshot_time_id
            AND s.instance_name = @instance_name
            AND pc.performance_object_name = 'Process' AND pc.performance_counter_name = 'Working Set'
            AND pc.performance_instance_name NOT IN ('_Total', 'Idle')
        GROUP BY pc.performance_instance_name
    ) AS ws
    INNER JOIN
    (  SELECT
            ISNULL(pc.performance_instance_name, N'') as process_name,
            MIN(pc.formatted_value / (1024*1024))        as minimum_value,
            MAX(pc.formatted_value / (1024*1024))        as maximum_value,
            AVG(pc.formatted_value / (1024*1024))        as average_value
        FROM [snapshots].[performance_counters] as pc
        INNER JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
        WHERE
            s.snapshot_time_id = @snapshot_time_id
            AND s.instance_name = @instance_name
            AND pc.performance_object_name = 'Process' AND pc.performance_counter_name = 'Virtual Bytes'
            AND pc.performance_instance_name NOT IN ('_Total', 'Idle')
        GROUP BY pc.performance_instance_name
    ) AS vb ON (vb.process_name = ws.process_name)
    ORDER BY ws_average_value DESC

END;
GO

GRANT EXECUTE ON [snapshots].[rpt_memory_usage_per_process]             TO [mdw_reader]
GO


-- snapshots.rpt_wait_stats_by_category_by_snapshot
--  Returns deltas of wait stats with one snapshot interval and aggregated by category
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @start_time - (Optional) time window start (UTC)
--    @end_time - time window end (UTC)
--    @time_window_size - Number of intervals in the time window (provide if @start_time is NULL)
--    @time_interval_min - Number of minutes in each interval (provide if @start_time is NULL)
--    @category_name - (Optional) Name of wait category to filter on (all categories if NULL)
--
IF (NOT OBJECT_ID(N'snapshots.rpt_wait_stats_by_category_by_snapshot', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_wait_stats_by_category_by_snapshot] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_wait_stats_by_category_by_snapshot]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_wait_stats_by_category_by_snapshot] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_wait_stats_by_category_by_snapshot]
    @instance_name sysname,
    @start_time datetime = NULL,
    @end_time datetime = NULL,
    @time_window_size smallint = NULL,
    @time_interval_min smallint = NULL, 
    @category_name nvarchar(20) = NULL
AS
BEGIN
    SET NOCOUNT ON;

    -- Convert snapshot_time (datetimeoffset) to a UTC datetime
    IF (@end_time IS NULL)
        SET @end_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MAX(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));

    IF (@start_time IS NULL)
    BEGIN
        -- If time_window_size and time_interval_min are set use them
        -- to determine the start time
        -- Otherwise use the earliest available snapshot_time
        IF @time_window_size IS NOT NULL AND @time_interval_min IS NOT NULL
        BEGIN
            SET @start_time = DATEADD(minute, @time_window_size * @time_interval_min * -1.0, @end_time);
        END
        ELSE
        BEGIN
            -- Convert min snapshot_time (datetimeoffset(7)) to a UTC datetime
            SET @start_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MIN(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));
        END
    END

    DECLARE @end_snapshot_time_id int;
    SELECT @end_snapshot_time_id = MAX(snapshot_time_id) FROM core.snapshots WHERE snapshot_time <= @end_time;

    DECLARE @start_snapshot_time_id int;
    SELECT @start_snapshot_time_id = MIN(snapshot_time_id) FROM core.snapshots WHERE snapshot_time >= @start_time;

    -- If the selected time window is > 1 hour, we'll chart wait times at 15 minute intervals.  If the selected 
    -- time window is < 1 hour, we'll use 5 minute intervals. 
    DECLARE @group_interval_min int
    IF DATEDIFF (minute, @start_time, @end_time) > 60
    BEGIN
        SET @group_interval_min = 15
    END 
    ELSE BEGIN
        SET @group_interval_min = 5
    END;
    -- Get wait times by waittype (plus CPU time, modeled as a waittype)
    WITH wait_times AS 
    ( 
        SELECT 
            s.snapshot_id, s.snapshot_time_id, s.snapshot_time, 
            DENSE_RANK() OVER (ORDER BY ws.collection_time) AS [rank], 
            ws.collection_time, wt.category_name, ws.wait_type, 
            ws.waiting_tasks_count, 
        ISNULL (ws.signal_wait_time_ms, 0) AS signal_wait_time_ms, 
        ISNULL (ws.wait_time_ms, 0) - ISNULL (ws.signal_wait_time_ms, 0) AS wait_time_ms, 
        ISNULL (ws.wait_time_ms, 0) AS wait_time_ms_cumulative 
    FROM snapshots.os_wait_stats AS ws
        INNER JOIN core.wait_types_categorized wt ON wt.wait_type = ws.wait_type
        INNER JOIN core.snapshots s ON ws.snapshot_id = s.snapshot_id
        WHERE s.snapshot_time_id BETWEEN @start_snapshot_time_id AND @end_snapshot_time_id
            AND s.instance_name = @instance_name
            AND wt.category_name = ISNULL (@category_name, wt.category_name)
        AND wt.ignore != 1

            AND ws.collection_time IN 
            (
                SELECT MAX(collection_time) 
                FROM snapshots.os_wait_stats ws2 
                WHERE ws2.collection_time BETWEEN @start_time AND @end_time
                GROUP BY DATEDIFF (minute, '19000101', ws2.collection_time) / @group_interval_min
            )
    )

    -- Get resource wait stats for this snapshot_time_id interval
    -- We must convert all datetimeoffset values to UTC datetime values before returning to Reporting Services
    SELECT 
        CONVERT (datetime, SWITCHOFFSET (CAST (w2.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        CONVERT (datetime, SWITCHOFFSET (CAST (w2.snapshot_time AS datetimeoffset(7)), '+00:00')) AS snapshot_time, 
        w2.snapshot_time_id, 
        CONVERT (datetime, SWITCHOFFSET (CAST (w1.collection_time AS datetimeoffset(7)), '+00:00')) AS interval_start, 
        CONVERT (datetime, SWITCHOFFSET (CAST (w2.collection_time AS datetimeoffset(7)), '+00:00')) AS interval_end, 
        w2.category_name, w2.wait_type, 
        -- All wait stats will be reset to zero by a service cycle, which will cause 
        -- (snapshot2waittime-snapshot1waittime) calculations to produce an incorrect 
        -- negative wait time for the interval.  Detect this and avoid calculating 
        -- negative wait time/wait count/signal time deltas. 
        CASE 
            WHEN (w2.waiting_tasks_count - w1.waiting_tasks_count) < 0 THEN w2.waiting_tasks_count 
            ELSE (w2.waiting_tasks_count - w1.waiting_tasks_count) 
        END AS waiting_tasks_count_delta, 
        CASE 
            WHEN (w2.wait_time_ms - w1.wait_time_ms) < 0 THEN w2.wait_time_ms
            ELSE (w2.wait_time_ms - w1.wait_time_ms)
        END / CAST (DATEDIFF (second, w1.collection_time, w2.collection_time) AS decimal) AS resource_wait_time_delta, 
        CASE 
            WHEN (w2.signal_wait_time_ms - w1.signal_wait_time_ms) < 0 THEN w2.signal_wait_time_ms 
            ELSE (w2.signal_wait_time_ms - w1.signal_wait_time_ms) 
        END / CAST (DATEDIFF (second, w1.collection_time, w2.collection_time) AS decimal) AS resource_signal_time_delta, 
        DATEDIFF (second, w1.collection_time, w2.collection_time) AS interval_sec, 
        w2.wait_time_ms_cumulative
    -- Self-join - w1 represents wait stats at the beginning of the sample interval, while w2 
    -- shows the wait stats at the end of the interval. 
    FROM wait_times AS w1 
    INNER JOIN wait_times AS w2 ON w1.wait_type = w2.wait_type AND w1.[rank] = w2.[rank]-1 

    UNION ALL 

    -- Treat sum of all signal waits as CPU "wait time"
    SELECT 
        CONVERT (datetime, SWITCHOFFSET (CAST (w2.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        CONVERT (datetime, SWITCHOFFSET (CAST (w2.snapshot_time AS datetimeoffset(7)), '+00:00')) AS snapshot_time, 
        w2.snapshot_time_id, 
        CONVERT (datetime, SWITCHOFFSET (CAST (w1.collection_time AS datetimeoffset(7)), '+00:00')) AS interval_start, 
        CONVERT (datetime, SWITCHOFFSET (CAST (w2.collection_time AS datetimeoffset(7)), '+00:00')) AS interval_end, 
        'CPU' AS category_name, 
        'CPU (Signal Wait)' AS wait_type, 
        0 AS waiting_tasks_count_delta, 
        -- Handle wait stats resets
        CASE 
            WHEN (SUM (w2.signal_wait_time_ms) - SUM (w1.signal_wait_time_ms)) < 0 THEN SUM (w2.signal_wait_time_ms)
            ELSE (SUM (w2.signal_wait_time_ms) - SUM (w1.signal_wait_time_ms)) 
        END / CAST (DATEDIFF (second, w1.collection_time, w2.collection_time) AS decimal) AS resource_wait_time_delta, 
        0 AS resource_signal_time_delta, 
        DATEDIFF (second, w1.collection_time, w2.collection_time) AS interval_sec, 
        w2.wait_time_ms_cumulative
    FROM wait_times AS w1
    INNER JOIN wait_times AS w2 ON w1.wait_type = w2.wait_type AND w1.[rank] = w2.[rank]-1
    -- Only return CPU stats if we were told to return the 'CPU' category or all categories
    WHERE (@category_name IS NULL OR @category_name = 'CPU')
    GROUP BY w1.collection_time, w1.collection_time, w2.collection_time, w2.snapshot_time, w2.snapshot_time_id, w2.wait_time_ms_cumulative

    UNION ALL 

    -- Get actual used CPU time from perfmon data (average across the whole snapshot_time_id interval, 
    -- and use this average for each sample time in this interval).  Note that the "% Processor Time" 
    -- counter in the "Process" object can exceed 100% (for example, it can range from 0-800% on an 
    -- 8 CPU server). 
    SELECT
        CONVERT (datetime, SWITCHOFFSET (CAST (w2.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        CONVERT (datetime, SWITCHOFFSET (CAST (w2.snapshot_time AS datetimeoffset(7)), '+00:00')) AS snapshot_time, 
        w2.snapshot_time_id, 
        CONVERT (datetime, SWITCHOFFSET (CAST (w1.collection_time AS datetimeoffset(7)), '+00:00')) AS interval_start, 
        CONVERT (datetime, SWITCHOFFSET (CAST (w2.collection_time AS datetimeoffset(7)), '+00:00')) AS interval_end, 
        'CPU' AS category_name, 
        'CPU (Consumed)' AS wait_type, 
        0 AS waiting_tasks_count_delta, 
        -- Get sqlservr %CPU usage for the perfmon sample that immediately precedes 
        -- each wait stats sample.  Multiply by 10 to convert "% CPU" to "ms CPU/sec". 
        10 * (  
            SELECT TOP 1 formatted_value
            FROM snapshots.performance_counters AS pc
            INNER JOIN core.snapshots s ON pc.snapshot_id = s.snapshot_id
            WHERE pc.performance_object_name = 'Process' AND pc.performance_counter_name = '% Processor Time' 
                AND pc.performance_instance_name = 'sqlservr'
                AND s.snapshot_time_id <= @end_snapshot_time_id
                AND s.instance_name = @instance_name
                AND pc.snapshot_id < w2.snapshot_id
            ORDER BY pc.snapshot_id DESC
        ) AS resource_wait_time_delta, 
        0 AS resource_signal_time_delta, 
        DATEDIFF (second, w1.collection_time, w2.collection_time) AS interval_sec, 
        NULL
    FROM wait_times AS w1
    INNER JOIN wait_times AS w2 ON w1.wait_type = w2.wait_type AND w1.[rank] = w2.[rank]-1
    -- Only return CPU stats if we weren't passed in a specific wait category
    WHERE (@category_name IS NULL OR @category_name = 'CPU')
    GROUP BY w1.collection_time, w2.collection_time, w2.snapshot_time, w2.snapshot_time_id, w2.snapshot_id

    ORDER BY collection_time, category_name, wait_type
    -- These trace flags are necessary for a good plan, due to the join on ascending core.snapshot PK
    OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390);
END
GO

GRANT EXECUTE ON [snapshots].[rpt_wait_stats_by_category_by_snapshot]   TO [mdw_reader]
GO


--
-- snapshots.rpt_cpu_usage
--  Returns values for System CPU usage and SQL Server CPU usage collected through perf counters.
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @start_time - (Optional) time window start (UTC)
--    @end_time - time window end (UTC)
--    @time_window_size - Number of intervals in the time window (provide if @start_time is NULL)
--    @time_interval_min - Number of minutes in each interval (provide if @start_time is NULL)
--
IF (NOT OBJECT_ID(N'snapshots.rpt_cpu_usage', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_cpu_usage] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_cpu_usage]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_cpu_usage] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_cpu_usage]
    @instance_name sysname,
    @start_time datetime = NULL,
    @end_time datetime = NULL,
    @time_window_size smallint = NULL,
    @time_interval_min smallint = NULL
AS
BEGIN
    SET NOCOUNT ON;

    -- Convert snapshot_time (datetimeoffset(7)) to a UTC datetime
    IF (@end_time IS NULL)
        SET @end_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MAX(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));

    IF (@start_time IS NULL)
    BEGIN
        -- If time_window_size and time_interval_min are set use them
        -- to determine the start time
        -- Otherwise use the earliest available snapshot_time
        IF @time_window_size IS NOT NULL AND @time_interval_min IS NOT NULL
        BEGIN
            SET @start_time = DATEADD(minute, @time_window_size * @time_interval_min * -1.0, @end_time);
        END
        ELSE
        BEGIN
            -- Convert min snapshot_time (datetimeoffset(7)) to a UTC datetime
            SET @start_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MIN(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));
        END
    END

    DECLARE @end_snapshot_time_id int;
    SELECT @end_snapshot_time_id = MAX(snapshot_time_id) FROM core.snapshots WHERE snapshot_time <= @end_time;

    DECLARE @start_snapshot_time_id int;
    SELECT @start_snapshot_time_id = MIN(snapshot_time_id) FROM core.snapshots WHERE snapshot_time >= @start_time;

    -- Determine the CPU count on the target system by querying the number of "Processor" 
    -- counter instances we captured in a perfmon sample that was captured around the same 
    -- time. 
    DECLARE @cpu_count smallint
        SELECT @cpu_count = COUNT (DISTINCT pc.performance_instance_name)
        FROM snapshots.performance_counters AS pc
        INNER JOIN core.snapshots s ON s.snapshot_id = pc.snapshot_id 
        WHERE pc.performance_object_name = 'Processor' AND pc.performance_counter_name = '% Processor Time'
        AND pc.performance_instance_name != '_Total'
        AND s.snapshot_time_id BETWEEN @start_snapshot_time_id AND @end_snapshot_time_id 
        AND s.instance_name = @instance_name
            AND pc.collection_time = 
                (SELECT TOP 1 collection_time FROM snapshots.performance_counter_values pcv2 WHERE pcv2.snapshot_id = s.snapshot_id)
    SELECT 
        CASE pc.performance_object_name
            WHEN 'Processor' THEN 'System'
            ELSE 'SQL Server'
        END AS series,
        s.snapshot_time_id, 
        CONVERT (datetime, SWITCHOFFSET (CAST (s.snapshot_time AS datetimeoffset(7)), '+00:00')) AS snapshot_time, 
        -- Processor time on an eight proc system is 0-800% for the Process object, 
        -- but 0-100% for the Processor object
        CASE pc.performance_object_name
            WHEN 'Processor' THEN pc.formatted_value 
            ELSE pc.formatted_value / @cpu_count
        END AS formatted_value
    FROM snapshots.performance_counters pc
    JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id BETWEEN @start_snapshot_time_id AND @end_snapshot_time_id
        AND 
        (
            (pc.performance_object_name = 'Processor' AND pc.performance_counter_name = '% Processor Time' AND pc.performance_instance_name = '_Total')
            OR (pc.performance_object_name = 'Process' AND pc.performance_counter_name = '% Processor Time' AND pc.performance_instance_name = 'sqlservr')
        )
    ORDER BY pc.collection_time, series
        -- These trace flags are necessary for a good plan, due to the join on ascending PK w/range filter
        OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390)
        
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_cpu_usage]                            TO [mdw_reader]
GO


--
-- snapshots.rpt_mem_usage
--  Returns values for System memory usage and SQL Server memory usage collected through perf counters.
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @start_time - (Optional) time window start (UTC)
--    @end_time - time window end (UTC)
--    @time_window_size - Number of intervals in the time window (provide if @start_time is NULL)
--    @time_interval_min - Number of minutes in each interval (provide if @start_time is NULL)
--
IF (NOT OBJECT_ID(N'snapshots.rpt_mem_usage', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_mem_usage] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_mem_usage]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_mem_usage] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_mem_usage]
    @instance_name sysname,
    @start_time datetime = NULL,
    @end_time datetime = NULL,
    @time_window_size smallint = NULL,
    @time_interval_min smallint = NULL
AS
BEGIN
    SET NOCOUNT ON;

    -- Convert snapshot_time (datetimeoffset) to a UTC datetime
    IF (@end_time IS NULL)
        SET @end_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MAX(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));

    IF (@start_time IS NULL)
    BEGIN
        -- If time_window_size and time_interval_min are set use them
        -- to determine the start time
        -- Otherwise use the earliest available snapshot_time
        IF @time_window_size IS NOT NULL AND @time_interval_min IS NOT NULL
        BEGIN
            SET @start_time = DATEADD(minute, @time_window_size * @time_interval_min * -1.0, @end_time);
        END
        ELSE
        BEGIN
            -- Convert min snapshot_time (datetimeoffset) to a UTC datetime
            SET @start_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MIN(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));
        END
    END

    DECLARE @end_snapshot_time_id int;
    SELECT @end_snapshot_time_id = MAX(snapshot_time_id) FROM core.snapshots WHERE snapshot_time <= @end_time;

    DECLARE @start_snapshot_time_id int;
    SELECT @start_snapshot_time_id = MIN(snapshot_time_id) FROM core.snapshots WHERE snapshot_time >= @start_time;

    SELECT 
        N'System' AS series,
        s.snapshot_time_id,
        CONVERT (datetime, SWITCHOFFSET (CAST (s.snapshot_time AS datetimeoffset(7)), '+00:00')) AS snapshot_time, 
        AVG(pc.formatted_value/(1024*1024)) AS formatted_value
    FROM snapshots.performance_counters pc
    JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id BETWEEN @start_snapshot_time_id AND @end_snapshot_time_id
        AND (pc.performance_object_name = 'Process' AND pc.performance_counter_name = 'Working Set' AND pc.performance_instance_name = '_Total')
             -- uncomment when defect 109313 is fixed
             --OR pc.path LIKE N'\Memory\Pool Nonpaged Bytes' 
             --OR pc.path LIKE N'\Memory\Cache Bytes')
    GROUP BY
        s.snapshot_time_id,
        s.snapshot_time
    UNION ALL 
    SELECT 
        N'SQL Server' AS series,
        s.snapshot_time_id,
        CONVERT (datetime, SWITCHOFFSET (CAST (s.snapshot_time AS datetimeoffset(7)), '+00:00')) AS snapshot_time, 
        AVG(pc.formatted_value/(1024*1024)) AS formatted_value
    FROM snapshots.performance_counters pc
    JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id BETWEEN @start_snapshot_time_id AND @end_snapshot_time_id
        AND (pc.performance_object_name = 'Process' AND pc.performance_counter_name = 'Working Set' AND pc.performance_instance_name = 'sqlservr')
    GROUP BY
        s.snapshot_time_id,
        s.snapshot_time
    ORDER BY 
        snapshot_time_id, series
    -- These trace flags are necessary for a good plan, due to the join on ascending PK w/range filter
    OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390)
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_mem_usage]                            TO [mdw_reader]
GO

--
-- snapshots.rpt_io_usage
--  Returns values for System Disk IO usage and SQL Server Disk IO usage collected through perf counters.
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @start_time - (Optional) time window start (UTC)
--    @end_time - time window end (UTC)
--    @time_window_size - Number of intervals in the time window (provide if @start_time is NULL)
--    @time_interval_min - Number of minutes in each interval (provide if @start_time is NULL)
--
IF (NOT OBJECT_ID(N'snapshots.rpt_io_usage', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_io_usage] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_io_usage]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_io_usage] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_io_usage]
    @instance_name sysname,
    @start_time datetime = NULL,
    @end_time datetime = NULL,
    @time_window_size smallint = NULL,
    @time_interval_min smallint = NULL
AS
BEGIN
    SET NOCOUNT ON;

    -- Convert snapshot_time (datetimeoffset) to a UTC datetime
    IF (@end_time IS NULL)
        SET @end_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MAX(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));

    IF (@start_time IS NULL)
    BEGIN
        -- If time_window_size and time_interval_min are set use them
        -- to determine the start time
        -- Otherwise use the earliest available snapshot_time
        IF @time_window_size IS NOT NULL AND @time_interval_min IS NOT NULL
        BEGIN
            SET @start_time = DATEADD(minute, @time_window_size * @time_interval_min * -1.0, @end_time);
        END
        ELSE
        BEGIN
            -- Convert min snapshot_time (datetimeoffset) to a UTC datetime
            SET @start_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MIN(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));
        END
    END

    DECLARE @end_snapshot_time_id int;
    SELECT @end_snapshot_time_id = MAX(snapshot_time_id) FROM core.snapshots WHERE snapshot_time <= @end_time;

    DECLARE @start_snapshot_time_id int;
    SELECT @start_snapshot_time_id = MIN(snapshot_time_id) FROM core.snapshots WHERE snapshot_time >= @start_time;

    SELECT 
        N'System' AS series,
        s.snapshot_time_id,
        CONVERT (datetime, SWITCHOFFSET (CAST (s.snapshot_time AS datetimeoffset(7)), '+00:00')) AS snapshot_time, 
        AVG(pc.formatted_value/(1024)) AS formatted_value
    FROM snapshots.performance_counters pc
    JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id BETWEEN @start_snapshot_time_id AND @end_snapshot_time_id
        AND (pc.performance_object_name = 'Process' AND pc.performance_counter_name = 'IO Read Bytes/sec' AND pc.performance_instance_name = '_Total')
             -- uncomment when defect 109313 is fixed
             --OR pc.path LIKE N'\Process(!_Total)\IO Write Bytes/sec' ESCAPE N'!')
    GROUP BY
        s.snapshot_time_id,
        s.snapshot_time
    UNION ALL 
    SELECT 
        N'SQL Server' AS series,
        s.snapshot_time_id,
        CONVERT (datetime, SWITCHOFFSET (CAST (s.snapshot_time AS datetimeoffset(7)), '+00:00')) AS snapshot_time, 
        AVG(pc.formatted_value/(1024)) AS formatted_value
    FROM snapshots.performance_counters pc
    JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id BETWEEN @start_snapshot_time_id AND @end_snapshot_time_id
        AND (pc.performance_object_name = 'Process' AND pc.performance_counter_name = 'IO Read Bytes/sec' AND pc.performance_instance_name = 'sqlservr')
             -- uncomment when defect 109313 is fixed
             --OR pc.path LIKE N'\Process(sqlservr)\IO Write Bytes/sec')
    GROUP BY
        s.snapshot_time_id,
        s.snapshot_time
    ORDER BY 
        snapshot_time_id, series
    -- These trace flags are necessary for a good plan, due to the join on ascending PK w/range filter
    OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390)
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_io_usage]                             TO [mdw_reader]
GO


--
-- snapshots.rpt_network_usage
--  Returns values for System network usage and SQL Server network usage collected through perf counters.
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @start_time - (Optional) time window start (UTC)
--    @end_time - time window end (UTC)
--    @time_window_size - Number of intervals in the time window (provide if @start_time is NULL)
--    @time_interval_min - Number of minutes in each interval (provide if @start_time is NULL)
--
IF (NOT OBJECT_ID(N'snapshots.rpt_network_usage', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_network_usage] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_network_usage]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_network_usage] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_network_usage]
    @instance_name sysname,
    @start_time datetime = NULL,
    @end_time datetime = NULL,
    @time_window_size smallint = NULL,
    @time_interval_min smallint = NULL
AS
BEGIN
    SET NOCOUNT ON;

    -- Convert snapshot_time (datetimeoffset) to a UTC datetime
    IF (@end_time IS NULL)
        SET @end_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MAX(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));

    IF (@start_time IS NULL)
    BEGIN
        -- If time_window_size and time_interval_min are set use them
        -- to determine the start time
        -- Otherwise use the earliest available snapshot_time
        IF @time_window_size IS NOT NULL AND @time_interval_min IS NOT NULL
        BEGIN
            SET @start_time = DATEADD(minute, @time_window_size * @time_interval_min * -1.0, @end_time);
        END
        ELSE
        BEGIN
            -- Convert min snapshot_time (datetimeoffset) to a UTC datetime
            SET @start_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MIN(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));
        END
    END

    DECLARE @end_snapshot_time_id int;
    SELECT @end_snapshot_time_id = MAX(snapshot_time_id) FROM core.snapshots WHERE snapshot_time <= @end_time;

    DECLARE @start_snapshot_time_id int;
    SELECT @start_snapshot_time_id = MIN(snapshot_time_id) FROM core.snapshots WHERE snapshot_time >= @start_time;

    SELECT 
        N'System' AS series,
        s.snapshot_time_id,
        CONVERT (datetime, SWITCHOFFSET (CAST (s.snapshot_time AS datetimeoffset(7)), '+00:00')) AS snapshot_time, 
        AVG(pc.formatted_value/(1024)) AS formatted_value
    FROM snapshots.performance_counters pc
    JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id BETWEEN @start_snapshot_time_id AND @end_snapshot_time_id
        -- all instances 
        AND (pc.performance_object_name = 'Network Interface' AND pc.performance_counter_name = 'Bytes Total/sec')
    GROUP BY
        s.snapshot_time_id,
        s.snapshot_time
    ORDER BY 
        snapshot_time_id
    -- These trace flags are necessary for a good plan, due to the join on ascending PK w/range filter
    OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390)
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_network_usage]                        TO [mdw_reader]
GO


--
-- snapshots.rpt_wait_stats_one_snapshot
--  Returns summary of wait stats grouped per wait category for one snapshot
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--    @category_name - (Optional) Name of wait category to filter on (all categories if NULL)
--
IF (NOT OBJECT_ID(N'snapshots.rpt_wait_stats_one_snapshot', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_wait_stats_one_snapshot] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_wait_stats_one_snapshot]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_wait_stats_one_snapshot] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_wait_stats_one_snapshot]
    @instance_name sysname,
    @snapshot_time_id int, 
    @category_name nvarchar(20) = NULL
AS
BEGIN
    SET NOCOUNT ON;

    -- Find the snapshot_id of the last wait stats data set in the following 
    -- snapshot_time_id interval
    DECLARE @current_snapshot_id int
    SELECT TOP 1 @current_snapshot_id = s.snapshot_id
    FROM core.snapshots AS s
    INNER JOIN snapshots.os_wait_stats ws ON s.snapshot_id = ws.snapshot_id
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id > @snapshot_time_id
    ORDER BY s.snapshot_time_id ASC, s.snapshot_id ASC, ws.collection_time ASC;

    -- Find the snapshot_id of the last wait stats data set in the preceding 
    -- snapshot_time_id interval
    DECLARE @previous_snapshot_id int
    SELECT TOP 1 @previous_snapshot_id = ISNULL (s.snapshot_id, 0)
    FROM core.snapshots AS s
    INNER JOIN snapshots.os_wait_stats ws ON s.snapshot_id = ws.snapshot_id
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id < @snapshot_time_id
    ORDER BY s.snapshot_time_id DESC, s.snapshot_id DESC, ws.collection_time DESC;

    -- Get wait times by waittype (plus CPU time, modeled as a waittype)
    WITH wait_times AS 
    ( 
        SELECT 
            s.snapshot_id, s.snapshot_time_id, 
            DENSE_RANK() OVER (ORDER BY collection_time) AS [rank], 
            ws.collection_time, wt.category_name, ws.wait_type, 
            ws.waiting_tasks_count, 
            ISNULL (ws.signal_wait_time_ms, 0) AS signal_wait_time_ms, 
            ISNULL (ws.wait_time_ms - ISNULL (ws.signal_wait_time_ms, 0), 0) AS wait_time_ms, 
            ws.wait_time_ms AS wait_time_ms_cumulative
        FROM snapshots.os_wait_stats AS ws
        INNER JOIN core.wait_types_categorized wt ON wt.wait_type = ws.wait_type
        INNER JOIN core.snapshots s ON ws.snapshot_id = s.snapshot_id
        WHERE s.snapshot_id BETWEEN @previous_snapshot_id AND @current_snapshot_id
            AND s.instance_name = @instance_name
            AND (wt.category_name = ISNULL (@category_name, wt.category_name))
            AND wt.ignore != 1
    )

    -- Get resource wait stats for this snapshot_time_id interval
    SELECT 
        CONVERT (datetime, SWITCHOFFSET (CAST (w2.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        w2.category_name, w2.wait_type, 
        -- All wait stats will be reset to zero by a service cycle, which will cause 
        -- (snapshot2waittime-snapshot1waittime) calculations to produce an incorrect 
        -- negative wait time for the interval.  Detect this and avoid calculating 
        -- negative wait time/wait count/signal time deltas. 
        CASE 
            WHEN (w2.waiting_tasks_count - w1.waiting_tasks_count) < 0 THEN w2.waiting_tasks_count 
            ELSE (w2.waiting_tasks_count - w1.waiting_tasks_count) 
        END AS waiting_tasks_count_delta, 
        CASE 
            WHEN (w2.wait_time_ms - w1.wait_time_ms) < 0 THEN w2.wait_time_ms
            ELSE (w2.wait_time_ms - w1.wait_time_ms)
        END / CAST (DATEDIFF (second, w1.collection_time, w2.collection_time) AS decimal) AS resource_wait_time_delta, 
        CASE 
            WHEN (w2.signal_wait_time_ms - w1.signal_wait_time_ms) < 0 THEN w2.signal_wait_time_ms 
            ELSE (w2.signal_wait_time_ms - w1.signal_wait_time_ms) 
        END / CAST (DATEDIFF (second, w1.collection_time, w2.collection_time) AS decimal) AS resource_signal_time_delta, 
        DATEDIFF (second, w1.collection_time, w2.collection_time) AS interval_sec, 
        w2.wait_time_ms_cumulative
    -- Self-join - w1 represents wait stats at the beginning of the sample interval, while w2 
    -- shows the wait stats at the end of the interval. 
    FROM wait_times AS w1 
    INNER JOIN wait_times AS w2 ON w1.wait_type = w2.wait_type AND w1.[rank] = w2.[rank]-1 

    UNION ALL 

    -- Treat sum of all signal waits as CPU "wait time"
    SELECT 
        CONVERT (datetime, SWITCHOFFSET (CAST (w2.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        'CPU' AS category_name, 
        'CPU (Signal Wait)' AS wait_type, 
        0 AS waiting_tasks_count_delta, 
        -- Handle wait stats resets
        CASE 
            WHEN (SUM (w2.signal_wait_time_ms) - SUM (w1.signal_wait_time_ms)) < 0 THEN SUM (w2.signal_wait_time_ms)
            ELSE (SUM (w2.signal_wait_time_ms) - SUM (w1.signal_wait_time_ms)) 
        END / CAST (DATEDIFF (second, w1.collection_time, w2.collection_time) AS decimal) AS resource_wait_time_delta, 
        0 AS resource_signal_time_delta, 
        DATEDIFF (second, w1.collection_time, w2.collection_time) AS interval_sec, 
        w2.wait_time_ms_cumulative
    FROM wait_times AS w1
    INNER JOIN wait_times AS w2 ON w1.wait_type = w2.wait_type AND w1.[rank] = w2.[rank]-1
    -- Only return CPU stats if we weren't passed in a specific wait category
    WHERE (@category_name IS NULL OR @category_name = 'CPU')
    GROUP BY w1.collection_time, w2.collection_time, w2.wait_time_ms_cumulative

    UNION ALL 

    -- Get actual used CPU time from perfmon data (average across the whole snapshot_time_id interval, 
    -- and use this average for each sample time in this interval).  Note that the "% Processor Time" 
    -- counter in the "Process" object can exceed 100% (for example, it can range from 0-800% on an 
    -- 8 CPU server). 
    SELECT
        CONVERT (datetime, SWITCHOFFSET (CAST (w2.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        'CPU' AS category_name, 
        'CPU (Consumed)' AS wait_type, 
        0 AS waiting_tasks_count_delta, 
        -- Get sqlservr %CPU usage for the perfmon sample that immediately precedes 
        -- each wait stats sample.  Multiply by 10 to convert "% CPU" to "ms CPU/sec". 
        10 * (  
            SELECT TOP 1 formatted_value
            FROM snapshots.performance_counters AS pc
            INNER JOIN core.snapshots s ON pc.snapshot_id = s.snapshot_id
            WHERE pc.performance_object_name = 'Process' AND pc.performance_counter_name = '% Processor Time' 
                AND pc.performance_instance_name = 'sqlservr'
                AND s.snapshot_id < @current_snapshot_id
                AND s.instance_name = @instance_name
                AND pc.snapshot_id < w2.snapshot_id
            ORDER BY pc.snapshot_id DESC
        ) AS resource_wait_time_delta, 
        0 AS resource_signal_time_delta, 
        DATEDIFF (second, w1.collection_time, w2.collection_time) AS interval_sec, 
        NULL
    FROM wait_times AS w1
    INNER JOIN wait_times AS w2 ON w1.wait_type = w2.wait_type AND w1.[rank] = w2.[rank]-1
    -- Only return CPU stats if we weren't passed in a specific wait category
    WHERE (@category_name IS NULL OR @category_name = 'CPU')
    GROUP BY w1.collection_time, w2.collection_time, w2.snapshot_id

    ORDER BY collection_time, category_name, wait_type
    -- These trace flags are necessary for a good plan, due to the join on ascending core.snapshot PK
    OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390);

END;
GO

GRANT EXECUTE ON [snapshots].[rpt_wait_stats_one_snapshot]              TO [mdw_reader]
GO

--
-- snapshots.rpt_server_activity
--  Returns values for various perf counters indicating server's activity
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @start_time - (Optional) time window start (UTC)
--    @end_time - time window end (UTC)
--    @time_window_size - Number of intervals in the time window (provide if @start_time is NULL)
--    @time_interval_min - Number of minutes in each interval (provide if @start_time is NULL)
--
IF (NOT OBJECT_ID(N'snapshots.rpt_server_activity', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_server_activity] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_server_activity]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_server_activity] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_server_activity]
    @instance_name sysname,
    @start_time datetime = NULL,
    @end_time datetime = NULL,
    @time_window_size smallint = NULL,
    @time_interval_min smallint = NULL
AS
BEGIN
    SET NOCOUNT ON;

    -- Convert snapshot_time (datetimeoffset) to a UTC datetime
    IF (@end_time IS NULL)
        SET @end_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MAX(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));

    IF (@start_time IS NULL)
    BEGIN
        -- If time_window_size and time_interval_min are set use them
        -- to determine the start time
        -- Otherwise use the earliest available snapshot_time
        IF @time_window_size IS NOT NULL AND @time_interval_min IS NOT NULL
        BEGIN
            SET @start_time = DATEADD(minute, @time_window_size * @time_interval_min * -1.0, @end_time);
        END
        ELSE
        BEGIN
            -- Convert min snapshot_time (datetimeoffset) to a UTC datetime
            SET @start_time = CONVERT (datetime, SWITCHOFFSET (CAST ((SELECT MIN(snapshot_time) FROM core.snapshots) AS datetimeoffset(7)), '+00:00'));
        END
    END

    DECLARE @end_snapshot_time_id int;
    SELECT @end_snapshot_time_id = MAX(snapshot_time_id) FROM core.snapshots WHERE snapshot_time <= @end_time;

    DECLARE @start_snapshot_time_id int;
    SELECT @start_snapshot_time_id = MIN(snapshot_time_id) FROM core.snapshots WHERE snapshot_time >= @start_time;


    SELECT 
        SUBSTRING(pc.path, CHARINDEX(N'\', pc.path, 2)+1, LEN(pc.path) - CHARINDEX(N'\', pc.path, 2)) as series,
        s.snapshot_time_id,
        CONVERT (datetime, SWITCHOFFSET (CAST (s.snapshot_time AS datetimeoffset(7)), '+00:00')) AS snapshot_time, 
        CONVERT (datetime, SWITCHOFFSET (CAST (pc.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        pc.formatted_value
    FROM snapshots.performance_counters pc
    JOIN core.snapshots s ON (s.snapshot_id = pc.snapshot_id)
    WHERE s.instance_name = @instance_name
        AND s.snapshot_time_id BETWEEN @start_snapshot_time_id AND @end_snapshot_time_id
        AND (pc.performance_object_name LIKE '%SQL%:General Statistics' OR pc.performance_object_name LIKE '%SQL%:SQL Statistics')
        AND pc.performance_counter_name IN ('Logins/sec', 'Logouts/sec', 'Batch Requests/sec', 'Transactions', 
            'User Connections', 'SQL Compilations/sec', 'SQL Re-Compilations/sec')
    ORDER BY 
        pc.collection_time, series
    -- These trace flags are necessary for a good plan, due to the join on ascending PK w/range filter
    OPTION (QUERYTRACEON 2389, QUERYTRACEON 2390)
END;
GO

GRANT EXECUTE ON [snapshots].[rpt_server_activity]                      TO [mdw_reader]
GO

--
-- snapshots.rpt_wait_stats_one_snapshot_one_category
--  Returns details of wait stats for one snapshot and one wait category
-- Parameters: 
--    @instance_name - SQL Server instance name
--    @snapshot_time_id - time window identifier (snapshot ID from core.snapshots) 
--    @category_name - Name of wait category to filter on
--
IF (NOT OBJECT_ID(N'snapshots.rpt_wait_stats_one_snapshot_one_category', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_wait_stats_one_snapshot_one_category] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_wait_stats_one_snapshot_one_category]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_wait_stats_one_snapshot_one_category] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_wait_stats_one_snapshot_one_category]
    @instance_name sysname,
    @snapshot_time_id int,
    @category_name nvarchar(20)
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Use CTE to select first all raw data that we neeed
    -- For each snapshot append a [rank] column, which later will be used to do the self-join
    WITH wait_times AS 
    (
    -- First part gets resource wait times for each wait_type,
    -- calculated as wait_time_ms - signal_wait_time_ms 
    SELECT 
        DENSE_RANK() OVER (ORDER BY ws1.collection_time) AS [rank],
        s.snapshot_time_id,
        ws1.collection_time, 
        ct.category_name,
        ev.wait_type,
        ws1.waiting_tasks_count,
        ws1.wait_time_ms - ws1.signal_wait_time_ms as resource_wait_time_ms
    FROM core.snapshots s
    JOIN snapshots.os_wait_stats ws1 on (ws1.snapshot_id = s.snapshot_id)
    JOIN core.wait_types ev on (ev.wait_type = ws1.wait_type)
    JOIN core.wait_categories ct on (ct.category_id = ev.category_id)
    WHERE ct.category_name = @category_name
      AND s.instance_name = @instance_name
      AND (s.snapshot_time_id = @snapshot_time_id OR s.snapshot_time_id = @snapshot_time_id - 1)
    )
    -- Do a self-join to calculate deltas between snapshots
    SELECT 
        CONVERT (datetime, SWITCHOFFSET (CAST (t1.collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        t1.wait_type,
        (t1.waiting_tasks_count - t2.waiting_tasks_count) AS waiting_tasks_count_delta,
        (t1.resource_wait_time_ms - ISNULL(t2.resource_wait_time_ms,0))/CAST(DATEDIFF(second, t2.collection_time, t1.collection_time) AS decimal) AS resource_wait_time_delta
    FROM wait_times t1
    LEFT OUTER JOIN wait_times t2 on (t2.rank = t1.rank-1 and t2.wait_type = t1.wait_type)
    WHERE t1.snapshot_time_id = @snapshot_time_id
    ORDER BY collection_time, wait_type
END;
GO
GRANT EXECUTE ON [snapshots].[rpt_wait_stats_one_snapshot_one_category]  TO [mdw_reader]
GO


/**********************************************************************/
/* Used by CTP6 and CTP6 Refresh/RC0 (but not RTM):                   */
/**********************************************************************/
--
-- snapshots.rpt_sql_process_memory
--  Returns details of the SQL process' memory usage
-- Parameters: 
--    @ServerName - SQL Server instance name
--    @EndTime - End of the user-selected time window (UTC)
--    @WindowSize - Number of minutes in the time window 
--
IF (NOT OBJECT_ID(N'snapshots.rpt_sql_process_memory', 'P') IS NULL)
BEGIN
    RAISERROR('Dropping procedure [snapshots].[rpt_sql_process_memory] ...', 0, 1)  WITH NOWAIT;
    DROP PROCEDURE [snapshots].[rpt_sql_process_memory]
END
GO 

RAISERROR('Creating procedure [snapshots].[rpt_sql_process_memory] ...', 0, 1)  WITH NOWAIT;
GO
CREATE PROCEDURE [snapshots].[rpt_sql_process_memory]
    @ServerName sysname,
    @EndTime datetime = NULL,
    @WindowSize int
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Divide our time window up into 40 evenly-sized time intervals, and find the last collection_time within each of these intervals
    CREATE TABLE #intervals (
        interval_time_id        int, 
        interval_start_time     datetimeoffset(7),
        interval_end_time       datetimeoffset(7),
        interval_id             int, 
        first_collection_time   datetimeoffset(7), 
        last_collection_time    datetimeoffset(7), 
        first_snapshot_id       int,
        last_snapshot_id        int, 
        source_id               int, 
        snapshot_id             int, 
        collection_time         datetimeoffset(7), 
        collection_time_id      int
    )
    -- GUID 49268954-... is the Server Activity CS
    INSERT INTO #intervals
    EXEC [snapshots].[rpt_interval_collection_times] 
        @ServerName, @EndTime, @WindowSize, 'snapshots.os_process_memory', '49268954-4FD4-4EB6-AA04-CD59D9BB5714', 40, 0

    SELECT 
        CONVERT (datetime, SWITCHOFFSET (CAST (collection_time AS datetimeoffset(7)), '+00:00')) AS collection_time, 
        CASE 
            WHEN series = 'virtual_address_space_reserved_kb' THEN 'Virtual Memory Reserved'
            WHEN series = 'virtual_address_space_committed_kb' THEN 'Virtual Memory Committed'
            WHEN series = 'physical_memory_in_use_kb' THEN 'Physical Memory In Use'
            WHEN series = 'process_physical_memory_low' THEN 'Physical Memory Low'
            WHEN series = 'process_virtual_memory_low' THEN 'Virtual Memory Low'
            ELSE series
        END AS series,
        [value] / 1024 AS [value] -- convert KB to MB
    FROM 
    (
        SELECT 
            pm.collection_time,
            pm.virtual_address_space_reserved_kb,
            pm.virtual_address_space_committed_kb,
            pm.physical_memory_in_use_kb,
            CONVERT (bigint, pm.process_physical_memory_low) AS process_physical_memory_low,
            CONVERT (bigint, pm.process_virtual_memory_low) AS process_virtual_memory_low
        FROM [snapshots].[os_process_memory] AS pm
        INNER JOIN #intervals AS i ON (i.last_snapshot_id = pm.snapshot_id AND i.last_collection_time = pm.collection_time)
    ) AS pvt
    UNPIVOT
    (
        [value] for [series] in 
            (virtual_address_space_reserved_kb, virtual_address_space_committed_kb, 
            physical_memory_in_use_kb)
    ) AS unpvt
END
GO
GRANT EXECUTE ON [snapshots].[rpt_sql_process_memory]                   TO [mdw_reader]
GO



/**********************************************************************/
/* END OF LEGACY OBJECTS                                              */
/**********************************************************************/



/**********************************************************************/
/* OPERATOR CHECK                                                     */
/**********************************************************************/

-- This trigger needs to be defined after we have created all our tables,
-- otherwise it would interfere with them
IF EXISTS (SELECT object_id FROM sys.triggers WHERE name = N'add_operator_check' AND type = N'TR')
BEGIN
    RAISERROR('Dropping database trigger [add_operator_check] ...', 0, 1)  WITH NOWAIT;
    DROP TRIGGER [add_operator_check] ON DATABASE
END
GO 

RAISERROR('Creating database trigger [add_operator_check] ...', 0, 1)  WITH NOWAIT;
GO
CREATE TRIGGER [add_operator_check]
ON DATABASE
WITH EXECUTE AS 'mdw_check_operator_admin'
FOR CREATE_TABLE 
AS 
BEGIN
    DECLARE @schema_name sysname;
    DECLARE @table_name sysname;

    -- Set options required by the rest of the code in this SP.
    SET ANSI_NULLS ON
    SET ANSI_PADDING ON
    SET ANSI_WARNINGS ON
    SET ARITHABORT ON
    SET CONCAT_NULL_YIELDS_NULL ON
    SET NUMERIC_ROUNDABORT OFF
    SET QUOTED_IDENTIFIER ON 


    SELECT @schema_name = EVENTDATA().value('(/EVENT_INSTANCE/SchemaName)[1]', 'sysname')
    IF (@schema_name = N'custom_snapshots')
    BEGIN
        SELECT @table_name = EVENTDATA().value('(/EVENT_INSTANCE/ObjectName)[1]', 'sysname')

        -- Dynamically add a constraint on the newly created table
        -- Table must have the snapshot_id column
        DECLARE @check_name sysname;
        SELECT @check_name = N'CHK_check_operator_' + CONVERT(nvarchar(36), NEWID());
        DECLARE @sql nvarchar(2000);
        SELECT @sql = N'ALTER TABLE ' + QUOTENAME(@schema_name) + N'.' + QUOTENAME(@table_name) +
                      N' ADD CONSTRAINT ' + QUOTENAME(@check_name) + ' CHECK (core.fn_check_operator(snapshot_id) = 1);';

        -- We dont expect any result set returned while executing ALTER TABLE statement in Dynamic SQL
        EXEC(@sql)
        WITH RESULT SETS NONE
        
        -- Dynamically revoke the CONTROL right on the table for mdw_writer
        -- That way mdw_writer creates the table but cannot remove it or alter it
        SELECT @sql = N'DENY ALTER ON ' + QUOTENAME(@schema_name) + N'.' + QUOTENAME(@table_name) +
                      N'TO [mdw_writer]';

        -- We dont expect any result set returned while executing DENY statement in Dynamic SQL
        EXEC(@sql)
        WITH RESULT SETS NONE
    END
END;
GO



IF EXISTS (SELECT object_id FROM sys.triggers WHERE name = N'deny_drop_table' AND type = N'TR')
BEGIN
    RAISERROR('Dropping database trigger [deny_drop_table] ...', 0, 1)  WITH NOWAIT;
    DROP TRIGGER [deny_drop_table] ON DATABASE
END
GO 

RAISERROR('Creating database trigger [deny_drop_table] ...', 0, 1)  WITH NOWAIT;
GO
CREATE TRIGGER [deny_drop_table]
ON DATABASE
FOR DROP_TABLE 
AS 
BEGIN
    -- Security check (role membership)
    IF (NOT (ISNULL(IS_MEMBER(N'mdw_admin'), 0) = 1) AND NOT (ISNULL(IS_SRVROLEMEMBER(N'sysadmin'), 0) = 1))
    BEGIN
        RAISERROR(14677, 16, -1, 'mdw_admin');
    END;
END;
GO

/**********************************************************************/
/* install system collector types                                     */
/**********************************************************************/
RAISERROR('', 0, 1)  WITH NOWAIT;
RAISERROR('Adding collector types...', 0, 1)  WITH NOWAIT;
GO

-- Install TSQL Query collector type
EXEC [core].[sp_add_collector_type] @collector_type_uid = '302e93d1-3424-4be7-aa8e-84813ecf2419'
GO

-- Install SqlTrace collector type
EXEC [core].[sp_add_collector_type] @collector_type_uid = '0E218CF8-ECB5-417B-B533-D851C0251271'
GO

-- Install Performance Counters collector type
EXEC [core].[sp_add_collector_type] @collector_type_uid = '294605dd-21de-40b2-b20f-f3e170ea1ec3'

-- Install Query Activity collectory type
EXEC [core].[sp_add_collector_type] @collector_type_uid = '14AF3C12-38E6-4155-BD29-F33E7966BA23'
GO




/**********************************************************************/
/* Data Purge job                                            */
/**********************************************************************/
RAISERROR('', 0, 1)  WITH NOWAIT;
RAISERROR('Creating purge data job...', 0, 1)  WITH NOWAIT;
GO

BEGIN TRY
    BEGIN TRANSACTION;

    DECLARE @job_owner NVARCHAR(256);
    SELECT @job_owner = SUSER_SNAME();

    DECLARE @db_name sysname
    SELECT @db_name = DB_NAME();

    DECLARE @job_name_for_purge sysname
    SELECT @job_name_for_purge = N'mdw_purge_data_' + QUOTENAME(@db_name)

    DECLARE @job_id UNIQUEIDENTIFIER; 
    SELECT @job_id = job_id FROM [msdb].[dbo].[sysjobs_view] WHERE [name] = @job_name_for_purge
    IF @job_id IS NULL
    BEGIN 
        -- Add job category if it does not exist. On Katmai server this will be already created.
        IF NOT EXISTS (SELECT name FROM msdb.dbo.syscategories WHERE name=N'Data Collector' AND category_class=1)
        BEGIN
            EXEC msdb.dbo.sp_add_category @class=N'JOB', @type=N'LOCAL', @name=N'Data Collector'
        END

        -- Add job
        EXEC msdb.dbo.sp_add_job 
                @job_name=@job_name_for_purge, 
                @enabled=1, 
                @notify_level_eventlog=0, 
                @notify_level_email=0, 
                @notify_level_netsend=0, 
                @notify_level_page=0, 
                @delete_level=0, 
                @description=N'Runs every day and removes data from Management Data Warehouse database that reached its expiration date.', 
                @category_name=N'Data Collector', 
                @owner_login_name=@job_owner,
                @job_id = @job_id OUTPUT;

        -- Add job step
        EXEC msdb.dbo.sp_add_jobstep 
                @job_id=@job_id, 
                @step_name=N'Step 1', 
                @step_id=1, 
                @cmdexec_success_code=0, 
                @on_success_action=1, 
                @on_success_step_id=0, 
                @on_fail_action=2, 
                @on_fail_step_id=0, 
                @retry_attempts=0, 
                @retry_interval=0, 
                @os_run_priority=0, 
                @subsystem=N'TSQL', 
                @command=N'exec core.sp_purge_data', 
                @database_name=@db_name, 
                @flags=4;

        -- Set the job step to start
        EXEC msdb.dbo.sp_update_job @job_id = @job_id, @start_step_id = 1;

        -- Add schedule
        EXEC msdb.dbo.sp_add_jobschedule 
                @job_id=@job_id, 
                @name=N'mdw_purge_data_schedule', 
                @enabled=1, 
                @freq_type=4, 
                @freq_interval=1, 
                @freq_subday_type=1, 
                @freq_subday_interval=0, 
                @freq_relative_interval=0, 
                @freq_recurrence_factor=0, 
                @active_start_time=20000;

        -- Set the job server
        EXEC msdb.dbo.sp_add_jobserver @job_id = @job_id, @server_name = N'(local)';
    END
    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    IF (@@TRANCOUNT > 0) 
        ROLLBACK TRANSACTION

    -- Rethrow the error
    DECLARE @ErrorMessage   NVARCHAR(4000);
    DECLARE @ErrorSeverity  INT;
    DECLARE @ErrorState     INT;
    DECLARE @ErrorNumber    INT;
    DECLARE @ErrorLine      INT;
    DECLARE @ErrorProcedure NVARCHAR(200);
    SELECT @ErrorLine = ERROR_LINE(),
           @ErrorSeverity = ERROR_SEVERITY(),
           @ErrorState = ERROR_STATE(),
           @ErrorNumber = ERROR_NUMBER(),
           @ErrorMessage = ERROR_MESSAGE(),
           @ErrorProcedure = ISNULL(ERROR_PROCEDURE(), '-');

    RAISERROR (14684, -1, -1 , @ErrorNumber, @ErrorSeverity, @ErrorState, @ErrorProcedure, @ErrorLine, @ErrorMessage);

END CATCH
GO



SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

/**********************************************************************/
/* UTILITY SCHEMAS                                                    */
/* Utility objects (in MDW) live in one of the following schemas      */
/*   [Snapshots] - stores the "live" tables (the tables loaded by DC  */
/*                 collection items                                   */
/*   [Sysutility_ucp_staging] - contains the views, SPs, functions    */
/*                 that help to transform the "live" table data into  */
/*                 the "cache" tables (see below)                     */
/*   [sysutility_ucp_core] - contains the cleansed tables, and     */
/*                 views, SPs, and functions that represent the guts  */
/*                 of the Utility data warehouse                      */
/*   [sysutility_ucp_misc] - contains other objects that don't fit    */
/*                 the schemas above. This schema currently contains  */
/*                 only two objects (both functions), and should be   */
/*                 eliminated at some point                           */
/*                                                                    */
/* In an ideal world, the UCP tables in the [snapshots] schema would  */
/* live in the [sysutility_ucp_staging] schema instead. Current DC    */
/* limitations do not allow for that                                  */
/*                                                                    */
/* The objects in the [snapshots] and [sysutility_ucp_staging] schema */
/* should be entirely private to the MDW database. Only objects in    */
/* the [sysutility_ucp_core] schema and the [sysutility_ucp_misc]  */
/* schemas should be referenced externally - either by objects in MSDB*/
/* or by Utility object model code                                    */
/**********************************************************************/
RAISERROR('', 0, 1)  WITH NOWAIT;
BEGIN
  DECLARE @sql nvarchar(128)

  -- 
  -- Unfortunately, CREATE SCHEMA needs to be the only statement in its 
  -- batch. That's why we use dynamic sql below
  --
  
  RAISERROR('Create schema [sysutility_ucp_misc]', 0, 1)  WITH NOWAIT;
  IF (SCHEMA_ID('sysutility_ucp_misc') IS NULL)
  BEGIN
    SET @sql = 'CREATE SCHEMA [sysutility_ucp_misc]'
    EXEC sp_executesql @sql
  END
  
  RAISERROR('Create schema [sysutility_ucp_staging]', 0, 1)  WITH NOWAIT;
  IF (SCHEMA_ID('sysutility_ucp_staging') IS NULL)
  BEGIN
    SET @sql = 'CREATE SCHEMA [sysutility_ucp_staging]'
    EXEC sp_executesql @sql
  END
  
  RAISERROR('Create schema [sysutility_ucp_core]', 0, 1)  WITH NOWAIT;
  IF (SCHEMA_ID('sysutility_ucp_core') IS NULL)
  BEGIN
    SET @sql = 'CREATE SCHEMA [sysutility_ucp_core]'
    EXEC sp_executesql @sql
  END
  
END
GO



-----------------------------------------------------------------------
-- View sysutility_ucp_misc.utility_objects_internal
--   Categorizes all Utility-owned objects in the MDW database.  Used, for 
--   example, to dynamically discover which tables are cache tables in order 
--   to update stats on them, or to purge data from all tables that play a 
--   given role. 
-----------------------------------------------------------------------
IF OBJECT_ID ('sysutility_ucp_misc.utility_objects_internal') IS NOT NULL
BEGIN
   RAISERROR('Dropping view sysutility_ucp_misc.utility_objects_internal', 0, 1) WITH NOWAIT;
   DROP VIEW sysutility_ucp_misc.utility_objects_internal;
END
GO

RAISERROR('Creating view sysutility_ucp_misc.utility_objects_internal', 0, 1) WITH NOWAIT;
GO

CREATE VIEW sysutility_ucp_misc.utility_objects_internal AS 
SELECT 
   SCHEMA_NAME (obj.[schema_id]) AS [object_schema], 
   obj.name AS [object_name], 
   obj.type_desc AS sql_object_type, 
   CAST (prop.value AS varchar(60)) AS utility_object_type 
FROM sys.extended_properties AS prop
INNER JOIN sys.objects AS obj ON prop.major_id = obj.[object_id]
WHERE prop.name = 'MS_UtilityObjectType';
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'MISC', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_misc', 
   @level1type = 'VIEW', @level1name = 'utility_objects_internal';
GO


-----------------------------------------------------------------------
-- Function to get max file size using growth type = KB. Parameters include file_size, max_size, growth size (all in KB)
-- The algorithm is:
--    the max file size = (max_size - ((max_size - file_size) % growth_size))
-- 
-- max_size - file_size means the remaining available free space.
-- because we grow a fixed chunk specified by growth_size, so we need to figure out
-- what would the remainder free space after the maximum growth, and then subtract that
-- from max_size.
-----------------------------------------------------------------------
IF (OBJECT_ID(N'[sysutility_ucp_misc].[fn_get_max_size_available_by_growth_type_kb]') IS NOT NULL)
BEGIN
   RAISERROR('Dropping function [sysutility_ucp_misc].[fn_get_max_size_available_by_growth_type_kb]', 0, 1)  WITH NOWAIT;
   DROP FUNCTION [sysutility_ucp_misc].[fn_get_max_size_available_by_growth_type_kb]
END
GO

RAISERROR('Creating function [sysutility_ucp_misc].[fn_get_max_size_available_by_growth_type_kb]', 0, 1)  WITH NOWAIT;
GO
CREATE FUNCTION [sysutility_ucp_misc].[fn_get_max_size_available_by_growth_type_kb](
                @file_size_kb REAL,
                @max_size_kb REAL, 
                @growth_size_kb REAL)
RETURNS REAL 
AS
BEGIN
    DECLARE @max_size_available_kb REAL;
    
    SELECT @max_size_available_kb = @file_size_kb;
    
    IF (@growth_size_kb > 0 AND @max_size_kb > @file_size_kb)
    BEGIN
        SELECT @max_size_available_kb = 
            (@max_size_kb - 
              CONVERT(REAL, CONVERT(BIGINT, @max_size_kb - @file_size_kb) % CONVERT(BIGINT, @growth_size_kb)))
    END
    
    RETURN @max_size_available_kb
END
GO

-----------------------------------------------------------------------
-- Function to get max file size using growth type = percentage. Parameters include file_size, max_size (in KB) and percentage
-- The algorithm is:
--    exp = log(  (max_size / file_size) / (1 + growth_percent / 100)  )
--    the max file size = file_size * (1 + growth_percent / 100) ^ exp
-----------------------------------------------------------------------
IF (OBJECT_ID(N'[sysutility_ucp_misc].[fn_get_max_size_available_by_growth_type_percent]') IS NOT NULL)
BEGIN
   RAISERROR('Dropping function [sysutility_ucp_misc].[fn_get_max_size_available_by_growth_type_percent]', 0, 1)  WITH NOWAIT;
   DROP FUNCTION [sysutility_ucp_misc].[fn_get_max_size_available_by_growth_type_percent]
END
GO

RAISERROR('Creating function [sysutility_ucp_misc].[fn_get_max_size_available_by_growth_type_percent]', 0, 1)  WITH NOWAIT;
GO
CREATE FUNCTION [sysutility_ucp_misc].[fn_get_max_size_available_by_growth_type_percent](
                @file_size_kb REAL,
                @max_size_kb REAL, 
                @growth_percent REAL)
RETURNS REAL 
AS
BEGIN
    DECLARE @max_size_available_kb REAL;
    DECLARE @one_plus_growth_percent REAL;
    DECLARE @exponent REAL;
    
    SELECT @max_size_available_kb = @file_size_kb;
    SELECT @one_plus_growth_percent = 1 + @growth_percent / 100;
    --- @file_size_kb > 0 is added to avoid the divided by zero exception. When a database is in the Emergency state, the size
    --- of its log file is zero. 
    IF (@growth_percent > 0 AND @max_size_kb > @file_size_kb AND @file_size_kb > 0)
    BEGIN
        SELECT @exponent = FLOOR(LOG10(@max_size_kb / @file_size_kb) / LOG10(@one_plus_growth_percent));
        SELECT @max_size_available_kb = @file_size_kb * POWER(@one_plus_growth_percent, @exponent);
    END
    
    RETURN @max_size_available_kb
END
GO

-----------------------------------------------------------------------
--  Function to get max size available depending on the file_size, its mx_size, growth and free_space_on_drive (everything in KB).
--  Algorithm used for calculating max size available on the drive:
--
--      if no auto grow then return the current file size, done.
--
--      assuming auto grow now:
--      if max size is configured and it's less than the (current file size + volume free space)
--          which means the max size boundary is in effect, then we use the max_size as the boundary.
--      if max size is not configured (unlimited growth) or it's already bigger than the remaining size,
--          which means the max size boundary is NOT in effect, then we use the (curent file size + volume free space)
--          as the boundary.
--
--      now we got the boundary size, we need to take a look the growth type:
--      if growth type = kb (0), then we simply try to fit as many chunks as possible (need to use % operation here)
--      if growth type = percent (1), then it's a little tricky, we need to find the max exp where
--          (current file size) * (1 + growth percent) ^ exp <= boundary size.
--      log/power operations involved. one we figure out the exp, we can compute the max available size.
-- 
--  NOTE: the return value is 
--      (the current file size      +   the max possible additional space the file can grow into)
-----------------------------------------------------------------------
IF (OBJECT_ID(N'[sysutility_ucp_misc].[fn_get_max_size_available]') IS NOT NULL)
BEGIN
   RAISERROR('Dropping function [sysutility_ucp_misc].[fn_get_max_size_available]', 0, 1)  WITH NOWAIT;
   DROP FUNCTION [sysutility_ucp_misc].[fn_get_max_size_available]
END
GO

RAISERROR('Creating function [sysutility_ucp_misc].[fn_get_max_size_available]', 0, 1)  WITH NOWAIT;
GO
CREATE FUNCTION [sysutility_ucp_misc].[fn_get_max_size_available](
                @file_size_kb REAL,
                @max_size_kb REAL, 
                @growth REAL,       --  growth is KB when type is not percentage, or a whole number percentage when percentage
                @smo_growth_type SMALLINT,  -- @smo_growth_type == 0 is KB growth, == 1 means percentage, or == 99 not supported to grow
                @free_space_on_drive_kb BIGINT)
RETURNS REAL 
AS
BEGIN
    DECLARE @max_size_available_kb REAL;
    DECLARE @projected_max_file_size_kb REAL;
    
    -- Be conservative and initialize total space to @file_size_kb (assume no autogrow)
    SELECT @max_size_available_kb  = @file_size_kb;
    
    -- Let projected size be the current file size + volume free space (assuming no one else is competing and its completely available for this file)
    SELECT @projected_max_file_size_kb = @file_size_kb + @free_space_on_drive_kb;

    -- No auto grow, return the configured file size
    IF (@smo_growth_type = 99)
    BEGIN
        SELECT @max_size_available_kb = @file_size_kb;
    END
    ELSE
    BEGIN
        IF (0 < @max_size_kb AND @max_size_kb < @projected_max_file_size_kb)
        BEGIN
            -- if maxsize is configured and it's less than the project space
            -- then we use the maxsize as the growth boundary.
            SELECT @max_size_available_kb =
                CASE
                    WHEN (@smo_growth_type = 1) -- percent growth
                    THEN sysutility_ucp_misc.fn_get_max_size_available_by_growth_type_percent(@file_size_kb, @max_size_kb, @growth)

                    WHEN (@smo_growth_type = 0) -- KB growth
                    THEN sysutility_ucp_misc.fn_get_max_size_available_by_growth_type_kb(@file_size_kb, @max_size_kb, @growth)
                    
                    ELSE @max_size_kb
                END
        END
        ELSE
        BEGIN
            -- either maxsize is not configured, in this case we use the project space
            -- or maxsize is bigger than the project space, and we suse the project space as well.
            SELECT @max_size_available_kb =
                CASE
                    WHEN (@smo_growth_type = 1) -- percent growth
                    THEN sysutility_ucp_misc.fn_get_max_size_available_by_growth_type_percent(@file_size_kb, @projected_max_file_size_kb, @growth)

                    WHEN (@smo_growth_type = 0) -- KB growth
                    THEN sysutility_ucp_misc.fn_get_max_size_available_by_growth_type_kb(@file_size_kb, @projected_max_file_size_kb, @growth)
                    
                    ELSE @projected_max_file_size_kb
                END
        END
    END

    -- VSTS 351411
    -- In SQL2008 and SQL2008 R2, unfortunately the support for file stream file
    -- in SMO and dmv aren't complete: it always return 0 for everything including 
    -- @file_size_kb, @max_size_kb, growth, thus walking through the code path above, 
    -- we'll return 0, then the our data file storage utilization property use this 
    -- value (as the denominator) to compute the percentage which would result 
    -- in divide by zero (DBZ).
    -- 
    -- So for that case, we simply return the @projected_max_file_size_kb to avoid DBZ. Of course
    -- there is a tiny chance @projected_max_file_size_kb is also 0 (due to volume free space is 0
    -- the volume is full!) so we'll simply return 1 (kb)
    --
    -- Note 1: to avoid comparing equality of double-typed variable to 0, check against 1 KB
    --         it wouldn't be any faster but readability is lower.
    IF (@max_size_available_kb < 1.0) 
    BEGIN
        SELECT @max_size_available_kb = @projected_max_file_size_kb;
        -- what if @projected_max_file_size_kb is still 0 (or near 0)? use 1 kb.
        IF (@max_size_available_kb < 1.0)
        BEGIN
            SELECT @max_size_available_kb = 1.0;
        END
    END
    
    RETURN @max_size_available_kb;
END
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'MISC', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_misc', 
   @level1type = 'FUNCTION', @level1name = 'fn_get_max_size_available';
GO


-------------------------------------------------------------------------
-- Create table: sysutility_ucp_batch_manifests_internal
-- This is the target table to store the manifest information for every batch collected 
-- The purpose of the manifest is to qualify the integrity of the data uploaded on the UCP.
-- The batch manifest primarily includes:
-- 1. server_instance_name: the server\instance name of the MI
-- 2. xx_row_count: row count for each of the table collected/uploaded by the utility T-SQL collection item query
-- 3. batch_time: the batch creation date-time stamp 
--
-- Note: the manifest info is stored in the form of name/value pair. This pattern is 
-- used is to minimize structural changes to the table in case there are collection 
-- queries added/removed in future that affects the manifest.
-------------------------------------------------------------------------
IF NOT EXISTS (SELECT name FROM sys.objects WHERE object_id = OBJECT_ID(N'[snapshots].[sysutility_ucp_batch_manifests_internal]') )
BEGIN
    RAISERROR('Creating table [snapshots].[sysutility_ucp_batch_manifests_internal]', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[sysutility_ucp_batch_manifests_internal]
    (
        [server_instance_name]  SYSNAME NOT NULL,
        [batch_time]            DATETIMEOFFSET(7) NOT NULL,  
        [parameter_name]        SYSNAME NOT NULL,           -- Name, representing the collection query uploading data to the live table
        [parameter_value]       SQL_VARIANT NULL,           -- Value, indicating the number of rows collected/uploaded by the respective query
        [collection_time]       DATETIMEOFFSET(7) NULL,
        [snapshot_id]           INT NULL
    ) ON [PRIMARY]

    ALTER TABLE [snapshots].[sysutility_ucp_batch_manifests_internal]  WITH CHECK ADD  CONSTRAINT [FK_sysutility_ucp_batch_manifests_internal] FOREIGN KEY([snapshot_id])
    REFERENCES [core].[snapshots_internal] ([snapshot_id])
    ON DELETE CASCADE
    ALTER TABLE [snapshots].[sysutility_ucp_batch_manifests_internal] CHECK CONSTRAINT [FK_sysutility_ucp_batch_manifests_internal]

    ALTER TABLE [snapshots].[sysutility_ucp_batch_manifests_internal]  WITH CHECK ADD  CONSTRAINT [CHK_check_operator_E4F8A95D-2C44-48B6-85BA-E78E47C7ACCE] CHECK  (([core].[fn_check_operator]([snapshot_id])=(1)))
    ALTER TABLE [snapshots].[sysutility_ucp_batch_manifests_internal] CHECK CONSTRAINT [CHK_check_operator_E4F8A95D-2C44-48B6-85BA-E78E47C7ACCE]

    -- This index is used by the caching job to identify the latest consistent batches 
    -- This index is also used by the DC purge job to seek against snapshot_id
    CREATE CLUSTERED INDEX CI_sysutility_ucp_batch_manifests_internal 
        ON [snapshots].[sysutility_ucp_batch_manifests_internal](snapshot_id)
    
    EXEC sp_addextendedproperty 
        @name = 'MS_UtilityObjectType', @value = 'LIVE', 
        @level0type = 'SCHEMA', @level0name = 'snapshots', 
        @level1type = 'TABLE', @level1name = 'sysutility_ucp_batch_manifests_internal';
END
GO


--*********************************************************************
-- Create table: consistent_batch_manifests_internal
-- This table stores the consistent batch information for the enrolled MI's.
-- The data returned by the view consistent_batch_manifests is stored 
-- in this table and is consumed by the caching job step.  
--*********************************************************************
IF (OBJECT_ID(N'[sysutility_ucp_staging].[consistent_batch_manifests_internal]', 'U') IS NULL)
BEGIN
   RAISERROR('Creating [sysutility_ucp_staging].[consistent_batch_manifests_internal] table', 0, 1)  WITH NOWAIT;
   CREATE TABLE [sysutility_ucp_staging].[consistent_batch_manifests_internal]
   (
        [server_instance_name]      SYSNAME NOT NULL,
        [batch_time]                DATETIMEOFFSET NOT NULL
        
      CONSTRAINT PK_consistent_batch_manifests_internal PRIMARY KEY CLUSTERED 
         (server_instance_name, batch_time) 
   )
   
   EXEC sp_addextendedproperty 
      @name = 'MS_UtilityObjectType', @value = 'STAGING', 
      @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_staging', 
      @level1type = 'TABLE', @level1name = 'consistent_batch_manifests_internal';
END;
GO

----------------------------------------------------------------------------
-- Table [sysutility_ucp_dac_collected_execution_statistics_internal]
--    Stores the output of sp_sysutility_instance_retrieve_dac_execution_statistics, 
--    which is executed by the Utility Data Collector collection set to retrieve DAC 
--    info, including execution statistics like the amount of CPU time being consumed 
--    by each DAC.  The execution statistics (%CPU) returned by this query are 
--    actually collected by a different SQL Agent job that runs every 15 seconds on 
--    the managed instance. 
----------------------------------------------------------------------------
IF (OBJECT_ID(N'[snapshots].[sysutility_ucp_dac_collected_execution_statistics_internal]', 'U') IS NULL)
BEGIN
   RAISERROR('Creating [snapshots].[sysutility_ucp_dac_collected_execution_statistics_internal] table', 0, 1)  WITH NOWAIT;
   CREATE TABLE [snapshots].[sysutility_ucp_dac_collected_execution_statistics_internal]
   (
      [physical_server_name] SYSNAME,
      [server_instance_name] SYSNAME,
      [dac_db] SYSNAME,
      [dac_deploy_date] DATETIME,
      [dac_description] NVARCHAR(4000) NULL,
      [dac_name] SYSNAME,
      [interval_start_time] DATETIMEOFFSET NULL,-- The first every-15-second DAC CPU collection time included in this row's time interval
      [interval_end_time] DATETIMEOFFSET NULL,  -- The last every-15-second DAC CPU collection time included in this row's time interval (typically ~15 min after interval_start_time)
      [interval_cpu_time_ms] BIGINT NULL,       -- The total amount of CPU time consumed by this DAC in the time interval
      [interval_cpu_pct] REAL NULL,             -- The average %CPU utilization for this DAC in the time interval (% is of all available CPU cycles, not just a single CPU)
      [lifetime_cpu_time_ms] BIGINT NULL,       -- The cumulative total CPU time consumed by this DAC since we started monitoring it
      [batch_time] datetimeoffset(7),           -- Start time for one execution of the Utility data collection job
      [collection_time] [datetimeoffset](7),    -- The execution time of the DC query that gathered this data from the managed instance
      [snapshot_id] [int], 
      
      -- This index is used by the caching job to copy the latest consistent batch from live to cache table
      CONSTRAINT PK_sysutility_ucp_dac_collected_execution_statistics_internal PRIMARY KEY CLUSTERED 
         (server_instance_name, batch_time, dac_name) 
   );      

    -- This index is used by the DC purge job to seek against snapshot_id
   CREATE NONCLUSTERED INDEX NCI_sysutility_ucp_dac_collected_execution_statistics_internal 
        ON [snapshots].[sysutility_ucp_dac_collected_execution_statistics_internal](snapshot_id)    

   -- Create a foreign key referencing snapshots_internal so that rows in this table will be deleted by the DC purge job
   ALTER TABLE [snapshots].[sysutility_ucp_dac_collected_execution_statistics_internal] WITH CHECK 
   ADD CONSTRAINT [fk_dac_collected_execution_statistics_internal_snapshots_internal] 
   FOREIGN KEY([snapshot_id])
   REFERENCES [core].[snapshots_internal] ([snapshot_id])
   ON DELETE CASCADE;
   
   EXEC sp_addextendedproperty 
      @name = 'MS_UtilityObjectType', @value = 'LIVE', 
      @level0type = 'SCHEMA', @level0name = 'snapshots', 
      @level1type = 'TABLE', @level1name = 'sysutility_ucp_dac_collected_execution_statistics_internal';
END;
GO

----------------------------------------------------------------------------------
-- View latest_dac_cpu_utilization
--    Gets the latest DAC-related information and CPU utilization for each DAC on a
--    managed instanced
----------------------------------------------------------------------------------
IF  EXISTS (SELECT name FROM sys.views WHERE object_id = OBJECT_ID(N'[sysutility_ucp_staging].[latest_dac_cpu_utilization]'))
BEGIN
   RAISERROR('Dropping [sysutility_ucp_staging].[latest_dac_cpu_utilization] view', 0, 1) WITH NOWAIT;
   DROP VIEW [sysutility_ucp_staging].[latest_dac_cpu_utilization]
END
GO

RAISERROR('Creating [sysutility_ucp_staging].[latest_dac_cpu_utilization] view', 0, 1) WITH NOWAIT;
GO
CREATE VIEW [sysutility_ucp_staging].[latest_dac_cpu_utilization]
AS
  SELECT physical_server_name, ds.server_instance_name, dac_db, dac_deploy_date, dac_description, dac_name, 
         lifetime_cpu_time_ms, interval_cpu_pct AS latest_cpu_pct, interval_cpu_time_ms AS latest_interval_cpu_time_ms, 
         interval_start_time AS latest_interval_start_time, interval_end_time AS latest_interval_end_time, 
         ds.batch_time,
         N'Utility[@Name=''' + CONVERT(SYSNAME, SERVERPROPERTY(N'ServerName')) + N''']/DeployedDac[@Name=''' + ds.dac_name + N''' and @ServerInstanceName=''' + ds.server_instance_name + N''']' AS urn,
         N'SQLSERVER:\Utility\'+CASE WHEN 0 = CHARINDEX(N'\', CONVERT(SYSNAME, SERVERPROPERTY(N'ServerName')), 1) THEN CONVERT(SYSNAME, SERVERPROPERTY(N'ServerName')) + N'\DEFAULT' ELSE CONVERT(SYSNAME, SERVERPROPERTY(N'ServerName')) END 
        +N'\DeployedDacs\'+msdb.dbo.fn_encode_sqlname_for_powershell(ds.dac_name+'.'+ds.server_instance_name) AS powershell_path
  FROM [snapshots].[sysutility_ucp_dac_collected_execution_statistics_internal] ds
        INNER JOIN [sysutility_ucp_staging].[consistent_batch_manifests_internal] cb
        ON ds.server_instance_name = cb.server_instance_name AND ds.batch_time = cb.batch_time
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'STAGING', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_staging', 
   @level1type = 'VIEW', @level1name = 'latest_dac_cpu_utilization';
GO


----------------------------------------------------------------------------
--- MDW Table for storing SMO server collection set information.
----------------------------------------------------------------------------

IF NOT EXISTS (SELECT name FROM .[sys].[objects] WHERE object_id = OBJECT_ID(N'[snapshots].[sysutility_ucp_smo_properties_internal]'))
BEGIN
   RAISERROR('Creating [snapshots].[sysutility_ucp_smo_properties_internal] table', 0, 1) WITH NOWAIT;
   CREATE TABLE [snapshots].[sysutility_ucp_smo_properties_internal]
   (
      [physical_server_name]  SYSNAME,
      [server_instance_name]  SYSNAME,
      [object_type]           INT,
      [urn]                   NVARCHAR(4000),
      [property_name]         NVARCHAR(128),
      [property_value]        SQL_VARIANT,
      [batch_time]            DATETIMEOFFSET(7),  -- Start time for one execution of the Utility data collection job
      [collection_time]       DATETIMEOFFSET(7) NULL,
      [snapshot_id]           INT NULL,
      -- NOTE: compression is enabled on this table at runtime (during Create UCP) in sp_initialize_mdw_internal              
   )

    -- This index is used by the caching job to copy the latest consistent batch from live to cache table
   CREATE CLUSTERED INDEX CI_sysutility_ucp_smo_properties_internal 
        ON [snapshots].[sysutility_ucp_smo_properties_internal](server_instance_name, batch_time); 

    -- This index is used by the DC purge job to seek against snapshot_id
    CREATE NONCLUSTERED INDEX NCI_sysutility_ucp_smo_properties_internal 
        ON [snapshots].[sysutility_ucp_smo_properties_internal](snapshot_id)       

   -- Create a foreign key referencing snapshots_internal so that rows in this table will be deleted by the DC purge job
   ALTER TABLE [snapshots].[sysutility_ucp_smo_properties_internal] WITH CHECK 
   ADD CONSTRAINT [FK_sysutility_ucp_smo_properties_internal_snapshots_internal] 
   FOREIGN KEY([snapshot_id])
   REFERENCES [core].[snapshots_internal] ([snapshot_id])
   ON DELETE CASCADE;

    EXEC sp_addextendedproperty 
        @name = 'MS_UtilityObjectType', @value = 'LIVE', 
        @level0type = 'SCHEMA', @level0name = 'snapshots', 
        @level1type = 'TABLE', @level1name = 'sysutility_ucp_smo_properties_internal'; 
END
GO

------------------------------------------------------------------------------
--  SQL Server View to read latest snapshot data for SMO properties
------------------------------------------------------------------------------
IF  EXISTS (SELECT name FROM sys.views WHERE object_id = OBJECT_ID(N'[sysutility_ucp_staging].[latest_smo_properties]'))
BEGIN
   RAISERROR('Dropping [sysutility_ucp_staging].[latest_smo_properties] view', 0, 1) WITH NOWAIT;
   DROP VIEW [sysutility_ucp_staging].[latest_smo_properties]
END
GO

RAISERROR('Creating [sysutility_ucp_staging].[latest_smo_properties] view', 0, 1) WITH NOWAIT;
GO
CREATE VIEW [sysutility_ucp_staging].[latest_smo_properties]
AS
   SELECT s.physical_server_name, s.server_instance_name, s.urn, s.object_type, 
          s.property_name,s.property_value, s.snapshot_id, s.batch_time
   FROM [snapshots].[sysutility_ucp_smo_properties_internal] AS s
         INNER JOIN [sysutility_ucp_staging].[consistent_batch_manifests_internal] cb
         ON s.server_instance_name = cb.server_instance_name AND s.batch_time = cb.batch_time     
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'STAGING', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_staging', 
   @level1type = 'VIEW', @level1name = 'latest_smo_properties';
GO


-----------------------------------------------------------------------
--  Created [snapshots].[sysutility_ucp_cpu_memory_configurations_internal] and [snapshots].[sysutility_ucp_cpu_affinity_internal] tables if they are not existing.
-----------------------------------------------------------------------
IF NOT EXISTS (SELECT name FROM sys.objects WHERE object_id = OBJECT_ID(N'[snapshots].[sysutility_ucp_cpu_memory_configurations_internal]'))
BEGIN
    RAISERROR('Creating table [snapshots].[sysutility_ucp_cpu_memory_configurations_internal]', 0, 1)  WITH NOWAIT;   
    CREATE TABLE [snapshots].[sysutility_ucp_cpu_memory_configurations_internal](
      [server_instance_name]     SYSNAME NOT NULL,
      [is_clustered_server]      SMALLINT NULL,
      [virtual_server_name]      SYSNAME,
      [physical_server_name]     SYSNAME NOT NULL,
      [num_processors]           INT NULL,
      [server_processor_usage]   REAL,
      [instance_processor_usage] REAL,
      [cpu_name]                 NVARCHAR(128) NULL,
      [cpu_caption]              NVARCHAR(128) NULL,
      [cpu_family]               NVARCHAR(128) NULL,
      [cpu_architecture]         NVARCHAR(64) NULL,
      [cpu_max_clock_speed]      DECIMAL(10,0) NULL,    -- this is in MHz (Mega Hertz)
      [cpu_clock_speed]          DECIMAL(10,0) NULL,
      [l2_cache_size]            DECIMAL(10,0) NULL,
      [l3_cache_size]            DECIMAL(10,0) NULL,
      [batch_time]               [datetimeoffset](7),  -- Start time for one execution of the Utility data collection job
      [collection_time]          [datetimeoffset](7) NULL,
      [snapshot_id]              [int] NULL
      
    -- This index is used by the caching job to copy the latest consistent batch from live to cache table
    CONSTRAINT PK_sysutility_cpu_memory_related_info_internal_clustered PRIMARY KEY CLUSTERED 
        ([server_instance_name], [batch_time], [physical_server_name])

    ) ON [PRIMARY];

    -- This index is used by the DC purge job to seek against snapshot_id
    CREATE NONCLUSTERED INDEX NCI_sysutility_ucp_cpu_memory_configurations_internal 
        ON [snapshots].[sysutility_ucp_cpu_memory_configurations_internal](snapshot_id)       

    ALTER TABLE [snapshots].[sysutility_ucp_cpu_memory_configurations_internal]  WITH CHECK ADD  CONSTRAINT [FK_sysutility_cpu_memory_related_info_snapshots_internal_snapshots_internal] FOREIGN KEY([snapshot_id])
    REFERENCES [core].[snapshots_internal] ([snapshot_id])
    ON DELETE CASCADE;

    ALTER TABLE [snapshots].[sysutility_ucp_cpu_memory_configurations_internal] CHECK CONSTRAINT [FK_sysutility_cpu_memory_related_info_snapshots_internal_snapshots_internal];

    ALTER TABLE [snapshots].[sysutility_ucp_cpu_memory_configurations_internal]  WITH CHECK ADD  CONSTRAINT [CHK_check_operator_9DAA9ACC-F1E1-44F8-8B74-D081734E5F39] CHECK  (([core].[fn_check_operator]([snapshot_id])=(1)));

    ALTER TABLE [snapshots].[sysutility_ucp_cpu_memory_configurations_internal] CHECK CONSTRAINT [CHK_check_operator_9DAA9ACC-F1E1-44F8-8B74-D081734E5F39];
    
    EXEC sp_addextendedproperty 
        @name = 'MS_UtilityObjectType', @value = 'LIVE', 
        @level0type = 'SCHEMA', @level0name = 'snapshots', 
        @level1type = 'TABLE', @level1name = 'sysutility_ucp_cpu_memory_configurations_internal';
END
GO

-----------------------------------------------------------------------
--  Gets the latest CPU/memory configurations for each computer - along 
--  with the current CPU utilization for the computer.
-- 
-- NOTE: If there are multiple SQL instances (MIs) on a computer, each of 
--    them uploads its snapshot of information about the computer. We 
--    only want one entry for the computer - so we pick the entry that 
--    was uploaded last
-----------------------------------------------------------------------
IF  EXISTS (SELECT name FROM sys.views WHERE object_id = OBJECT_ID(N'[sysutility_ucp_staging].[latest_computer_cpu_memory_configuration]'))
BEGIN
   RAISERROR('Dropping [sysutility_ucp_staging].[latest_computer_cpu_memory_configuration] view', 0, 1) WITH NOWAIT;
   DROP VIEW [sysutility_ucp_staging].[latest_computer_cpu_memory_configuration]
END
GO

RAISERROR('Creating view [sysutility_ucp_staging].[latest_computer_cpu_memory_configuration]', 0, 1)  WITH NOWAIT;   
GO
CREATE VIEW [sysutility_ucp_staging].[latest_computer_cpu_memory_configuration]
AS
   SELECT * 
   FROM
      (
        SELECT  
           ROW_NUMBER() OVER (PARTITION BY t.physical_server_name ORDER BY t.batch_time DESC) AS Rank,
           [virtual_server_name], [is_clustered_server],[physical_server_name], 
           [num_processors], [cpu_name], [cpu_caption], [cpu_family], [cpu_architecture], [cpu_max_clock_speed], [cpu_clock_speed], 
           [l2_cache_size], [l3_cache_size], 
           [server_processor_usage],
           t.[batch_time],
           N'Utility[@Name=''' + CONVERT(SYSNAME, SERVERPROPERTY(N'ServerName')) + N''']/Computer[@Name=''' + t.physical_server_name + N''']' AS urn,
           N'SQLSERVER:\Utility\'+CASE WHEN 0 = CHARINDEX(N'\', CONVERT(SYSNAME, SERVERPROPERTY(N'ServerName')), 1) THEN CONVERT(SYSNAME, SERVERPROPERTY(N'ServerName')) + N'\DEFAULT' ELSE @@SERVERNAME END 
          +N'\Computers\'+msdb.dbo.fn_encode_sqlname_for_powershell(t.physical_server_name) AS powershell_path
        FROM [snapshots].[sysutility_ucp_cpu_memory_configurations_internal] AS t
             INNER JOIN [sysutility_ucp_staging].[consistent_batch_manifests_internal] cb
             ON t.server_instance_name = cb.server_instance_name AND t.batch_time = cb.batch_time
      ) AS S
   WHERE S.Rank = 1
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'STAGING', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_staging', 
   @level1type = 'VIEW', @level1name = 'latest_computer_cpu_memory_configuration';
GO


-----------------------------------------------------------------------

-----------------------------------------------------------------------
IF  EXISTS (SELECT name FROM sys.views WHERE object_id = OBJECT_ID(N'[sysutility_ucp_staging].[latest_instance_cpu_utilization]'))
BEGIN
   RAISERROR('Dropping [sysutility_ucp_staging].[latest_instance_cpu_utilization] view', 0, 1) WITH NOWAIT;
   DROP VIEW [sysutility_ucp_staging].[latest_instance_cpu_utilization]
END
GO
RAISERROR('Creating view [sysutility_ucp_staging].[latest_instance_cpu_utilization]', 0, 1)  WITH NOWAIT;   
GO
CREATE VIEW [sysutility_ucp_staging].[latest_instance_cpu_utilization]
AS
   SELECT t.[server_instance_name],  
          [instance_processor_usage],
          t.[batch_time]
   FROM [snapshots].[sysutility_ucp_cpu_memory_configurations_internal] AS t
        INNER JOIN [sysutility_ucp_staging].[consistent_batch_manifests_internal] cb
        ON t.server_instance_name = cb.server_instance_name AND t.batch_time = cb.batch_time
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'STAGING', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_staging', 
   @level1type = 'VIEW', @level1name = 'latest_instance_cpu_utilization';
GO


-----------------------------------------------------------------------
--  Created [snapshots].[sysutility_ucp_volumes_internal] tables to collect volume-related
--  information from the MIs
-----------------------------------------------------------------------

IF NOT EXISTS (SELECT name FROM sys.objects WHERE object_id = OBJECT_ID(N'[snapshots].[sysutility_ucp_volumes_internal]') )
BEGIN
    RAISERROR('Creating table [snapshots].[sysutility_ucp_volumes_internal]', 0, 1)  WITH NOWAIT;
    CREATE TABLE [snapshots].[sysutility_ucp_volumes_internal](
    [server_instance_name]  SYSNAME,
    [virtual_server_name]   SYSNAME,
    [physical_server_name]  SYSNAME,
    
    [volume_device_id]      SYSNAME NOT NULL,
    [volume_name]           [nvarchar](260) NOT NULL,

    [total_space_available] [real] NULL, -- in MB
    [free_space]            [real] NULL, -- in MB
    [batch_time]            datetimeoffset(7),  -- Start time for one execution of the Utility data collection job
    [collection_time]       datetimeoffset(7) NULL,
    [snapshot_id]           [int] NULL
    
    -- This index is used by the caching job to copy the latest consistent batch from live to cache table
    CONSTRAINT PK_sysutility_volumes_info_internal PRIMARY KEY CLUSTERED
        (server_instance_name, batch_time, volume_device_id)
    
    ) ON [PRIMARY]
    
    -- This index is used by the DC purge job to seek against snapshot_id
    CREATE NONCLUSTERED INDEX NCI_sysutility_ucp_volumes_internal
        ON [snapshots].[sysutility_ucp_volumes_internal](snapshot_id) 
            
    ALTER TABLE [snapshots].[sysutility_ucp_volumes_internal]  WITH CHECK ADD  CONSTRAINT [FK_volumes_info_snapshots_internal] FOREIGN KEY([snapshot_id])
    REFERENCES [core].[snapshots_internal] ([snapshot_id])
    ON DELETE CASCADE
    ALTER TABLE [snapshots].[sysutility_ucp_volumes_internal] CHECK CONSTRAINT [FK_volumes_info_snapshots_internal]

    ALTER TABLE [snapshots].[sysutility_ucp_volumes_internal]  WITH CHECK ADD  CONSTRAINT [CHK_check_operator_D79F8519-D243-4176-8291-6F3BA8EF776D] CHECK  (([core].[fn_check_operator]([snapshot_id])=(1)))
    ALTER TABLE [snapshots].[sysutility_ucp_volumes_internal] CHECK CONSTRAINT [CHK_check_operator_D79F8519-D243-4176-8291-6F3BA8EF776D] 
    
    EXEC sp_addextendedproperty 
        @name = 'MS_UtilityObjectType', @value = 'LIVE', 
        @level0type = 'SCHEMA', @level0name = 'snapshots', 
        @level1type = 'TABLE', @level1name = 'sysutility_ucp_volumes_internal';   
END
GO


-----------------------------------------------------------------------
--  View to get latest volumes information. 
--  NOTE: If you change the output of this view in any way, be sure to also update the 
--  corresponding "stub" object in instmsdb.sql. 
--  
--  NOTE: When you have multiple SQL instances on a machine, each of them is uploading
--     volume information (as it sees it). However, we only want one entry for each
--     volume on the computer. What we do here is to simply pick the entry from the 
--     instance that uploaded last - hence the partition-by (physical_server_name,volume_device_id)
--     The "latest_computers" view exhibits very similar behavior.
-----------------------------------------------------------------------
IF  EXISTS (SELECT name FROM sys.views WHERE object_id = OBJECT_ID(N'[sysutility_ucp_staging].[latest_volumes]'))
BEGIN
   RAISERROR('Dropping view [sysutility_ucp_staging].[latest_volumes]', 0, 1)  WITH NOWAIT;
   DROP VIEW [sysutility_ucp_staging].[latest_volumes]
END
GO

RAISERROR('Creating view [sysutility_ucp_staging].[latest_volumes]', 0, 1)  WITH NOWAIT;
GO
CREATE VIEW [sysutility_ucp_staging].[latest_volumes] AS
   SELECT [virtual_server_name],
          [physical_server_name],
          [volume_device_id],
          [volume_name],
          [total_space_available],  -- in MB
          [free_space], -- in MB
          N'SQLSERVER:\Utility\'+ CASE WHEN 0 = CHARINDEX(N'\', CONVERT(SYSNAME, SERVERPROPERTY(N'ServerName')), 1) 
                                        THEN CONVERT(SYSNAME, SERVERPROPERTY(N'ServerName')) + N'\DEFAULT' 
                                        ELSE CONVERT(SYSNAME, SERVERPROPERTY(N'ServerName')) 
                                    END 
          +N'\Computers\'+msdb.dbo.fn_encode_sqlname_for_powershell(physical_server_name) 
          +N'\Volumes\'+msdb.dbo.fn_encode_sqlname_for_powershell(volume_name) AS powershell_path,
          [batch_time],
          [snapshot_id]
      FROM
      (
      SELECT 
         [virtual_server_name],
         [physical_server_name],
         [volume_device_id],
         [volume_name],
         [total_space_available],
         [free_space],
         V.[batch_time],
         [snapshot_id], 
         ROW_NUMBER() OVER (PARTITION BY physical_server_name,volume_device_id ORDER BY V.batch_time DESC) rk
      FROM [snapshots].[sysutility_ucp_volumes_internal] AS V
           INNER JOIN [sysutility_ucp_staging].[consistent_batch_manifests_internal] cb
           ON V.server_instance_name = cb.server_instance_name AND V.batch_time = cb.batch_time
      ) AS T
      WHERE T.rk = 1
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'STAGING', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_staging', 
   @level1type = 'VIEW', @level1name = 'latest_volumes';
GO


--*********************************************************************
-- Create procedure: sp_get_consistent_batches
-- This procedure gets the consistent batch information for the enrolled MI's
-- and stores it into the consistent_batch_manifests_internal table.
-- This SP is executed by the caching job to identify the consistent batches 
-- whose data needs to be copied from the live to cache tables for every run.
--
-- A batch is marked consistent if the number of records in each table uploaded 
-- match to the respective row count in the batch manifest. The evaluation of 
-- last batch (a) and second-last batch (b) for a given MI is based on following rules: 
-- . Both a and b exists and are consistent: 
--      Most recent batch (a) is returned    
-- . Either a or b exists
--      Returns a or b only if it is consistent. Possiblity of delayed upload from MI side.
-- . Both a and b exists and are inconsistent: 
--      No batch info is returned for that MI. Possiblity of failure in collection/upload on MI side.    
-- . Both a and b does not exist
--      No batch info is not returned for that MI. Aged-out data is currently not considered, VSTS #319498
--*********************************************************************
IF OBJECT_ID ('[sysutility_ucp_staging].[sp_get_consistent_batches]') IS NOT NULL 
BEGIN
    RAISERROR ('Dropping procedure [sysutility_ucp_staging].[sp_get_consistent_batches]', 0, 1) WITH NOWAIT;
    DROP PROCEDURE [sysutility_ucp_staging].[sp_get_consistent_batches]
END;
GO

RAISERROR ('Creating procedure [sysutility_ucp_staging].[sp_get_consistent_batches]', 0, 1) WITH NOWAIT;
GO

CREATE PROCEDURE [sysutility_ucp_staging].[sp_get_consistent_batches] 
AS
BEGIN
    SET NOCOUNT ON;   

    -- Note: As we are not currently caching the aged-out data, this SP
    -- clears the existing records and inserts the new data. However, as a fix for 
    -- VSTS #319498 (display aged-out data) this behavior needs to be changed 
    -- to UPSERT for any existing or new data and delete the entries whose 
    -- data is purged (> 7 days). 

    -- Get the manifest information for the latest uploaded batches.  The "manifest" info includes 
    -- the expected number of rows that should have been uploaded into each live table for the 
    -- batch.  This query captures the manifest for most recent unprocessed batch (T) and the 
    -- immediately prior (T-1) unprocessed batch from each managed instance since the last execution 
    -- of the sp_copy_live_table_data_into_cache_tables stored proc. 
    -- 
    -- This rowset is staged in a temp table b/c the query optimizer cannot accurately predict the 
    -- number of rows that qualify for "WHERE bm.snapshot_id > sp.latest_consistent_snapshot_id". 
    -- 
    -- Note: This view may fetch the last 2 batch manifest rows for a given MI. The reason for 
    -- considering two batches is to use the latest one that is consistent. If the latest one is 
    -- missing (delayed upload) or inconsistent (failed or still-in-progress upload), we will use 
    -- the second-to-last batch, assuming that it is consistent. This makes the caching job 
    -- resilient to occasional delays in the MI upload job. 
    SELECT server_instance_name
        , batch_time
        , CONVERT(INT, dac_packages_row_count) AS dac_packages_row_count
        , CONVERT(INT, cpu_memory_configurations_row_count) AS cpu_memory_configurations_row_count
        , CONVERT(INT, volumes_row_count) AS volumes_row_count
        , CONVERT(INT, smo_properties_row_count) AS smo_properties_row_count        
    INTO #batch_manifests_latest
    FROM  (SELECT bm.server_instance_name, bm.batch_time, bm.parameter_name, bm.parameter_value 
           FROM snapshots.sysutility_ucp_batch_manifests_internal bm
              , msdb.dbo.sysutility_ucp_snapshot_partitions_internal AS sp
           WHERE bm.snapshot_id > sp.latest_consistent_snapshot_id 
             -- The [time_id] = 1 partition gives us the max snapshot_id the last time that the 
             -- sp_copy_live_table_data_into_cache_tables proc was executed (previous high water 
             -- mark).  We will consider for processing any snapshots that have been uploaded 
             -- since then. 
             AND sp.time_id = 1) AS lbm
    PIVOT (MAX(parameter_value) FOR parameter_name IN (dac_packages_row_count
                                                     , cpu_memory_configurations_row_count
                                                     , volumes_row_count
                                                     , smo_properties_row_count)) pvt;
    
    -- Truncate the table
    TRUNCATE TABLE [sysutility_ucp_staging].[consistent_batch_manifests_internal];
    
    -- Get the set of latest batches that are consistent with respect to the data uploaded to 
    -- each live table.  A check is made to verify that the number of rows uploaded matches the 
    -- expected row count in that batch's manifest. 
    -- 
    -- These rowsets are staged in temp tables b/c the query optimizer cannot accurately predict 
    -- the number of rows that qualify for "HAVING COUNT(*) = bm.cpu_memory_configurations_row_count". 

    SELECT bm.server_instance_name, bm.batch_time
    INTO #dac_statistics_consistent_batches
    FROM #batch_manifests_latest bm
    -- Note: No records in DAC table doesn't mean issue with upload -- a MI with no DACs is 
    -- perfectly valid; use an outer join so that we tolerate the no-DACs case.  
    LEFT JOIN [snapshots].[sysutility_ucp_dac_collected_execution_statistics_internal] ds 
        ON bm.server_instance_name = ds.server_instance_name AND bm.batch_time = ds.batch_time    
    GROUP BY bm.server_instance_name, bm.batch_time, bm.dac_packages_row_count, ds.batch_time    
    HAVING SUM(CASE WHEN ds.batch_time IS NULL THEN 0 ELSE 1 END) = bm.dac_packages_row_count

    SELECT bm.server_instance_name, bm.batch_time 
    INTO #cpu_memory_configurations_consistent_batches
    FROM #batch_manifests_latest bm
    INNER JOIN [snapshots].[sysutility_ucp_cpu_memory_configurations_internal] cm 
        ON bm.server_instance_name = cm.server_instance_name AND bm.batch_time = cm.batch_time   
    GROUP BY bm.server_instance_name, bm.batch_time, bm.cpu_memory_configurations_row_count
    HAVING COUNT(*) = bm.cpu_memory_configurations_row_count

    SELECT bm.server_instance_name, bm.batch_time 
    INTO #volumes_consistent_batches
    FROM #batch_manifests_latest bm
    INNER JOIN [snapshots].[sysutility_ucp_volumes_internal] vo 
        ON bm.server_instance_name = vo.server_instance_name AND bm.batch_time = vo.batch_time        
    GROUP BY bm.server_instance_name, bm.batch_time, bm.volumes_row_count
    HAVING COUNT(*) = bm.volumes_row_count

    SELECT bm.server_instance_name, bm.batch_time
    INTO #smo_properties_consistent_batches
    FROM #batch_manifests_latest bm
    INNER JOIN [snapshots].[sysutility_ucp_smo_properties_internal] sp 
        ON bm.server_instance_name = sp.server_instance_name AND bm.batch_time = sp.batch_time   
    GROUP BY bm.server_instance_name, bm.batch_time, bm.smo_properties_row_count
    HAVING COUNT(*) = bm.smo_properties_row_count


    -- Insert the new consistent batch information.  A consistent batch is a batch where all of 
    -- the live tables have the expected number of rows. 
    INSERT INTO [sysutility_ucp_staging].[consistent_batch_manifests_internal]
    SELECT bm.server_instance_name
        , bm.batch_time 
    FROM
    (
        -- Fetch the latest (order by DESC) consistent batches uploaded by the MI's
        SELECT ROW_NUMBER() OVER (PARTITION BY bm.server_instance_name ORDER BY bm.batch_time DESC) AS [rank]
            , bm.server_instance_name
            , bm.batch_time
        FROM #batch_manifests_latest AS bm
        INNER JOIN #dac_statistics_consistent_batches AS ds  
            ON bm.server_instance_name = ds.server_instance_name AND bm.batch_time = ds.batch_time    
        INNER JOIN #cpu_memory_configurations_consistent_batches AS cm  
            ON bm.server_instance_name = cm.server_instance_name AND bm.batch_time = cm.batch_time   
        INNER JOIN #volumes_consistent_batches AS vo 
            ON bm.server_instance_name = vo.server_instance_name AND bm.batch_time = vo.batch_time   
        INNER JOIN #smo_properties_consistent_batches AS sp 
            ON bm.server_instance_name = sp.server_instance_name AND bm.batch_time = sp.batch_time
    ) bm        
    WHERE bm.[rank] = 1;

END
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'STAGING', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_staging', 
   @level1type = 'PROCEDURE', @level1name = 'sp_get_consistent_batches';
GO

-- Note: There is another stored procedure in the sysutility_ucp_staging schema 
-- (sp_copy_live_table_data_into_cache_tables) that is created later in this script, 
-- because it depends on the schema of some objects in the sysutility_ucp_core schema. 



/***********************************************************************/
/* Utility SCHEMA: (sysutility_ucp_core)                               */
/* Dimension Tables and Views                                          */
/*                                                                     */
/*   We currently handle the following dimensions (and hence dimension */
/* tables).                                                            */
/*  Computers  (Table: computers_internal; view: latest_computers)     */
/*  Volumes    (Table: volumes_internal; view: latest_volumes)         */
/*  Instances  (Table: smo_servers_internal; view: latest_smo_servers) */
/*  Databases  (Table: databases_internal; view: latest_databases)     */
/*  FileGroups (Table: filegroups_internal; view: latest_filegroups)   */
/*  DataFiles  (Table: datafiles_internal; view: latest_datafiles)     */
/*  LogFiles   (Table: logfiles_internal; view: latest_logfiles)       */   
/*  Dacs       (Table: dacs_internal; view: latest_dacs)               */
/*                                                                     */
/* Every dimension table is clustered on its primary key. Each table   */
/* has processing_time as the prefix of its primary. This allows for   */
/* efficient queue-like behavior - inserts at the end, purges at the   */
/* beginning, and queries typically at the end.                        */
/*                                                                     */
/* Each of these dimension tables has a corresponding view that picks  */
/* the latest consistent information for that dimension. The latest    */
/* consistent information is determined by the value of the            */
/* "latest_processing_time" column in sysutility_ucp_processing_state   */
/* table in MSDB                                                       */
/***********************************************************************/

------------------------------------------------------------------------------------------------------------
-- Table dacs_internal
--    This is technically a dimension table for DACs. 
--      (For various logisical reasons, this also contains CPU utilization information for DACs.)
--    The key of this table is (processing_time, server_instance_name, dac_name). 
--       Machine_name is a regular column, but does not need to be part of the key (server_instance_name already
--       includes the appropriate information)
------------------------------------------------------------------------------------------------------------
IF (OBJECT_ID(N'[sysutility_ucp_core].[dacs_internal]', 'U') IS NULL)
BEGIN
   RAISERROR('Creating [sysutility_ucp_core].[dacs_internal] table', 0, 1)  WITH NOWAIT;
   CREATE TABLE [sysutility_ucp_core].[dacs_internal]
   (
      -- todo (VSTS #345036): This column will be removed
      [dac_id] INT IDENTITY,
      
      [server_instance_name] SYSNAME,  -- the server-qualified instance name
      [dac_name] SYSNAME,
      [urn] NVARCHAR(4000),
      [powershell_path] NVARCHAR(4000),

      [physical_server_name] SYSNAME,
      [dac_deploy_date] DATETIME,
      [dac_description] NVARCHAR(4000) NULL,

      -- todo (VSTS #345040)
      -- This is technically a "measure" column and should not be part of this dimension table   
      [dac_percent_total_cpu_utilization] REAL,
      
      [processing_time] DATETIMEOFFSET(7), 
      [batch_time] DATETIMEOFFSET(7),

      CONSTRAINT [PK_dacs_internal] 
        PRIMARY KEY CLUSTERED (processing_time, server_instance_name, dac_name)
   )

   EXEC sp_addextendedproperty 
      @name = 'MS_UtilityObjectType', @value = 'DIMENSION', 
      @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
      @level1type = 'TABLE', @level1name = 'dacs_internal';
END
GO

------------------------------------------------------------------------------
--  SQL Server View to read latest data for all DACs from cache table
--  NOTE: If you change the output of this view in any way, be sure to also update the 
--  corresponding "stub" object in instmsdb.sql. 
------------------------------------------------------------------------------
IF  EXISTS (SELECT name FROM sys.views WHERE object_id = OBJECT_ID(N'[sysutility_ucp_core].[latest_dacs]'))
BEGIN
   RAISERROR('Dropping [sysutility_ucp_core].[latest_dacs] view', 0, 1)  WITH NOWAIT; 
   DROP VIEW [sysutility_ucp_core].[latest_dacs]
END
GO
RAISERROR('Creating [sysutility_ucp_core].[latest_dacs] view', 0, 1)  WITH NOWAIT;
GO
CREATE VIEW [sysutility_ucp_core].[latest_dacs]
AS
   SELECT 
      dac_id, 
      server_instance_name, 
      dac_name, 
      physical_server_name, 
      dac_deploy_date, 
      dac_description, 
      urn,
      powershell_path,
      processing_time,
      batch_time,
      dac_percent_total_cpu_utilization
      FROM [sysutility_ucp_core].[dacs_internal] S
      WHERE S.processing_time =  (SELECT latest_processing_time FROM [msdb].[dbo].[sysutility_ucp_processing_state_internal])
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'CORE', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
   @level1type = 'VIEW', @level1name = 'latest_dacs';
GO


-----------------------------------------------------------------------
--  
-- Dimension table: computers_internal. 
--    (For logistical reasons, also includes percent_total_cpu_consumption)
--   Key: (processing_time, physical_server_name)
--
-- NOTE: We use physical_server_name as part of the key rather than the 
--    (logical) virtual_server_name. This is to allow for scenarios with clustering
--    where we have two or more servers clustered together but with potentially
--    different configurations.
--
-----------------------------------------------------------------------
IF NOT EXISTS (SELECT name FROM sys.objects WHERE object_id = OBJECT_ID(N'[sysutility_ucp_core].[computers_internal]'))
BEGIN
   RAISERROR('Creating table [sysutility_ucp_core].[computers_internal]', 0, 1)  WITH NOWAIT;
   CREATE TABLE [sysutility_ucp_core].[computers_internal]
   (
      -- todo (VSTS #345036): This column will be removed
      [id] INT IDENTITY, 
      
      virtual_server_name SYSNAME,
      physical_server_name SYSNAME,    -- differs from virtual_server_name for clustered servers
      is_clustered_server INT,
      
      num_processors INT,
      cpu_name NVARCHAR(128),
      cpu_caption NVARCHAR(128),
      cpu_family NVARCHAR(128),
      cpu_architecture NVARCHAR(64),
      cpu_max_clock_speed DECIMAL(10),
      cpu_clock_speed DECIMAL(10),
      l2_cache_size DECIMAL(10),
      l3_cache_size DECIMAL(10),
     
      -- todo (VSTS #345040)
      -- This is technically a "measure" column and should not be part of this dimension table   
      percent_total_cpu_utilization REAL,
      
      urn NVARCHAR(4000),
      powershell_path NVARCHAR(4000),
      
      processing_time DATETIMEOFFSET(7),
      batch_time DATETIMEOFFSET(7),
      
      CONSTRAINT [PK_computers_internal] 
        PRIMARY KEY CLUSTERED (processing_time, physical_server_name)
   )

   EXEC sp_addextendedproperty 
      @name = 'MS_UtilityObjectType', @value = 'DIMENSION', 
      @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
      @level1type = 'TABLE', @level1name = 'computers_internal';
END
GO

-----------------------------------------------------------------------
-- View to select latest data from [computers_internal] dimension table.
--  NOTE: If you change the shape of this view in any way, be sure to also 
--  update the corresponding "stub" object in instmsdb.sql. 
-----------------------------------------------------------------------
IF  EXISTS (SELECT name FROM sys.views WHERE object_id = OBJECT_ID(N'[sysutility_ucp_core].[latest_computers]'))
BEGIN
   RAISERROR('Dropping [sysutility_ucp_core].[latest_computers] view', 0, 1)  WITH NOWAIT; 
   DROP VIEW [sysutility_ucp_core].[latest_computers]
END
GO
RAISERROR('Creating view [sysutility_ucp_core].[latest_computers]', 0, 1)  WITH NOWAIT;
GO
CREATE VIEW [sysutility_ucp_core].[latest_computers] AS 
  SELECT [id], 
         virtual_server_name, 
         physical_server_name, 
         is_clustered_server,
         num_processors, 
         cpu_name, 
         cpu_caption, 
         cpu_family, 
         cpu_architecture, 
         cpu_max_clock_speed, 
         cpu_clock_speed, 
         l2_cache_size, 
         l3_cache_size, 
         urn,
         powershell_path,
         processing_time,
         batch_time,
         percent_total_cpu_utilization
  FROM [sysutility_ucp_core].[computers_internal]
  WHERE processing_time = (SELECT latest_processing_time FROM [msdb].[dbo].[sysutility_ucp_processing_state_internal]);
GO 

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'CORE', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
   @level1type = 'VIEW', @level1name = 'latest_computers';
GO


-----------------------------------------------------------------------
-- Dimension table: volumes_internal
--   - PK (processing_time, physical_server_name, volume_name)
--   - also includes physical_server_name 
--
--   - Includes "measure" columns (total_space_available, free_space)
-- Created cache table [sysutility_ucp_core].[volumes_internal] for storage view. 
-----------------------------------------------------------------------
IF NOT EXISTS (SELECT name FROM sys.objects WHERE object_id = OBJECT_ID(N'[sysutility_ucp_core].[volumes_internal]') )
BEGIN
  RAISERROR('Creating table [sysutility_ucp_core].[volumes_internal]', 0, 1)  WITH NOWAIT;
  CREATE TABLE [sysutility_ucp_core].[volumes_internal]
  (
     -- todo (VSTS #345036): This column will be removed
     [ID] INT IDENTITY, 
   
     virtual_server_name SYSNAME,
     physical_server_name SYSNAME,
   
     volume_device_id SYSNAME,
     volume_name NVARCHAR(260),

     -- todo (VSTS #345040)
     -- These are technically "measure" columns and should not be part of this dimension table
     total_space_available real,  -- in MB
     free_space  real,  -- in MB

     processing_time DATETIMEOFFSET(7),
     batch_time DATETIMEOFFSET(7),
     powershell_path NVARCHAR(4000) NULL,
   
     CONSTRAINT pk_volumes_internal 
       PRIMARY KEY CLUSTERED(processing_time, physical_server_name, volume_device_id)
  )

   EXEC sp_addextendedproperty 
      @name = 'MS_UtilityObjectType', @value = 'DIMENSION', 
      @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
      @level1type = 'TABLE', @level1name = 'volumes_internal';
END
GO

-- If we are upgrading a SQL 2008 R2 MDW database, add the powershell_path column to volumes_internal for 
-- performance reasons (this column was not in the table in SQL Server 2008 R2).  See changelists 1844247 
-- and 1797832 for background. 
IF NOT EXISTS (SELECT * FROM sys.columns WHERE name = 'powershell_path' AND object_id = OBJECT_ID('sysutility_ucp_core.volumes_internal'))
BEGIN
    RAISERROR('Adding powershell_path column to [sysutility_ucp_core].[volumes_internal]', 0, 1) WITH NOWAIT;
    ALTER TABLE sysutility_ucp_core.volumes_internal ADD powershell_path NVARCHAR(4000) NULL;
END;
GO

------------------------------------------------------------------------------
--  SQL Server View to read latest information for volumes
--  NOTE: If you change the output of this view in any way, be sure to also update the 
--  corresponding "stub" object in instmsdb.sql. 
------------------------------------------------------------------------------
IF EXISTS (SELECT name FROM sys.views WHERE object_id = OBJECT_ID(N'[sysutility_ucp_core].[latest_volumes]'))
BEGIN
   RAISERROR('Dropping view [sysutility_ucp_core].[latest_volumes]', 0, 1)  WITH NOWAIT;
   DROP VIEW [sysutility_ucp_core].[latest_volumes]
END
GO
 
RAISERROR('Creating view [sysutility_ucp_core].[latest_volumes]', 0, 1)  WITH NOWAIT;
GO
CREATE VIEW [sysutility_ucp_core].[latest_volumes]
   AS
   SELECT [ID], 
          [virtual_server_name], 
          [physical_server_name], 
          [volume_device_id], 
          [volume_name], 
          [powershell_path],
          [processing_time],
          [batch_time], 
          [total_space_available], 
          (total_space_available - free_space) AS [total_space_utilized], 
          (CASE WHEN total_space_available = 0 THEN 0 ELSE (100 * (total_space_available - free_space))/total_space_available END) AS  [percent_total_space_utilization]
   FROM [sysutility_ucp_core].[volumes_internal]
   WHERE processing_time = (SELECT latest_processing_time FROM [msdb].[dbo].[sysutility_ucp_processing_state_internal])
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'CORE', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
   @level1type = 'VIEW', @level1name = 'latest_volumes';
GO

         
-- =============================================
-- Dimension Table for SQL Server Instances
-- Table: smo_servers_internal
-- Key: processing_time, server_instance_name
-- =============================================
IF(OBJECT_ID(N'[sysutility_ucp_core].[smo_servers_internal]', 'U') IS NULL)
BEGIN
    RAISERROR ('Creating table [sysutility_ucp_core].[smo_servers_internal]', 0, 1) WITH NOWAIT;
    CREATE TABLE [sysutility_ucp_core].[smo_servers_internal]
    (
        [urn] NVARCHAR(320) 
        , [powershell_path]  NVARCHAR(4000)
        , [processing_time] DATETIMEOFFSET(7)
        , [batch_time] DATETIMEOFFSET(7)

        -- SMO properties
        , [AuditLevel] SMALLINT 
        , [BackupDirectory] NVARCHAR(260) 
        , [BrowserServiceAccount] NVARCHAR(128) 
        , [BrowserStartMode] SMALLINT 
        , [BuildClrVersionString] NVARCHAR(20) 
        , [BuildNumber] INT 
        , [Collation] NVARCHAR(128) 
        , [CollationID] INT 
        , [ComparisonStyle] INT 
        , [ComputerNamePhysicalNetBIOS] NVARCHAR(128) 
        , [DefaultFile] NVARCHAR(260) 
        , [DefaultLog] NVARCHAR(260) 
        , [Edition] NVARCHAR(64) 
        , [EngineEdition] SMALLINT 
        , [ErrorLogPath] NVARCHAR(260) 
        , [FilestreamShareName] NVARCHAR(260) 
        , [InstallDataDirectory] NVARCHAR(260) 
        , [InstallSharedDirectory] NVARCHAR(260) 
        , [InstanceName] NVARCHAR(128) 
        , [IsCaseSensitive] BIT 
        , [IsClustered] BIT 
        , [IsFullTextInstalled] BIT 
        , [IsSingleUser] BIT 
        , [Language] NVARCHAR(64) 
        , [MailProfile] NVARCHAR(128) 
        , [MasterDBLogPath] NVARCHAR(260) 
        , [MasterDBPath] NVARCHAR(260) 
        , [MaxPrecision] TINYINT 
        , [Name] NVARCHAR(128)        -- This is SERVERPROPERTY('ServerName')
        , [NamedPipesEnabled] BIT 
        , [NetName] NVARCHAR(128)     -- This is SERVERPROPERTY('MachineName')
        , [NumberOfLogFiles] INT 
        , [OSVersion] NVARCHAR(32) 
        , [PerfMonMode] SMALLINT 
        , [PhysicalMemory] INT 
        , [Platform] NVARCHAR(32) 
        , [Processors] SMALLINT 
        , [ProcessorUsage] INT 
        , [Product] NVARCHAR(48) 
        , [ProductLevel] NVARCHAR(32) 
        , [ResourceVersionString] NVARCHAR(32) 
        , [RootDirectory] NVARCHAR(260) 
        , [ServerType] SMALLINT 
        , [ServiceAccount] NVARCHAR(128) 
        , [ServiceInstanceId] NVARCHAR(64) 
        , [ServiceName] NVARCHAR(64) 
        , [ServiceStartMode] SMALLINT 
        , [SqlCharSet] SMALLINT 
        , [SqlCharSetName] NVARCHAR(32) 
        , [SqlDomainGroup] NVARCHAR(260) 
        , [SqlSortOrder] SMALLINT 
        , [SqlSortOrderName] NVARCHAR(64) 
        , [Status] SMALLINT 
        , [TapeLoadWaitTime] INT 
        , [TcpEnabled] BIT 
        , [VersionMajor] INT 
        , [VersionMinor] INT 
        , [VersionString] NVARCHAR(32)
       CONSTRAINT [PK_smo_servers_internal] 
         PRIMARY KEY CLUSTERED (processing_time, [Name])
         -- NOTE: compression is enabled on this table at runtime (during Create UCP) in sp_initialize_mdw_internal
    );
    
    EXEC sp_addextendedproperty 
        @name = 'MS_UtilityObjectType', @value = 'DIMENSION', 
        @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
        @level1type = 'TABLE', @level1name = 'smo_servers_internal';
END
GO

-- =====================================================================
-- Dimension Table for databases in a SQL Instance
-- Table: databases_internal
-- Key: processing_time, server_instance_name, name
-- =====================================================================

IF(OBJECT_ID(N'[sysutility_ucp_core].[databases_internal]', 'U') IS NULL)
BEGIN
    RAISERROR ('Creating table [sysutility_ucp_core].[databases_internal]', 0, 1) WITH NOWAIT;
    CREATE TABLE [sysutility_ucp_core].[databases_internal]
    (
        [urn] NVARCHAR(512)
        , [powershell_path]  NVARCHAR(MAX)
        , [processing_time] DATETIMEOFFSET(7)
        , [batch_time] DATETIMEOFFSET(7)
        , [server_instance_name]    SYSNAME
        , [parent_urn] NVARCHAR(320)
        , [Collation] NVARCHAR(128)
        , [CompatibilityLevel] SMALLINT
        , [CreateDate] DATETIME
        , [EncryptionEnabled] BIT
        , [Name] SYSNAME
        , [RecoveryModel] SMALLINT
        , [Trustworthy]    BIT       
        , [state] TINYINT NULL
       CONSTRAINT [PK_databases_internal] 
          PRIMARY KEY CLUSTERED (processing_time, server_instance_name, [Name]) 
          -- Note: compression is enabled on this table at runtime (during Create UCP) in sp_initialize_mdw_internal
    );
    
    EXEC sp_addextendedproperty 
        @name = 'MS_UtilityObjectType', @value = 'DIMENSION', 
        @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
        @level1type = 'TABLE', @level1name = 'databases_internal';
END
GO

-- If we are upgrading a SQL 2008 R2 MDW database, add the state column to databases_internal. This column allows 
-- better handling of unavailable databases (offline, emergency mode, recovering, etc).  See changelist 1856917
-- and VSTS 624995 for background. 
IF NOT EXISTS (
    SELECT * 
    FROM sys.columns 
    WHERE name = 'state' AND object_id = OBJECT_ID('sysutility_ucp_core.databases_internal'))
BEGIN
    RAISERROR('Adding state column to [sysutility_ucp_core].[databases_internal]', 0, 1) WITH NOWAIT;
    ALTER TABLE sysutility_ucp_core.databases_internal ADD state TINYINT NULL;
END;
GO

-- Constrain the values allowed in the state column. 
IF NOT EXISTS (
    SELECT * 
    FROM sys.check_constraints 
    WHERE name = 'chk_databases_internal_state' AND parent_object_id = OBJECT_ID('sysutility_ucp_core.databases_internal'))
BEGIN
    -- Current defined states are 0 (available) and 1 (not available -- emergency mode, offline, etc)
    ALTER TABLE sysutility_ucp_core.databases_internal
    ADD CONSTRAINT chk_databases_internal_state CHECK ([state] BETWEEN 0 AND 1);
END;
GO


-- =====================================================================
-- Dimension Table for filegroups in a database (in a SQL Instance)
-- Table: filegroups_internal
-- Key: processing_time, server_instance_name, database_name, name
-- =====================================================================
IF(OBJECT_ID(N'[sysutility_ucp_core].[filegroups_internal]', 'U') IS NULL)
BEGIN
    RAISERROR ('Creating table [sysutility_ucp_core].[filegroups_internal]', 0, 1) WITH NOWAIT;
    CREATE TABLE [sysutility_ucp_core].[filegroups_internal]
    (
        [urn] NVARCHAR(780)
        , [powershell_path]  NVARCHAR(MAX)
        , [processing_time] DATETIMEOFFSET(7)
        , [batch_time] DATETIMEOFFSET(7)
        , [server_instance_name]    SYSNAME
        , [database_name] SYSNAME
        , [parent_urn] NVARCHAR(512)
                
        -- SMO Properties
        , [Name] SYSNAME
        
        , CONSTRAINT PK_filegroups_internal
            PRIMARY KEY CLUSTERED(processing_time, server_instance_name, database_name, [Name])
        -- Note: compression is enabled on this table at runtime (during Create UCP) in sp_initialize_mdw_internal
    );    
    
    EXEC sp_addextendedproperty 
        @name = 'MS_UtilityObjectType', @value = 'DIMENSION', 
        @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
        @level1type = 'TABLE', @level1name = 'filegroups_internal';
END
GO

-- =====================================================================
-- Dimension Table for datafiles in a database (in a SQL Instance)
-- Table: datafiles_internal
-- Key: processing_time, server_instance_name, database_name, filegroup_name, name
--
-- VSTS #345570: The key length of the clustered index may be larger than 900 bytes.
-- 
-- ===================================================================== 
IF(OBJECT_ID(N'[sysutility_ucp_core].[datafiles_internal]', 'U') IS NULL)
BEGIN
    RAISERROR ('Creating table [sysutility_ucp_core].[datafiles_internal]', 0, 1) WITH NOWAIT;
    CREATE TABLE [sysutility_ucp_core].[datafiles_internal]
    (
        [urn] NVARCHAR(1500)
        , [powershell_path]  NVARCHAR(MAX)
        , [processing_time] DATETIMEOFFSET(7)
        , [batch_time] DATETIMEOFFSET(7)
        , [server_instance_name]    SYSNAME
        , [database_name] SYSNAME
        , [filegroup_name] SYSNAME
        , [parent_urn] NVARCHAR(780)
        , [physical_server_name] SYSNAME

        , [volume_name] NVARCHAR(260)
        , [volume_device_id] SYSNAME
        
        -- SMO Properties  
        , [Growth]  REAL
        , [GrowthType] SMALLINT
        , [MaxSize]  REAL
        , [Name] SYSNAME
        , [Size] REAL
        , [UsedSpace]  REAL
        , [FileName] NVARCHAR(260)
        , [VolumeFreeSpace] BIGINT
        
        , CONSTRAINT PK_datafiles_internal
            PRIMARY KEY CLUSTERED (processing_time, server_instance_name, database_name, [filegroup_name], [Name])
        -- Note: compression is enabled on this table at runtime (during Create UCP) in sp_initialize_mdw_internal
     );
     
    EXEC sp_addextendedproperty 
        @name = 'MS_UtilityObjectType', @value = 'DIMENSION', 
        @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
        @level1type = 'TABLE', @level1name = 'datafiles_internal';
END
GO

-- =====================================================================
-- Dimension Table for logfiles in a database (in a SQL Instance)
-- Table: logfiles_internal
-- Key: processing_time, server_instance_name, database_name, name
-- ===================================================================== 
IF(OBJECT_ID(N'[sysutility_ucp_core].[logfiles_internal]', 'U') IS NULL)
BEGIN
    RAISERROR ('Creating table [sysutility_ucp_core].[logfiles_internal]', 0, 1) WITH NOWAIT;
    CREATE TABLE [sysutility_ucp_core].[logfiles_internal]
    (
        [urn] NVARCHAR(1500)
        , [powershell_path]  NVARCHAR(MAX)
        , [processing_time] DATETIMEOFFSET(7)
        , [batch_time] DATETIMEOFFSET(7)
        , [server_instance_name]    SYSNAME
        , [database_name]    SYSNAME
        , [parent_urn] NVARCHAR(780)
        , [physical_server_name] SYSNAME

        , [volume_name] NVARCHAR(260) 
        , [volume_device_id] SYSNAME

      -- SMO Properties
        , [Growth]  REAL
        , [GrowthType] SMALLINT
        , [MaxSize]  REAL
        , [Name] SYSNAME
        , [Size] REAL
        , [UsedSpace]  REAL
        , [FileName] NVARCHAR(260)
        , [VolumeFreeSpace] BIGINT
        
        
        , CONSTRAINT PK_logfiles_internal
            PRIMARY KEY CLUSTERED (processing_time, server_instance_name, database_name, [Name])
        -- Note: compression is enabled on this table at runtime (during Create UCP) in sp_initialize_mdw_internal
    );
    
    EXEC sp_addextendedproperty 
        @name = 'MS_UtilityObjectType', @value = 'DIMENSION', 
        @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
        @level1type = 'TABLE', @level1name = 'logfiles_internal';
END
GO

-----------------------------------------------------------------------------
--  The view which returns server properties
--  NOTE: If you change the output of this view in any way, be sure to also update the 
--  corresponding "stub" object in instmsdb.sql. 
-----------------------------------------------------------------------------
IF  EXISTS (SELECT name FROM sys.views WHERE object_id = OBJECT_ID(N'[sysutility_ucp_core].[latest_smo_servers]'))
BEGIN
   RAISERROR('Dropping [sysutility_ucp_core].[latest_smo_servers] view', 0, 1) WITH NOWAIT;
   DROP VIEW [sysutility_ucp_core].[latest_smo_servers]
END
GO

RAISERROR('Creating [sysutility_ucp_core].[latest_smo_servers] view', 0, 1) WITH NOWAIT;
GO
CREATE VIEW [sysutility_ucp_core].[latest_smo_servers]
AS

SELECT urn
, [powershell_path]
, [processing_time]
, [batch_time]
, [AuditLevel]
, [BackupDirectory]
, [BrowserServiceAccount]
, [BrowserStartMode]
, [BuildClrVersionString]
, [BuildNumber]
, [Collation]
, [CollationID]
, [ComparisonStyle]
, [ComputerNamePhysicalNetBIOS]
, [DefaultFile]
, [DefaultLog]
, [Edition]
, [EngineEdition]
, [ErrorLogPath]
, [FilestreamShareName]
, [InstallDataDirectory]
, [InstallSharedDirectory]
, [InstanceName]
, [IsCaseSensitive]
, [IsClustered]
, [IsFullTextInstalled]
, [IsSingleUser]
, [Language]
, [MailProfile]
, [MasterDBLogPath]
, [MasterDBPath]
, [MaxPrecision]
, [Name]
, [NamedPipesEnabled]
, [NetName]
, [NumberOfLogFiles]
, [OSVersion]
, [PerfMonMode]
, [PhysicalMemory]
, [Platform]
, [Processors]
, [ProcessorUsage]
, [Product]
, [ProductLevel]
, [ResourceVersionString]
, [RootDirectory]
, [ServerType]
, [ServiceAccount]
, [ServiceInstanceId]
, [ServiceName]
, [ServiceStartMode]
, [SqlCharSet]
, [SqlCharSetName]
, [SqlDomainGroup]
, [SqlSortOrder]
, [SqlSortOrderName]
, [Status]
, [TapeLoadWaitTime]
, [TcpEnabled]
, [VersionMajor]
, [VersionMinor]
, [VersionString]
FROM [sysutility_ucp_core].[smo_servers_internal] AS SI
WHERE SI.processing_time =  (SELECT latest_processing_time FROM [msdb].[dbo].[sysutility_ucp_processing_state_internal])
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'CORE', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
   @level1type = 'VIEW', @level1name = 'latest_smo_servers';
GO


------------------------------------------------------------------------------
--  SQL Server View to read latest snapshot data for SMO Database object
--  NOTE: If you change the output of this view in any way, be sure to also update the 
--  corresponding "stub" object in instmsdb.sql. 
------------------------------------------------------------------------------
IF  EXISTS (SELECT name FROM sys.views WHERE object_id = OBJECT_ID(N'[sysutility_ucp_core].[latest_databases]'))
BEGIN
   RAISERROR('Dropping [sysutility_ucp_core].[latest_databases] view', 0, 1) WITH NOWAIT;
   DROP VIEW [sysutility_ucp_core].[latest_databases]
END
GO

RAISERROR('Creating [sysutility_ucp_core].[latest_databases] view', 0, 1) WITH NOWAIT;
GO
CREATE VIEW [sysutility_ucp_core].[latest_databases]
AS
    SELECT [urn]
        , [powershell_path]
        , [processing_time]
        , [batch_time]
        , [server_instance_name]
        , [parent_urn]
        , [Collation]
        , [CompatibilityLevel]
        , [CreateDate]
        , [EncryptionEnabled]
        , [Name]
        , [RecoveryModel]
        , [Trustworthy]  
        , [state]
        FROM [sysutility_ucp_core].[databases_internal] AS SI
        WHERE SI.processing_time =  (SELECT latest_processing_time FROM [msdb].[dbo].[sysutility_ucp_processing_state_internal])
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'CORE', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
   @level1type = 'VIEW', @level1name = 'latest_databases';
GO


------------------------------------------------------------------------------
--  SQL Server View to read latest snapshot data for SMO FileGroup object
--  NOTE: If you change the output of this view in any way, be sure to also update the 
--  corresponding "stub" object in instmsdb.sql. 
------------------------------------------------------------------------------
IF  EXISTS (SELECT name FROM sys.views WHERE object_id = OBJECT_ID(N'[sysutility_ucp_core].[latest_filegroups]'))
BEGIN
   RAISERROR('Dropping [sysutility_ucp_core].[latest_filegroups] view', 0, 1) WITH NOWAIT;
   DROP VIEW [sysutility_ucp_core].[latest_filegroups]
END
GO

RAISERROR('Creating [sysutility_ucp_core].[latest_filegroups] view', 0, 1) WITH NOWAIT;
GO
CREATE VIEW [sysutility_ucp_core].[latest_filegroups]
AS
    SELECT [urn]
        , [powershell_path]
        , [processing_time]
        , [batch_time]
        , [server_instance_name]
        , [database_name]
        , [parent_urn]
        , [Name]
        FROM [sysutility_ucp_core].[filegroups_internal] AS SI
        WHERE SI.processing_time =  (SELECT latest_processing_time FROM [msdb].[dbo].[sysutility_ucp_processing_state_internal])
            -- Suppress for "not available" databases (state=1). We lack full filegroup/file hierarchy metadata for these databases. 
            AND EXISTS (
                SELECT * FROM sysutility_ucp_core.latest_databases AS db 
                WHERE db.server_instance_name = SI.server_instance_name AND db.Name = SI.database_name AND db.state = 0)
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'CORE', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
   @level1type = 'VIEW', @level1name = 'latest_filegroups';
GO


------------------------------------------------------------------------------
--  SQL Server View to read latest snapshot data for SMO DataFile object
--  NOTE: If you change the output of this view in any way, be sure to also update the 
--  corresponding "stub" object in instmsdb.sql. 
------------------------------------------------------------------------------
IF  EXISTS (SELECT name FROM sys.views WHERE object_id = OBJECT_ID(N'[sysutility_ucp_core].[latest_datafiles]'))
BEGIN
   RAISERROR('Dropping [sysutility_ucp_core].[latest_datafiles] view', 0, 1) WITH NOWAIT;
   DROP VIEW [sysutility_ucp_core].[latest_datafiles]
END
GO

RAISERROR('Creating [sysutility_ucp_core].[latest_datafiles] view', 0, 1) WITH NOWAIT;
GO
CREATE VIEW [sysutility_ucp_core].[latest_datafiles]
AS
 SELECT [urn]
        , [powershell_path]
        , [processing_time]
        , [batch_time]
        , [server_instance_name]
        , [database_name] 
        , [filegroup_name]
        , [parent_urn]
        , [physical_server_name]
        , [volume_name]
        , [volume_device_id]
        , [Growth] 
        , [GrowthType]
        , [MaxSize] 
        , [Name]
        , [Size]
        , [UsedSpace] 
        , [FileName]
        , [VolumeFreeSpace]
        , [sysutility_ucp_misc].[fn_get_max_size_available]([Size], [MaxSize], [Growth], [GrowthType], [VolumeFreeSpace]) AS [available_space]
        FROM [sysutility_ucp_core].[datafiles_internal] AS SI
        WHERE SI.processing_time =  (SELECT latest_processing_time FROM [msdb].[dbo].[sysutility_ucp_processing_state_internal])
            -- Suppress for "not available" databases (state=1). We lack full filegroup/file hierarchy metadata for these databases. 
            AND EXISTS (
                SELECT * FROM sysutility_ucp_core.latest_databases AS db 
                WHERE db.server_instance_name = SI.server_instance_name AND db.Name = SI.database_name AND db.state = 0)
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'CORE', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
   @level1type = 'VIEW', @level1name = 'latest_datafiles';
GO


------------------------------------------------------------------------------
--  SQL Server View to read latest snapshot data for SMO DataFile object
--  NOTE: If you change the output of this view in any way, be sure to also update the 
--  corresponding "stub" object in instmsdb.sql. 
------------------------------------------------------------------------------
IF  EXISTS (SELECT name FROM sys.views WHERE object_id = OBJECT_ID(N'[sysutility_ucp_core].[latest_logfiles]'))
BEGIN
   RAISERROR('Dropping [sysutility_ucp_core].[latest_logfiles] view', 0, 1) WITH NOWAIT;
   DROP VIEW [sysutility_ucp_core].[latest_logfiles]
END
GO

RAISERROR('Creating [sysutility_ucp_core].[latest_logfiles] view', 0, 1) WITH NOWAIT;
GO
CREATE VIEW [sysutility_ucp_core].[latest_logfiles]
AS
 SELECT [urn]
        , [powershell_path]
        , [processing_time]
        , [batch_time]
        , [server_instance_name]
        , [database_name] 
        , [parent_urn]
        , [physical_server_name]
        , [volume_name]
        , [volume_device_id]
        , [Growth] 
        , [GrowthType]
        , [MaxSize] 
        , [Name]
        , [Size]
        , [UsedSpace] 
        , [FileName]
        , [VolumeFreeSpace]
        , [sysutility_ucp_misc].[fn_get_max_size_available]([Size], [MaxSize], [Growth], [GrowthType], [VolumeFreeSpace]) AS [available_space]
        FROM [sysutility_ucp_core].[logfiles_internal] AS SI
        WHERE SI.processing_time =  (SELECT latest_processing_time FROM [msdb].[dbo].[sysutility_ucp_processing_state_internal])
            -- Suppress for "not available" databases (state=1). We lack full filegroup/file hierarchy metadata for these databases. 
            AND EXISTS (
                SELECT * FROM sysutility_ucp_core.latest_databases AS db 
                WHERE db.server_instance_name = SI.server_instance_name AND db.Name = SI.database_name AND db.state = 0)
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'CORE', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
   @level1type = 'VIEW', @level1name = 'latest_logfiles';
GO


/***********************************************************************/
/* Utility SCHEMA: (sysutility_ucp_core)                               */
/* Measure Tables and Views                                            */
/*                                                                     */
/*   We currently handle the following measures (and hence measure     */
/* tables).                                                            */
/*   CPU           (Table: cpu_utilization_internal)                   */
/*   Storage Space (Table: space_utilization_internal)                 */
/*                                                                     */
/* CPU information is stored for the following dimensions              */
/*   - Computers                                                       */
/*   - Instances                                                       */
/*   - Dacs                                                            */
/*                                                                     */
/* Storage space information is stored for the following dimensions    */
/*   - Computers                                                       */
/*   - Volumes                                                         */
/*   - Instances                                                       */
/*   - Databases                                                       */
/*   - FileGroups                                                      */
/*   - DataFiles                                                       */
/*   - LogFiles                                                        */
/*                                                                     */
/* Each measure table stores information at different levels of        */
/* aggregation. Currently, we support 3 levels of aggregation          */
/*   - No aggregation (i.e.) latest "detail" information               */
/*   - Hourly                                                          */
/*   - Daily                                                           */
/* We expect to add additional aggregation levels in the future        */
/*                                                                     */
/* Information for each aggregation level is stored in a different     */
/* partition of the measure table. This allows us to leverage          */
/* partition pruning (for queries), and different maintenance operations */
/* for each partition (especially wrt purging of data.                 */
/*                                                                     */
/* Within each partition, (processing_time, object_type) */
/* is the prefix of the key. This allows for inserts at the end, and   */
/* purges at the front for new data. It also ensures that information  */
/* about each object type is collocated within a given collection time */
/*                                                                     */
/***********************************************************************/
---
-- Describes the aggregation level
-- 0 = Non-aggregated; 1 = Hourly, 2 = Daily, ...
--
IF NOT EXISTS(SELECT 0 FROM sys.types t 
              WHERE t.name = N'AggregationType' AND t.schema_id = SCHEMA_ID(N'sysutility_ucp_core'))
BEGIN
  RAISERROR ('Creating type [sysutility_ucp_core].[AggregationType]', 0, 1) WITH NOWAIT;
  CREATE TYPE [sysutility_ucp_core].[AggregationType] FROM TINYINT
END
GO

-- 0 = utility
-- 1 = computer
-- 2 = volume
-- 3 = instance
-- 4 = database (also dac)
-- 5 = filegroup
-- 6 = datafile
-- 7 = logfile
IF NOT EXISTS(SELECT 0 FROM sys.types t 
              WHERE t.name = N'ObjectType' AND t.[schema_id] = SCHEMA_ID(N'sysutility_ucp_core')) 
BEGIN
  RAISERROR ('Creating type [sysutility_ucp_core].[ObjectType]', 0, 1) WITH NOWAIT;
  CREATE TYPE [sysutility_ucp_core].[ObjectType] FROM TINYINT
END
GO


-- ============================================================================
-- Measure Table: CPU Utilization information
--   Supported dimensions: Computers, Instances, Databases (DACs)
--   Supported aggregation-levels: none, hourly, daily
--
-- The object_type field describes the dimension that the current entry (row) 
-- defines. 
-- The following conditions must hold.
--   If object_type = 1, server_instance_name = NULL and database_name = NULL and physical_server_name <> NULL
--   If object type = 3, physical_server_name = NULL, database_name = NULL, server_instance_name <> NULL
--   If object_type = 4, physical_server_name = NULL, server_instance_name <> NULL, database_name <> NULL
--   No other legal combinations
-- ============================================================================
IF(OBJECT_ID(N'[sysutility_ucp_core].[cpu_utilization_internal]', N'U') IS NULL)
BEGIN
    RAISERROR ('Creating table [sysutility_ucp_core].[cpu_utilization_internal]', 0, 1) WITH NOWAIT;
    
    CREATE TABLE [sysutility_ucp_core].[cpu_utilization_internal]
    (
       [processing_time]  DATETIMEOFFSET(7) NOT NULL,
       [aggregation_type] [sysutility_ucp_core].AggregationType NOT NULL,
       [object_type]      [sysutility_ucp_core].ObjectType NOT NULL, 
       
       -- Dimension keys
       [physical_server_name] SYSNAME DEFAULT N'',
       [server_instance_name] SYSNAME DEFAULT N'',
       [database_name]        SYSNAME DEFAULT N'',
      
       -- The actual measure columns.
       percent_total_cpu_utilization REAL,
        
       -- NOTE: This index is redefined at runtime (during Create UCP) in sp_initialize_mdw_internal              
       CONSTRAINT pk_cpu_utilization_internal 
         PRIMARY KEY CLUSTERED(aggregation_type, processing_time, object_type, 
                               physical_server_name, server_instance_name, database_name) 
    )
    
    EXEC sp_addextendedproperty 
        @name = 'MS_UtilityObjectType', @value = 'MEASURE', 
        @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
        @level1type = 'TABLE', @level1name = 'cpu_utilization_internal';
END
GO

---------------------------------------------------------------------------
-- View to select information from the [cpu_utilization_internal] measure
-- table.
--  NOTE: If you change the shape of this view in any way, be sure to also 
--  update the corresponding "stub" object in instmsdb.sql. 
-----------------------------------------------------------------------
IF  EXISTS (SELECT name FROM sys.views WHERE object_id = OBJECT_ID(N'[sysutility_ucp_core].[cpu_utilization]'))
BEGIN
   RAISERROR('Dropping [sysutility_ucp_core].[cpu_utilization] view', 0, 1)  WITH NOWAIT; 
   DROP VIEW [sysutility_ucp_core].[cpu_utilization]
END
GO
RAISERROR('Creating [sysutility_ucp_core].[cpu_utilization] view', 0, 1) WITH NOWAIT;
GO
CREATE VIEW [sysutility_ucp_core].[cpu_utilization]
AS
  SELECT aggregation_type, processing_time, object_type,
         physical_server_name, server_instance_name, database_name,
         percent_total_cpu_utilization
  FROM [sysutility_ucp_core].[cpu_utilization_internal]
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'CORE', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
   @level1type = 'VIEW', @level1name = 'cpu_utilization';
GO


-- ============================================================================
-- Measure Table: Space Utilization information
--   Supported dimensions: Computers, Volumes, Instances, Databases, 
--                         Filegroups, DataFiles, LogFiles
--   Supported aggregation-levels: none, hourly, daily
--
-- The object_type field describes the dimension that the current entry (row) 
-- defines. 
-- The following conditions must hold.
--    Utility  (type=0), all dimension columns must be ''
--    Computer (type=1), virtual_server_name must be non-null; the rest must be ''
--    Volume   (type=2), virtual_server_name, volume_device_id must be non-null; rest ''
--    Instance (type=3), server_instance_name must be non-NULL, everything else must be ''
--    Database (type=4), server_instance_name, database_name must be non-null
--                          other keys must be ''
--    FileGroup(type=5), server_instance_name, datbase_name, filegroup_name must be non-NULL
--                          other keys must be ''
--    DataFile (type=6), server_instance_name, database_name, filegroup_name, dbfile_name 
--                       must be non-null. Other keys must be ''
--    LogFile  (type=7), server_instance_name, database_name, dbfile_name must be non-null
--                          other keys must be ''
--
-- IMPORTANT: Unlike the cpu_utilization measure table, the space_utilization measure
--   table uses virtual_server_name to represent a computer (and volume). This is
--   because storage is shared (potentially) across a failover-cluster-instance,
--   and we want to track history of the space usage regardless of the specific 
--   current "owner" of the storage
--
-- The space_utilization_internal table stores information about two distinct hierarchies.
-- The Utility->Computer->Volume hierarchy and the Instance->Database->FileGroup->File 
-- hiererachy. A row in the table is part of only one of these hierarchies.
--
-- There are 4 distinct measure columns we maintain in this table. Not all of them are
-- really need. See VSTS #345039
--   total_space_bytes    : the maximum amount of storage available
--   allocated_space_bytes: the currently allocated amount of storage. Typically, the
--                          same as total_space_bytes, except for files
--   used_space_bytes     : the current used up space
--   available_space_bytes: the amount of space that's available for further use. 
--                          Typically this is total_space_bytes - used_space_bytes. 
--                          Except for files, where this is more complicated
--  
-- We should be able to live with just two of these columns (total_space and used_space)
--
-- For the instance-database-... hierarchy, only used_space_bytes is rolled up. All
-- the other values are rolled up to NULL
-- For the Utility-computer-volume hierarchy, all values are rolled up.
--  
--                          
-- 
-- VSTS #345570: The key length of the clustered index may be larger than 900 bytes.
--
-- ============================================================================
IF(OBJECT_ID(N'[sysutility_ucp_core].[space_utilization_internal]', N'U') IS NULL)
BEGIN
    RAISERROR ('Creating table [sysutility_ucp_core].[space_utilization_internal]', 0, 1) WITH NOWAIT;
    CREATE TABLE [sysutility_ucp_core].[space_utilization_internal]
    (
       [processing_time] DATETIMEOFFSET(7) NOT NULL,
       [aggregation_type] [sysutility_ucp_core].AggregationType NOT NULL,
       [object_type]      [sysutility_ucp_core].ObjectType NOT NULL, 
          
       -- The dimension columns
       [virtual_server_name]  SYSNAME DEFAULT N'',
       [server_instance_name] SYSNAME DEFAULT N'',
       [volume_device_id]     SYSNAME DEFAULT N'',
       [database_name]        SYSNAME DEFAULT N'',
       [filegroup_name]       SYSNAME DEFAULT N'',
       [dbfile_name]          SYSNAME DEFAULT N'',
       
       -- todo (VSTS #345039)
       -- we don't need all 4 of the columns below. We only need used_space and available_space
       [used_space_bytes]        REAL,
       [allocated_space_bytes]   REAL,
       [total_space_bytes]       REAL,
       [available_space_bytes]   REAL,

       -- NOTE: This index is redefined at runtime (during Create UCP) in sp_initialize_mdw_internal              
       CONSTRAINT pk_storage_utilization 
         PRIMARY KEY CLUSTERED(
            aggregation_type,
            processing_time, 
            object_type, 
            virtual_server_name, 
            volume_device_id, 
            server_instance_name, 
            database_name, 
            [filegroup_name], 
            dbfile_name) 
    )
    
    EXEC sp_addextendedproperty 
        @name = 'MS_UtilityObjectType', @value = 'MEASURE', 
        @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
        @level1type = 'TABLE', @level1name = 'space_utilization_internal';
END
GO

---------------------------------------------------------------------------
-- View to select information from the [space_utilization_internal] measure
-- table.
--  NOTE: If you change the shape of this view in any way, be sure to also 
--  update the corresponding "stub" object in instmsdb.sql. 
-----------------------------------------------------------------------
IF  EXISTS (SELECT name FROM sys.views WHERE object_id = OBJECT_ID(N'[sysutility_ucp_core].[space_utilization]'))
BEGIN
   RAISERROR('Dropping [sysutility_ucp_core].[space_utilization] view', 0, 1)  WITH NOWAIT; 
   DROP VIEW [sysutility_ucp_core].[space_utilization]
END
GO

RAISERROR('Creating [sysutility_ucp_core].[space_utilization] view', 0, 1) WITH NOWAIT;
GO
CREATE VIEW [sysutility_ucp_core].[space_utilization]
AS
  SELECT aggregation_type, 
         processing_time, 
         object_type,
         virtual_server_name, 
         volume_device_id, 
         server_instance_name, 
         database_name, 
         [filegroup_name], 
         dbfile_name,
         total_space_bytes, 
         allocated_space_bytes, 
         used_space_bytes, 
         available_space_bytes
  FROM [sysutility_ucp_core].[space_utilization_internal]
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'CORE', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
   @level1type = 'VIEW', @level1name = 'space_utilization';
GO


-----------------------------------------------------------------------------------------
-- Procedure sp_copy_live_table_data_into_cache_tables
--   Copies the latest snapshot of data from the "live" tables into the "cache" tables 
-----------------------------------------------------------------------------------------
IF OBJECT_ID ('sysutility_ucp_staging.sp_copy_live_table_data_into_cache_tables') IS NOT NULL
BEGIN
   RAISERROR('Dropping procedure sysutility_ucp_staging.sp_copy_live_table_data_into_cache_tables procedure', 0, 1) WITH NOWAIT;
   DROP PROCEDURE sysutility_ucp_staging.sp_copy_live_table_data_into_cache_tables;
END
GO
RAISERROR('Creating procedure sysutility_ucp_staging.sp_copy_live_table_data_into_cache_tables', 0, 1) WITH NOWAIT;
GO
CREATE PROCEDURE sysutility_ucp_staging.sp_copy_live_table_data_into_cache_tables
AS
BEGIN
      SET NOCOUNT ON;
      -- Snapshot isolation prevents the nightly purge jobs that delete much older data from blocking us. 
      SET TRANSACTION ISOLATION LEVEL SNAPSHOT; 

      DECLARE @max_snapshot_id INT, @num_snapshots_partitions INT
      
      SELECT @max_snapshot_id = ISNULL(MAX(snapshot_id),0) FROM [core].[snapshots]
      SELECT @num_snapshots_partitions = COUNT(*) FROM [msdb].[dbo].[sysutility_ucp_snapshot_partitions_internal]
      DECLARE @task_start_time DATETIME = GETUTCDATE();
      DECLARE @task_elapsed_ms INT;
      DECLARE @row_count INT;
      
        -- Initialize the snapshot partitions to default (0)
        IF(@num_snapshots_partitions = 0)
        BEGIN
            INSERT INTO [msdb].[dbo].[sysutility_ucp_snapshot_partitions_internal] VALUES (2, 0)
            INSERT INTO [msdb].[dbo].[sysutility_ucp_snapshot_partitions_internal] VALUES (1, 0)
            INSERT INTO [msdb].[dbo].[sysutility_ucp_snapshot_partitions_internal] VALUES (0, 0)
        END   
                
        DECLARE @processing_time_current DATETIMEOFFSET(7) = SYSDATETIMEOFFSET();

         --
         -- Stage 0:
         --  Identify the batches that were recently uploaded and are consistent 
         --  Data belonging to these batches will be copied from live to cache table.	  
         EXEC [sysutility_ucp_staging].[sp_get_consistent_batches]
         SET @task_elapsed_ms = DATEDIFF (ms, @task_start_time, GETUTCDATE());
         RAISERROR ('sp_get_consistent_batches: %d ms', 0, 1, @task_elapsed_ms);
         SET @task_start_time = GETUTCDATE();

         ----- 
         ----- Stage 1: Insert into all the dimension tables 
         -----          computers_internal, dacs_internal, volumes_internal,
         -----          smo_servers_internal, databases_internal, filegroups_internal, 
         -----          datafiles_internal, logfiles_internal
         -----   (Then move to Stage 2: the "measure" tables)
         -----

         -- A note about the expression used for [batch_time] in the INSERT queries, below: 
         -- 
         -- We want to expose batch_time w/a UCP-local time zone so it can be exposed as a UCP-local datetime 
         -- in the GUI (the GUI consumes directly from the enumerator).  The batch_time values in each of the 
         -- queries below have their time zone offset switched to produce a datetimeoffset with the local UCP 
         -- server's time zone offset.  
         -- 
         -- This works well except when the server's time zone offset has changed since the [batch_time] was 
         -- generated due to a Daylight Saving Time change.  (Unfortunately, there is no way in T-SQL to 
         -- determine what the server's time zone offset was at some arbitrary point in the past.)  The risk 
         -- of this is low since we generally do this processing within 15 minutes of timestamp generation.  
         -- This doesn't actually result in truly incorrect batch_times that would affect data processing 
         -- since the UTC time value that underlies every datetimeoffset is unchanged when you switch the 
         -- value's time zone offset. 
         
         --
         -- Insert into the "computers" dimension table
         --
         INSERT INTO [sysutility_ucp_core].[computers_internal] (
               virtual_server_name, 
               is_clustered_server,
               physical_server_name,
               num_processors, 
               cpu_name, cpu_caption, cpu_family, cpu_architecture, cpu_max_clock_speed, cpu_clock_speed, 
               l2_cache_size, l3_cache_size,
               percent_total_cpu_utilization,
               batch_time, processing_time,
               urn, powershell_path)
            SELECT virtual_server_name, is_clustered_server, physical_server_name, 
                   num_processors, 
                   cpu_name, cpu_caption, cpu_family, cpu_architecture, cpu_max_clock_speed, cpu_clock_speed,
                   l2_cache_size, l3_cache_size, 
                   server_processor_usage,
                   SWITCHOFFSET (batch_time, DATENAME(TZoffset, SYSDATETIMEOFFSET())) AS batch_time, 
                   @processing_time_current AS processing_time, 
                   urn, powershell_path
            FROM [sysutility_ucp_staging].[latest_computer_cpu_memory_configuration]
          SET @row_count = @@ROWCOUNT;
          SET @task_elapsed_ms = DATEDIFF (ms, @task_start_time, GETUTCDATE());
          RAISERROR ('Insert into [computers_internal]: %d rows, %d ms', 0, 1, @row_count, @task_elapsed_ms);
          SET @task_start_time = GETUTCDATE();
           
          --
          -- Insert into the "dacs_internal" dimension-table
          --
          INSERT INTO [sysutility_ucp_core].[dacs_internal] (
                 server_instance_name, dac_name, 
                 physical_server_name, dac_deploy_date, dac_description,
                 dac_percent_total_cpu_utilization, 
                 batch_time, processing_time,
                 urn, powershell_path)
           SELECT server_instance_name, dac_name,
                  physical_server_name, dac_deploy_date, dac_description,
                  latest_cpu_pct, 
                  SWITCHOFFSET (batch_time, DATENAME(TZoffset, SYSDATETIMEOFFSET())) AS batch_time, 
                  @processing_time_current AS processing_time, 
                  urn, powershell_path
           FROM [sysutility_ucp_staging].[latest_dac_cpu_utilization] 
          SET @row_count = @@ROWCOUNT;
          SET @task_elapsed_ms = DATEDIFF (ms, @task_start_time, GETUTCDATE());
          RAISERROR ('Insert into [dacs_internal]: %d rows, %d ms', 0, 1, @row_count, @task_elapsed_ms);
          SET @task_start_time = GETUTCDATE();
           
         --- Insert into the volumes_internal dimension table  
         INSERT INTO [sysutility_ucp_core].[volumes_internal] (
                virtual_server_name, physical_server_name, volume_device_id, volume_name,
                total_space_available, free_space, powershell_path,
                batch_time, processing_time)
           SELECT virtual_server_name, physical_server_name, volume_device_id, volume_name,
                  total_space_available, free_space, powershell_path,
                  SWITCHOFFSET (batch_time, DATENAME(TZoffset, SYSDATETIMEOFFSET())) AS batch_time, 
                  @processing_time_current AS processing_time
           FROM [sysutility_ucp_staging].[latest_volumes]
         SET @row_count = @@ROWCOUNT;
         SET @task_elapsed_ms = DATEDIFF (ms, @task_start_time, GETUTCDATE());
         RAISERROR ('Insert into [volumes_internal]: %d rows, %d ms', 0, 1, @row_count, @task_elapsed_ms);
         SET @task_start_time = GETUTCDATE();
           
         INSERT INTO [sysutility_ucp_core].[smo_servers_internal]
         (
            [urn]  
            , [powershell_path]
            , [processing_time] 
            , [batch_time]
            , [AuditLevel]  
            , [BackupDirectory]  
            , [BrowserServiceAccount]  
            , [BrowserStartMode]  
            , [BuildClrVersionString]  
            , [BuildNumber]  
            , [Collation]  
            , [CollationID]  
            , [ComparisonStyle]  
            , [ComputerNamePhysicalNetBIOS]  
            , [DefaultFile]  
            , [DefaultLog]  
            , [Edition]  
            , [EngineEdition]  
            , [ErrorLogPath]  
            , [FilestreamShareName]  
            , [InstallDataDirectory]  
            , [InstallSharedDirectory]  
            , [InstanceName]  
            , [IsCaseSensitive]  
            , [IsClustered]  
            , [IsFullTextInstalled]  
            , [IsSingleUser]  
            , [Language]  
            , [MailProfile]  
            , [MasterDBLogPath]  
            , [MasterDBPath]  
            , [MaxPrecision]  
            , [Name]  
            , [NamedPipesEnabled]  
            , [NetName]  
            , [NumberOfLogFiles]  
            , [OSVersion]  
            , [PerfMonMode]  
            , [PhysicalMemory]  
            , [Platform]  
            , [Processors]  
            , [ProcessorUsage]  
            , [Product]  
            , [ProductLevel]  
            , [ResourceVersionString]  
            , [RootDirectory]  
            , [ServerType]  
            , [ServiceAccount]  
            , [ServiceInstanceId]  
            , [ServiceName]  
            , [ServiceStartMode]  
            , [SqlCharSet]  
            , [SqlCharSetName]  
            , [SqlDomainGroup]  
            , [SqlSortOrder]  
            , [SqlSortOrderName]  
            , [Status]  
            , [TapeLoadWaitTime]  
            , [TcpEnabled]  
            , [VersionMajor]  
            , [VersionMinor]  
            , [VersionString]  
        )

        SELECT  urn
              , CONVERT(NVARCHAR(MAX), [powershell_path]) AS [powershell_path]
                , @processing_time_current AS [processing_time]  -- $FIXED: SQLBUVSTS-316258
                , SWITCHOFFSET (batch_time, DATENAME(TZoffset, SYSDATETIMEOFFSET())) AS [batch_time]  
                , CONVERT(SMALLINT,[AuditLevel]) AS [AuditLevel]
                , CONVERT(NVARCHAR(260) ,[BackupDirectory]) AS [BackupDirectory]
                , CONVERT(NVARCHAR(128) ,[BrowserServiceAccount]) AS [BrowserServiceAccount]
                , CONVERT(SMALLINT,[BrowserStartMode]) AS [BrowserStartMode]
                , CONVERT(NVARCHAR(20) ,[BuildClrVersionString]) AS [BuildClrVersionString]
                , CONVERT(INT,[BuildNumber]) AS [BuildNumber]
                , CONVERT(NVARCHAR(128),[Collation]) AS [Collation]
                , CONVERT(INT,[CollationID]) AS [CollationID]
                , CONVERT(INT,[ComparisonStyle]) AS [ComparisonStyle]
                , CONVERT(NVARCHAR(128),[ComputerNamePhysicalNetBIOS]) AS [ComputerNamePhysicalNetBIOS]
                , CONVERT(NVARCHAR(260),[DefaultFile]) AS [DefaultFile]
                , CONVERT(NVARCHAR(260),[DefaultLog]) AS [DefaultLog]
                , CONVERT(NVARCHAR(64),[Edition]) AS [Edition]
                , CONVERT(SMALLINT,[EngineEdition]) AS [EngineEdition]
                , CONVERT(NVARCHAR(260) ,[ErrorLogPath]) AS [ErrorLogPath]
                , CONVERT(NVARCHAR(260) ,[FilestreamShareName]) AS [FilestreamShareName]
                , CONVERT(NVARCHAR(260) ,[InstallDataDirectory]) AS [InstallDataDirectory]
                , CONVERT(NVARCHAR(260) ,[InstallSharedDirectory]) AS [InstallSharedDirectory]
                , CONVERT(NVARCHAR(128) ,[InstanceName]) AS [InstanceName]
                , CONVERT(BIT,[IsCaseSensitive]) AS [IsCaseSensitive]
                , CONVERT(BIT,[IsClustered]) AS [IsClustered]
                , CONVERT(BIT,[IsFullTextInstalled]) AS [IsFullTextInstalled]
                , CONVERT(BIT,[IsSingleUser]) AS [IsSingleUser]
                , CONVERT(NVARCHAR(64) ,[Language]) AS [Language]
                , CONVERT(NVARCHAR(128),[MailProfile]) AS [MailProfile]
                , CONVERT(NVARCHAR(260),[MasterDBLogPath]) AS [MasterDBLogPath]
                , CONVERT(NVARCHAR(260),[MasterDBPath]) AS [MasterDBPath]
                , CONVERT(TINYINT,[MaxPrecision]) AS [MaxPrecision]
                , CONVERT(NVARCHAR(128) ,[Name]) AS [Name]
                , CONVERT(BIT,[NamedPipesEnabled]) AS [NamedPipesEnabled]
                , CONVERT(NVARCHAR(128) ,[NetName]) AS [NetName]
                , CONVERT(INT,[NumberOfLogFiles]) AS [NumberOfLogFiles]
                , CONVERT(NVARCHAR(32) ,[OSVersion]) AS [OSVersion]
                , CONVERT(SMALLINT,[PerfMonMode]) AS [PerfMonMode]
                , CONVERT(INT,[PhysicalMemory]) AS [PhysicalMemory]
                , CONVERT(NVARCHAR(32) ,[Platform]) AS [Platform]
                , CONVERT(SMALLINT,[Processors]) AS [Processors]
                , CONVERT(INT,[ProcessorUsage]) AS [ProcessorUsage]
                , CONVERT(NVARCHAR(48) ,[Product]) AS [Product]
                , CONVERT(NVARCHAR(32) ,[ProductLevel]) AS [ProductLevel]
                , CONVERT(NVARCHAR(32) ,[ResourceVersionString]) AS [ResourceVersionString]
                , CONVERT(NVARCHAR(260) ,[RootDirectory]) AS [RootDirectory]
                , CONVERT(SMALLINT,[ServerType]) AS [ServerType]
                , CONVERT(NVARCHAR(128),[ServiceAccount]) AS [ServiceAccount]
                , CONVERT(NVARCHAR(64),[ServiceInstanceId]) AS [ServiceInstanceId]
                , CONVERT(NVARCHAR(64),[ServiceName]) AS [ServiceName]
                , CONVERT(SMALLINT,[ServiceStartMode]) AS [ServiceStartMode]
                , CONVERT(SMALLINT,[SqlCharSet]) AS [SqlCharSet]
                , CONVERT(NVARCHAR(32),[SqlCharSetName]) AS [SqlCharSetName]
                , CONVERT(NVARCHAR(128),[SqlDomainGroup]) AS [SqlDomainGroup]
                , CONVERT(SMALLINT,[SqlSortOrder]) AS [SqlSortOrder]
                , CONVERT(NVARCHAR(64),[SqlSortOrderName]) AS [SqlSortOrderName]
                , CONVERT(SMALLINT,[Status]) AS [Status]
                , CONVERT(INT,[TapeLoadWaitTime]) AS [TapeLoadWaitTime]
                , CONVERT(BIT,[TcpEnabled]) AS [TcpEnabled]
                , CONVERT(INT,[VersionMajor]) AS [VersionMajor]
                , CONVERT(INT,[VersionMinor]) AS [VersionMinor]
                , CONVERT(NVARCHAR(32),[VersionString]) AS [VersionString]
                
                FROM 
                    (SELECT urn, property_name, property_value, batch_time 
                    FROM [sysutility_ucp_staging].[latest_smo_properties]
                    WHERE object_type = 1) props     -- object_type = 1 is Server
                PIVOT
                (
                    MAX(property_value)
                    FOR property_name IN (    
                                   [powershell_path]
                                 , [AuditLevel]
                            , [BackupDirectory]
                            , [BrowserServiceAccount]
                            , [BrowserStartMode]
                            , [BuildClrVersionString]
                            , [BuildNumber]
                            , [Collation]
                            , [CollationID]
                            , [ComparisonStyle]
                            , [ComputerNamePhysicalNetBIOS]
                            , [DefaultFile]
                            , [DefaultLog]
                            , [Edition]
                            , [EngineEdition]
                            , [ErrorLogPath]
                            , [FilestreamShareName]
                            , [InstallDataDirectory]
                            , [InstallSharedDirectory]
                            , [InstanceName]
                            , [IsCaseSensitive]
                            , [IsClustered]
                            , [IsFullTextInstalled]
                            , [IsSingleUser]
                            , [Language]
                            , [MailProfile]
                            , [MasterDBLogPath]
                            , [MasterDBPath]
                            , [MaxPrecision]
                            , [Name]
                            , [NamedPipesEnabled]
                            , [NetName]
                            , [NumberOfLogFiles]
                            , [OSVersion]
                            , [PerfMonMode]
                            , [PhysicalMemory]
                            , [Platform]
                            , [Processors]
                            , [ProcessorUsage]
                            , [Product]
                            , [ProductLevel]
                            , [ResourceVersionString]
                            , [RootDirectory]
                            , [ServerType]
                            , [ServiceAccount]
                            , [ServiceInstanceId]
                            , [ServiceName]
                            , [ServiceStartMode]
                            , [SqlCharSet]
                            , [SqlCharSetName]
                            , [SqlDomainGroup]
                            , [SqlSortOrder]
                            , [SqlSortOrderName]
                            , [Status]
                            , [TapeLoadWaitTime]
                            , [TcpEnabled]
                            , [VersionMajor]
                            , [VersionMinor]
                            , [VersionString] )
                ) AS pvt         
         SET @row_count = @@ROWCOUNT;
         SET @task_elapsed_ms = DATEDIFF (ms, @task_start_time, GETUTCDATE());
         RAISERROR ('Insert into [smo_servers_internal]: %d rows, %d ms', 0, 1, @row_count, @task_elapsed_ms);
         SET @task_start_time = GETUTCDATE();
                  
         INSERT INTO [sysutility_ucp_core].[databases_internal]
                ([urn]
                , [powershell_path]
                , [processing_time]
                , [batch_time]
                , [server_instance_name]
                , [parent_urn]
                , [Collation]
                , [CompatibilityLevel]
                , [CreateDate]
                , [EncryptionEnabled]
                , [Name]
                , [RecoveryModel]
                , [Trustworthy]
                , [state])
            SELECT  [urn]
                  , CONVERT(NVARCHAR(MAX), [powershell_path]) AS [powershell_path]
                    , @processing_time_current AS processing_time  -- $FIXED: SQLBUVSTS-316258
                    , SWITCHOFFSET ([batch_time], DATENAME(TZoffset, SYSDATETIMEOFFSET())) AS [batch_time] 
                    , [server_instance_name]
                    , Left(urn, CHARINDEX('/Database[', urn, 1)-1) AS parent_urn
                    , CONVERT(NVARCHAR(128),[Collation]) AS Collation
                    , CONVERT(SMALLINT,[CompatibilityLevel]) AS CompatibilityLevel
                    -- DC (SSIS) doesn't support sql_variant, so all properties are uploaded as nvarchar(4000).  To successfully round-trip 
                    -- the property values through nvarchar, we use the same language-independent conversion style on MI and UCP. The shared 
                    -- fn_sysutility_get_culture_invariant_conversion_style_internal function gives us a consistent conversion style for each 
                    -- property data type that is language-independent and that won't cause data loss.  We also use this function on the MI 
                    -- when converting to nvarchar so that the two conversions are symmetrical.  (Ref: VSTS 361531, 359504, 12967)
                    , CONVERT(DATETIME, [CreateDate], msdb.dbo.fn_sysutility_get_culture_invariant_conversion_style_internal('datetime')) AS CreateDate
                    , CONVERT(BIT,[EncryptionEnabled]) AS EncryptionEnabled
                    , CONVERT(SYSNAME,[Name])AS [Name]
                    , CONVERT(SMALLINT,[RecoveryModel]) AS RecoveryModel
                    , CONVERT(BIT,[Trustworthy]) AS Trustworthy
                    -- Default to 0 (available) state. We'll update this to 1 (not available) for emergency/offline/etc databases later (we 
                    -- need to examine file and filegroup properties to infer whether a database should be available or not available). 
                    , 0 AS state
             FROM 
             (SELECT urn, server_instance_name, property_name, property_value, batch_time 
             FROM [sysutility_ucp_staging].[latest_smo_properties]
             WHERE object_type = 2) props -- object_type = 1 is Database
                PIVOT
                (
                    MAX(property_value)
                    FOR property_name IN ( 
                                       [powershell_path]
                                      , [ID]
                                      , [Collation]
                                            , [CompatibilityLevel]
                                            , [CreateDate]
                                            , [EncryptionEnabled]
                                            , [Name]
                                            , [RecoveryModel]
                                            , [Trustworthy] )
                ) AS pvt    
         SET @row_count = @@ROWCOUNT;
         SET @task_elapsed_ms = DATEDIFF (ms, @task_start_time, GETUTCDATE());
         RAISERROR ('Insert into [databases_internal]: %d rows, %d ms', 0, 1, @row_count, @task_elapsed_ms);
         SET @task_start_time = GETUTCDATE();
         
         
         INSERT INTO [sysutility_ucp_core].[filegroups_internal]
                ([urn]
                , [powershell_path]
                , [processing_time]
                , [batch_time]
                , [server_instance_name]
                , [database_name]
                , [parent_urn]
                , [Name])
            SELECT  [urn]
                  , CONVERT(NVARCHAR(MAX), [powershell_path]) AS [powershell_path]
                    , @processing_time_current AS processing_time  -- $FIXED: SQLBUVSTS-316258
                    , SWITCHOFFSET ([batch_time], DATENAME(TZoffset, SYSDATETIMEOFFSET())) AS [batch_time] 
                    , [server_instance_name]
                    , CONVERT(SYSNAME,[parent_name]) AS [database_name]
                    , Left(urn, CHARINDEX('/FileGroup[', urn, 1)-1) AS parent_urn
                    , CONVERT(SYSNAME,[Name]) AS Name
             FROM 
             (SELECT urn, server_instance_name, property_name, property_value, batch_time 
             FROM [sysutility_ucp_staging].[latest_smo_properties]
             WHERE object_type = 4) props -- object_type = 4 is FileGroup
                PIVOT
                (
                    MAX(property_value)
                    FOR property_name IN ( 
                                        [powershell_path]
                                      , [parent_name]
                                      , [ID]
                                            , [Name])
                ) AS pvt    
         SET @row_count = @@ROWCOUNT;
         SET @task_elapsed_ms = DATEDIFF (ms, @task_start_time, GETUTCDATE());
         RAISERROR ('Insert into [filegroups_internal]: %d rows, %d ms', 0, 1, @row_count, @task_elapsed_ms);
         SET @task_start_time = GETUTCDATE();
         
                  
         
         INSERT INTO [sysutility_ucp_core].[datafiles_internal]
                ([urn]
                , [powershell_path]
                , [processing_time]
                , [batch_time]
                , [server_instance_name]
                , [database_name]
                , [filegroup_name]
                , [parent_urn]
                , [Growth]
                , [GrowthType]
                , [MaxSize]
                , [Name]
                , [Size]
                , [UsedSpace]
                , [FileName]
                , [VolumeFreeSpace]
                , [volume_name]
                , [volume_device_id]
                , [physical_server_name])
            SELECT  [urn]
                  , CONVERT(NVARCHAR(MAX), pvt.[powershell_path]) AS [powershell_path]
                    , @processing_time_current AS processing_time  -- $FIXED: SQLBUVSTS-316258
                    , SWITCHOFFSET (pvt.[batch_time], DATENAME(TZoffset, SYSDATETIMEOFFSET())) AS [batch_time] 
                    , pvt.[server_instance_name]
                    , CONVERT(SYSNAME,[grandparent_name]) AS [database_name]
                    , CONVERT(SYSNAME,[parent_name]) AS [filegroup_name]
                    , Left(urn, CHARINDEX('/File[', urn, 1)-1) AS parent_urn
                    , CONVERT(REAL,[Growth]) AS Growth
                    , CONVERT(SMALLINT,[GrowthType]) AS GrowthType
                    , CONVERT(REAL,[MaxSize]) AS MaxSize
                    , CONVERT(SYSNAME,[Name]) AS Name
                    , CONVERT(REAL,[Size]) AS Size
                    , CONVERT(REAL,[UsedSpace]) AS UsedSpace
                    , CONVERT(NVARCHAR(260),[FileName]) AS FileName
                    , ISNULL(v.free_space, 0.0) * 1024 AS VolumeFreeSpace -- volumes_internal.free_space is MB, and VolumeFreeSpace is expected to be KB.
                    , ISNULL(v.volume_name, N'') AS volume_name
                    , v.[volume_device_id] AS [volume_device_id]
                    , pvt.[physical_server_name]
             FROM 
             (SELECT urn, physical_server_name, server_instance_name, property_name, property_value, batch_time 
             FROM [sysutility_ucp_staging].[latest_smo_properties]
             WHERE object_type = 5) props -- object_type = 5 is DataFile
                PIVOT
                (
                    MAX(property_value)
                    FOR property_name IN ( 
                                        [powershell_path]
                                      , [parent_name]
                                      , [grandparent_name]
                                      , [ID]
                                      , [Growth]
                                      , [GrowthType]
                                      , [MaxSize]
                                      , [Name]
                                      , [Size]
                                      , [UsedSpace]
                                      , [FileName] 
                                      , [volume_device_id])
                ) AS pvt
            LEFT OUTER JOIN
            [sysutility_ucp_core].[volumes_internal] v
            ON 
            v.physical_server_name = pvt.physical_server_name AND
            CONVERT(SYSNAME, pvt.[volume_device_id]) = v.volume_device_id
            WHERE v.processing_time = @processing_time_current
         SET @row_count = @@ROWCOUNT;
         SET @task_elapsed_ms = DATEDIFF (ms, @task_start_time, GETUTCDATE());
         RAISERROR ('Insert into [datafiles_internal]: %d rows, %d ms', 0, 1, @row_count, @task_elapsed_ms);
         SET @task_start_time = GETUTCDATE();
                             

         INSERT INTO [sysutility_ucp_core].[logfiles_internal]
                ([urn]
                , [powershell_path]
                , [processing_time]
                , [batch_time]
                , [server_instance_name]
                , [database_name]
                , [parent_urn]
                , [Growth]
                , [GrowthType]
                , [MaxSize]
                , [Name]
                , [Size]
                , [UsedSpace]
                , [FileName]
                , [VolumeFreeSpace]
                , [volume_name]
                , [volume_device_id]
                , [physical_server_name])
            SELECT[urn]
                  , CONVERT(NVARCHAR(MAX), pvt.[powershell_path]) AS [powershell_path]
                    , @processing_time_current AS processing_time  -- $FIXED: SQLBUVSTS-316258
                    , SWITCHOFFSET (pvt.[batch_time], DATENAME(TZoffset, SYSDATETIMEOFFSET())) AS [batch_time] 
                    , pvt.[server_instance_name]
                    , CONVERT(SYSNAME,[parent_name]) AS [database_name]
                    , Left(urn, CHARINDEX('/LogFile[', urn, 1)-1) AS parent_urn
                    , CONVERT(REAL,[Growth]) AS Growth
                    , CONVERT(SMALLINT,[GrowthType]) AS GrowthType
                    , CONVERT(REAL,[MaxSize]) AS MaxSize
                    , CONVERT(SYSNAME,[Name]) AS Name
                    , CONVERT(REAL,[Size]) AS Size
                    --- The collection data may not contain the UsedSpace property of the log file of a database in EMERGENCY state. 
                    , ISNULL(CONVERT(REAL,[UsedSpace]), 0.0) AS UsedSpace
                    , CONVERT(NVARCHAR(260),[FileName]) AS FileName
                    , ISNULL(v.free_space, 0.0) * 1024 AS VolumeFreeSpace -- volumes_internal.free_space is MB, and VolumeFreeSpace is expected to be KB.
                    , ISNULL(v.volume_name, N'') AS volume_name
                    , v.[volume_device_id] AS [volume_device_id]
                    , pvt.[physical_server_name]
             FROM 
             (SELECT urn, physical_server_name, server_instance_name, property_name, property_value, batch_time 
             FROM [sysutility_ucp_staging].[latest_smo_properties] 
             WHERE object_type = 3) props -- object_type = 3 is LogFile
                PIVOT
                (
                    MAX(property_value)
                    FOR property_name IN ( 
                                       [powershell_path]
                                      , [parent_name]
                                      , [ID]
                                      , [Growth]
                                      , [GrowthType]
                                      , [MaxSize]
                                      , [Name]
                                      , [Size]
                                      , [UsedSpace]
                                      , [FileName] 
                                      , [volume_device_id])
                ) AS pvt  
            LEFT OUTER JOIN
            [sysutility_ucp_core].[volumes_internal] v
            ON 
            v.physical_server_name = pvt.physical_server_name AND
            CONVERT(SYSNAME, pvt.[volume_device_id]) = v.volume_device_id
            WHERE v.processing_time = @processing_time_current
         SET @row_count = @@ROWCOUNT;
         SET @task_elapsed_ms = DATEDIFF (ms, @task_start_time, GETUTCDATE());
         RAISERROR ('Insert into [logfiles_internal]: %d rows, %d ms', 0, 1, @row_count, @task_elapsed_ms);
         SET @task_start_time = GETUTCDATE();
         
         -- Identify all of the databases for which we do not have all file/filegroup details because of the current 
         -- database status (db is offline, recovering, emergency mode, etc).  Update the state for these databases 
         -- to 1 ("not available"). 
         UPDATE sysutility_ucp_core.databases_internal 
         SET state = 1 
         FROM sysutility_ucp_core.databases_internal AS db
         WHERE 
             -- Case #1: Emergency mode databases are considered 'not available' -- the database is not recovered, and 
             -- we can't get correct log file metadata.  We detect this by looking for databases with a log file that 
             -- has a size of 0, which is impossible in an available database. 
            EXISTS (
                SELECT * 
                FROM sysutility_ucp_core.logfiles_internal AS lf
                WHERE lf.server_instance_name = db.server_instance_name 
                    AND lf.database_name = db.Name
                    AND lf.Size = 0
                    AND lf.processing_time = @processing_time_current)
            -- Case #2: When a database is in other "not available" states (like recovering, offline, suspect), we 
            -- cannot retrieve filegroup or file-level metadata.  We detect this case by looking for databases that seem 
            -- to have no filegroups, which is an impossible state for an online & available database. 
            OR NOT EXISTS (
                SELECT * 
                FROM sysutility_ucp_core.filegroups_internal AS fg
                WHERE fg.server_instance_name = db.server_instance_name 
                    AND fg.database_name = db.Name
                    AND fg.processing_time = @processing_time_current);
         SET @row_count = @@ROWCOUNT;
         SET @task_elapsed_ms = DATEDIFF (ms, @task_start_time, GETUTCDATE());
         RAISERROR ('Discover unavailable databases: %d rows, %d ms', 0, 1, @row_count, @task_elapsed_ms);
         SET @task_start_time = GETUTCDATE();
         
         
         ----- 
         ----- Stage 2: Insert into all the measure tables
         -----          cpu_utilization_internal (computers, instances, dacs)
         -----          space_utilization_internal (volumes, instances, databases, filegroups, datafiles, logfiles)
         ----- 
         ----- 
         INSERT INTO [sysutility_ucp_core].[cpu_utilization_internal](
              aggregation_type, object_type, processing_time,
              physical_server_name, server_instance_name, database_name,
              percent_total_cpu_utilization)
            SELECT 0,   -- No aggregation
                   1,   -- Computer Object
                   @processing_time_current,
                   physical_server_name,
                   N'', 
                   N'',
                   percent_total_cpu_utilization
            FROM [sysutility_ucp_core].[computers_internal]
            WHERE processing_time = @processing_time_current
              UNION ALL
            SELECT 0,   -- No aggregation
                   3,   -- Instance object
                   @processing_time_current, 
                   N'', 
                   server_instance_name,
                   N'',
                   instance_processor_usage
            FROM [sysutility_ucp_staging].[latest_instance_cpu_utilization]
              UNION ALL
            SELECT 0,   -- No aggregation
                   4,   -- Database/DAC object
                   @processing_time_current,
                   N'',   -- computer_name
                   server_instance_name,
                   dac_name,
                   dac_percent_total_cpu_utilization
            FROM [sysutility_ucp_core].[dacs_internal]
            WHERE processing_time = @processing_time_current
         SET @row_count = @@ROWCOUNT;
         SET @task_elapsed_ms = DATEDIFF (ms, @task_start_time, GETUTCDATE());
         RAISERROR ('Insert into [cpu_utilization_internal]: %d rows, %d ms', 0, 1, @row_count, @task_elapsed_ms);
         SET @task_start_time = GETUTCDATE();
            
         INSERT INTO [sysutility_ucp_core].[space_utilization_internal] (
               aggregation_type, object_type, processing_time,
               virtual_server_name, volume_device_id, 
               server_instance_name, database_name, [filegroup_name], dbfile_name,
               total_space_bytes, allocated_space_bytes, used_space_bytes, available_space_bytes)
            SELECT 0 AS aggregation_type,
                   CASE WHEN group_id = 0 AND [filegroup_name] = N'' THEN 7 -- logfile
                        WHEN group_id = 0 THEN 6 -- datafile
                        WHEN group_id = 1 THEN 5 -- filegroup
                        WHEN group_id = 3 THEN 4 -- database
                        WHEN group_id = 7 THEN 3 -- instance
                        ELSE NULL -- should never get here 
                   END as [object_type], 
                   @processing_time_current AS processing_time,
                   N'' as virtual_server_name, 
                   N'' as volume_device_id, 
                   ISNULL(server_instance_name, N''), -- shouldn't ever get to be null 
                   ISNULL(database_name, N''), 
                   ISNULL([filegroup_name], N''), 
                   ISNULL([dbfile_name], N''),
                   CASE WHEN group_id = 0 THEN total_space_kb * 1024 ELSE NULL END, 
                   CASE WHEN group_id = 0 THEN allocated_space_kb * 1024 ELSE NULL END, 
                   used_space_kb * 1024, 
                   CASE WHEN group_id = 0 THEN available_space_kb * 1024 ELSE NULL END
            FROM (
                SELECT server_instance_name, database_name, [filegroup_name], dbfile_name,  
                       SUM(MaxSize) AS total_space_kb,  -- Is this right?
                       SUM([Size]) AS allocated_space_kb, 
                       SUM(UsedSpace) AS used_space_kb, 
                       SUM(available_space) AS available_space_kb,
                       GROUPING_ID(server_instance_name, database_name, [filegroup_name], dbfile_name) AS group_id
                FROM (SELECT server_instance_name, database_name, [filegroup_name], [Name] as dbfile_name, 
                             MaxSize, [Size], UsedSpace,
                             [sysutility_ucp_misc].[fn_get_max_size_available]([Size], [MaxSize], [Growth], [GrowthType], [VolumeFreeSpace]) AS available_space
                       FROM [sysutility_ucp_core].[datafiles_internal] 
                       WHERE processing_time = @processing_time_current
                     UNION ALL
                      SELECT server_instance_name, database_name, N'' AS [filegroup_name], 
                             [Name] AS dbfile_name, 
                             MaxSize, [Size], UsedSpace,
                             [sysutility_ucp_misc].[fn_get_max_size_available]([Size], [MaxSize], [Growth], [GrowthType], [VolumeFreeSpace]) AS available_space
                      FROM [sysutility_ucp_core].[logfiles_internal]
                      WHERE processing_time = @processing_time_current) as dbfiles
                GROUP BY GROUPING SETS((server_instance_name), 
                                       (server_instance_name, database_name),
                                       (server_instance_name, database_name, [filegroup_name]),
                                       (server_instance_name, database_name, [filegroup_name], [dbfile_name])
                                      )
                 ) AS instance_space_utilizations
            UNION ALL       
            SELECT 0 AS aggregation_type,
                   CASE WHEN GROUPING_ID(virtual_server_name, volume_device_id) = 3 THEN 0 -- utility 
                        WHEN GROUPING_ID(virtual_server_name, volume_device_id) = 1 THEN 1 -- computer 
                        WHEN GROUPING_ID(virtual_server_name, volume_device_id) = 0 THEN 2 -- volume
                        ELSE NULL  -- should never get here
                   END AS object_type,
                   @processing_time_current as processing_time,
                   ISNULL(virtual_server_name, N''), 
                   ISNULL(volume_device_id, N'') AS volume_device_id, 
                   N'' as server_instance_name, 
                   N'' as database_name, 
                   N'' as [filegroup_name], 
                   N'' as dbfile_name,
                   SUM(total_space_available)*1048576 AS total_space_bytes,
                   SUM(total_space_available)*1048576 AS allocated_space_bytes,
                   SUM(total_space_available - free_space)*1048576  AS used_space_bytes,
                   SUM(free_space)*1048576 AS available_space_bytes
            FROM [sysutility_ucp_core].[volumes_internal] 
            WHERE processing_time = @processing_time_current
            GROUP BY GROUPING SETS ((),
                                    (virtual_server_name),
                                    (virtual_server_name, volume_device_id)
                                   )
         SET @row_count = @@ROWCOUNT;
         SET @task_elapsed_ms = DATEDIFF (ms, @task_start_time, GETUTCDATE());
         RAISERROR ('Insert into [space_utilization_internal]: %d rows, %d ms', 0, 1, @row_count, @task_elapsed_ms);

        --         
        -- State changes
        --      
        UPDATE [msdb].[dbo].[sysutility_ucp_processing_state_internal] 
          SET latest_processing_time = @processing_time_current 

        -- Update the snapshot partitions table
        -- Push down the previous snapshot partition values and 
        -- store the current max snapshot in the latest (top) record           
        UPDATE [msdb].[dbo].[sysutility_ucp_snapshot_partitions_internal] SET latest_consistent_snapshot_id = (SELECT TOP 1 latest_consistent_snapshot_id FROM [msdb].[dbo].[sysutility_ucp_snapshot_partitions_internal] WHERE time_id = 1)  WHERE time_id = 2
        UPDATE [msdb].[dbo].[sysutility_ucp_snapshot_partitions_internal] SET latest_consistent_snapshot_id = (SELECT TOP 1 latest_consistent_snapshot_id FROM [msdb].[dbo].[sysutility_ucp_snapshot_partitions_internal] WHERE time_id = 0)  WHERE time_id = 1
        UPDATE [msdb].[dbo].[sysutility_ucp_snapshot_partitions_internal] SET latest_consistent_snapshot_id = @max_snapshot_id WHERE time_id = 0

        -- As we have inserted chunk of data in the cache tables, the stats on these tables
        -- get stale there by leading to performance degradation of the health state queries
        -- Force update the stats on these tables so that the QO is able to generate a more 
        -- realisitc query execution plan
        SET @task_start_time = GETUTCDATE();
        UPDATE STATISTICS [msdb].[dbo].[sysutility_ucp_processing_state_internal];

        -- Update stats on all dimension and measure cache tables
        DECLARE @schema sysname
        DECLARE @name sysname
        DECLARE @query NVARCHAR(MAX)
 
        DECLARE cache_tables CURSOR FOR
        SELECT object_schema, [object_name]
        FROM sysutility_ucp_misc.utility_objects_internal
        WHERE utility_object_type IN ('DIMENSION', 'MEASURE');
 
        OPEN cache_tables;
        FETCH NEXT FROM cache_tables INTO @schema, @name;
        WHILE (@@FETCH_STATUS <> -1)
        BEGIN
            SET @query = 'UPDATE STATISTICS ' + QUOTENAME (@schema) + '.' + QUOTENAME (@name); 
            EXEC (@query);
            FETCH NEXT FROM cache_tables INTO @schema, @name;
        END;
        CLOSE cache_tables;
        DEALLOCATE cache_tables;
 
        SET @task_elapsed_ms = DATEDIFF (ms, @task_start_time, GETUTCDATE());
        RAISERROR ('Update statistics: %d ms', 0, 1, @task_elapsed_ms);
END
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'STAGING', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_staging', 
   @level1type = 'PROCEDURE', @level1name = 'sp_copy_live_table_data_into_cache_tables';
GO


-----------------------------------------------------------------------------------------
-- Procedure sp_copy_cache_table_data_into_aggregate_tables
--   Aggregates the latest round of data from the "cache" tables into the "aggregate" tables
--   for the appropriate aggregation interval 
-----------------------------------------------------------------------------------------
IF OBJECT_ID ('sysutility_ucp_core.sp_copy_cache_table_data_into_aggregate_tables') IS NOT NULL
BEGIN
   RAISERROR('Dropping procedure sysutility_ucp_core.sp_copy_cache_table_data_into_aggregate_tables procedure', 0, 1) WITH NOWAIT;
   DROP PROCEDURE sysutility_ucp_core.sp_copy_cache_table_data_into_aggregate_tables;
END
GO
RAISERROR('Creating procedure sysutility_ucp_core.sp_copy_cache_table_data_into_aggregate_tables', 0, 1) WITH NOWAIT;
GO
CREATE PROCEDURE sysutility_ucp_core.sp_copy_cache_table_data_into_aggregate_tables
    @aggregation_type INT,
    @endTime DATETIMEOFFSET(7)
AS
BEGIN       
    DECLARE @startTime DATETIMEOFFSET(7)
    DECLARE @lowerAggregationLevel [sysutility_ucp_core].AggregationType

    SELECT @lowerAggregationLevel = 0    -- compute from detail rows
    
    IF (@aggregation_type = 1)
       SELECT @startTime = DATEADD(hour, -1, @endTime)
    ELSE IF (@aggregation_type = 2)
       SELECT @startTime = DATEADD(day, -1, @endTime)
       -- todo (VSTS #345038) 
       -- Ideally, we would be using the hourly aggregation values to compute the 
       --   daily aggregation ("SELECT @lowerAggregationLevel = 1")
       --
    ELSE BEGIN
        -- todo. Raise an error instead
        RETURN(1)
    END
    
    INSERT INTO [sysutility_ucp_core].[cpu_utilization_internal] (
         aggregation_type, object_type, processing_time,
         physical_server_name, server_instance_name, database_name,
         percent_total_cpu_utilization)
       SELECT @aggregation_type, object_type, @endTime,
              physical_server_name, server_instance_name, database_name,
              AVG(percent_total_cpu_utilization)
       FROM [sysutility_ucp_core].[cpu_utilization_internal]
       WHERE (processing_time BETWEEN @startTime and @endTime) AND
             aggregation_type = @lowerAggregationLevel 
       GROUP BY object_type, physical_server_name, server_instance_name, database_name
    
    INSERT INTO [sysutility_ucp_core].[space_utilization_internal] (
         aggregation_type, object_type, processing_time,
         virtual_server_name, volume_device_id, server_instance_name, database_name, [filegroup_name], dbfile_name, 
         total_space_bytes, allocated_space_bytes, used_space_bytes, available_space_bytes)
       SELECT @aggregation_type, object_type, @endTime,
              virtual_server_name, volume_device_id, server_instance_name, database_name, [filegroup_name], dbfile_name,
              -- Is AVG the right aggregate to use - should this instead be LAST()
              AVG(total_space_bytes), AVG(allocated_space_bytes), AVG(used_space_bytes), AVG(available_space_bytes)
       FROM [sysutility_ucp_core].[space_utilization_internal]
       WHERE (processing_time BETWEEN @startTime and @endTime) AND
             aggregation_type = @lowerAggregationLevel
       GROUP BY object_type, virtual_server_name, volume_device_id, server_instance_name, database_name, [filegroup_name], dbfile_name 
           
END
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'CORE', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
   @level1type = 'PROCEDURE', @level1name = 'sp_copy_cache_table_data_into_aggregate_tables';
GO

-----------------------------------------------------------------------------------------
-- Procedure sp_purge_cache_tables
--   Deletes the data in Utility's MDW cache tables according to the data retention 
--   periods specified in msdb.dbo.sysutility_ucp_configuration_internal. 
-----------------------------------------------------------------------------------------
IF OBJECT_ID ('sysutility_ucp_core.sp_purge_cache_tables') IS NOT NULL
BEGIN
   RAISERROR('Dropping procedure sysutility_ucp_core.sp_purge_cache_tables procedure', 0, 1) WITH NOWAIT;
   DROP PROCEDURE sysutility_ucp_core.sp_purge_cache_tables;
END
GO
RAISERROR('Creating procedure sysutility_ucp_core.sp_purge_cache_tables', 0, 1) WITH NOWAIT;
GO
CREATE PROCEDURE sysutility_ucp_core.sp_purge_cache_tables
AS
BEGIN
    DECLARE @rows_affected bigint;
    DECLARE @delete_batch_size varchar(30);

    SET @delete_batch_size = 500;
    SET @rows_affected = -1;

    DECLARE @days_to_retain_minute_data int;
    DECLARE @days_to_retain_hour_data int;
    DECLARE @days_to_retain_day_data int;

    SELECT @days_to_retain_minute_data = CONVERT (int,current_value) 
    FROM [msdb].[dbo].[sysutility_ucp_configuration_internal] 
    WHERE name = 'MdwRetentionLengthInDaysForMinutesHistory';

    SELECT @days_to_retain_hour_data = CONVERT (int,current_value) 
    FROM [msdb].[dbo].[sysutility_ucp_configuration_internal] 
    WHERE name = 'MdwRetentionLengthInDaysForHoursHistory';

    SELECT @days_to_retain_day_data = CONVERT (int,current_value) 
    FROM [msdb].[dbo].[sysutility_ucp_configuration_internal] 
    WHERE name = 'MdwRetentionLengthInDaysForDaysHistory';

    DECLARE @date_threshold_minute_data DATETIMEOFFSET(7) = DATEADD(day, -@days_to_retain_minute_data, SYSDATETIMEOFFSET());
    DECLARE @date_threshold_hour_data DATETIMEOFFSET(7) = DATEADD(day, -@days_to_retain_hour_data, SYSDATETIMEOFFSET());
    DECLARE @date_threshold_day_data DATETIMEOFFSET(7) = DATEADD(day, -@days_to_retain_day_data, SYSDATETIMEOFFSET());
       
    DECLARE @schema sysname
    DECLARE @name sysname
    DECLARE @query NVARCHAR(MAX)

    DECLARE dimensions_cursor CURSOR FOR
    SELECT object_schema, [object_name] 
    FROM sysutility_ucp_misc.utility_objects_internal
    WHERE utility_object_type = 'DIMENSION';

    -- Purge the dimension tables. 
    -- The number of rows that can be deleted from these tables can be very large.  If we deleted 
    -- all of these rows in a single delete statement, we would hold locks for an arbitrarily-long 
    -- time (and potentially escalate to table locks), causing long-duration blocking.  This could 
    -- also lead to transaction log growth, since log records after the oldest still-open transaction 
    -- can't be truncated.  To avoid these two problems, we delete rows in batches of 500 and loop 
    -- until we've deleted all rows that we no longer need. 
    OPEN dimensions_cursor;
    FETCH NEXT FROM dimensions_cursor INTO @schema, @name;
    WHILE (@@FETCH_STATUS <> -1)
    BEGIN
        SET @rows_affected = -1;
        WHILE (@rows_affected != 0)
        BEGIN
            -- We use dynamic SQL here because the table name is variable, but this also has the benefit of 
            -- providing the optimizer with the final value for @delete_batch_size and @date_threshold. 
            SET @query = 'DELETE TOP (' + @delete_batch_size + ') FROM ' + QUOTENAME(@schema) + '.' + QUOTENAME(@name) +
                         ' WHERE processing_time < @date_threshold';
            EXEC sp_executesql @query, N'@date_threshold datetimeoffset(7)', @date_threshold = @date_threshold_minute_data;
            SET @rows_affected = @@ROWCOUNT;
        END;

        FETCH NEXT FROM dimensions_cursor INTO @schema, @name;
    END;
    CLOSE dimensions_cursor;
    DEALLOCATE dimensions_cursor;

    DECLARE measures_cursor CURSOR FOR
    SELECT object_schema, [object_name] 
    FROM sysutility_ucp_misc.utility_objects_internal
    WHERE utility_object_type = 'MEASURE';

    -- Delete "per-minute" (15 minute) data from measure tables
    OPEN measures_cursor;
    FETCH NEXT FROM measures_cursor INTO @schema, @name;
    WHILE (@@FETCH_STATUS <> -1)
    BEGIN
        SET @rows_affected = -1;
        WHILE (@rows_affected != 0)
        BEGIN
            SET @query = 'DELETE TOP (' + @delete_batch_size + ') FROM ' + QUOTENAME(@schema) + '.' + QUOTENAME(@name) +
                         ' WHERE processing_time < @date_threshold AND aggregation_type = 0';
            EXEC sp_executesql @query, N'@date_threshold datetimeoffset(7)', @date_threshold = @date_threshold_minute_data;
            SET @rows_affected = @@ROWCOUNT;
        END;            
        
        FETCH NEXT FROM measures_cursor INTO @schema, @name;
    END;
    CLOSE measures_cursor;
    
    -- Delete "per-hour" data from our measure-tables 
    OPEN measures_cursor;
    FETCH NEXT FROM measures_cursor INTO @schema, @name;
    WHILE (@@FETCH_STATUS <> -1)
    BEGIN
        SET @rows_affected = -1;
        WHILE (@rows_affected != 0)
        BEGIN
            SET @query = 'DELETE TOP (' + @delete_batch_size + ') FROM ' + QUOTENAME(@schema) + '.' + QUOTENAME(@name) +
                         ' WHERE processing_time < @date_threshold AND aggregation_type = 1';
            EXEC sp_executesql @query, N'@date_threshold datetimeoffset(7)', @date_threshold = @date_threshold_hour_data;
            SET @rows_affected = @@ROWCOUNT;
        END;    
        
        FETCH NEXT FROM measures_cursor INTO @schema, @name;
    END;
    CLOSE measures_cursor;
    
    -- Delete "per-day" data from measure tables
    OPEN measures_cursor;
    FETCH NEXT FROM measures_cursor INTO @schema, @name;
    WHILE (@@FETCH_STATUS <> -1)
    BEGIN
        SET @rows_affected = -1;
        WHILE (@rows_affected != 0)
        BEGIN
            SET @query = 'DELETE TOP (' + @delete_batch_size + ') FROM ' + QUOTENAME(@schema) + '.' + QUOTENAME(@name) +
                         ' WHERE processing_time < @date_threshold AND aggregation_type = 2';
            EXEC sp_executesql @query, N'@date_threshold datetimeoffset(7)', @date_threshold = @date_threshold_day_data;
            SET @rows_affected = @@ROWCOUNT;
        END;
        
        FETCH NEXT FROM measures_cursor INTO @schema, @name;
    END;
    CLOSE measures_cursor;
    DEALLOCATE measures_cursor;
    
END;
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'CORE', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
   @level1type = 'PROCEDURE', @level1name = 'sp_purge_cache_tables';
GO


-----------------------------------------------------------------------------------------
-- Procedure sp_initialize_mdw_internal
--   Performs runtime schema modifications that must be deferred until Create UCP.  
--   Executed by sp_sysutility_ucp_initialize_mdw in msdb.  
--   
--   Note that this proc calls xp_qv, which requires that the 'Agent XPs' sp_configure 
--   value be enabled. During upgrade, this setting will need to be manually enabled, 
--   since upgrade scripts are executed while Agent is stopped. 
-----------------------------------------------------------------------------------------
IF OBJECT_ID ('sysutility_ucp_core.sp_initialize_mdw_internal') IS NOT NULL
BEGIN
   RAISERROR('Dropping procedure sysutility_ucp_core.sp_initialize_mdw_internal procedure', 0, 1) WITH NOWAIT;
   DROP PROCEDURE sysutility_ucp_core.sp_initialize_mdw_internal;
END
GO
RAISERROR('Creating procedure sysutility_ucp_core.sp_initialize_mdw_internal', 0, 1) WITH NOWAIT;
GO
CREATE PROCEDURE sysutility_ucp_core.sp_initialize_mdw_internal
AS
BEGIN
   IF (msdb.dbo.fn_sysutility_ucp_get_edition_is_ucp_capable_internal() = 1)
   BEGIN
      RAISERROR ('Instance is able to be used as a Utility Control Point.', 0, 1) WITH NOWAIT;
   END
   ELSE BEGIN
      DECLARE @edition nvarchar(128);
      SELECT @edition = CONVERT(nvarchar(128), SERVERPROPERTY('Edition'));
      RAISERROR(37004, -1, -1, @edition);
      RETURN(1);
   END;
   
   -- The Utility schema uses two Enterprise-only engine features: compression and partitioning.  
   -- Utility is an Enterprise-only feature, but we share instmdw.sql and the MDW database with other 
   -- features that are not Enterprise only.  Therefore we can't include the following things as part 
   -- of the CREATE TABLE statements because instmdw.sql would fail when run on a non-Enterprise SKU. 
   -- To work around this, we defer this part of the UCP schema creation until Create UCP is run. 
   IF (3 = CONVERT (int, SERVERPROPERTY('EngineEdition'))) -- Enterprise/Enterprise Eval/Developer/Data Center
   BEGIN
      -- Enable data compression on tables that benefit from it the most
      RAISERROR ('Enabling compression on MDW tables', 0, 1) WITH NOWAIT;
      ALTER TABLE [snapshots].[sysutility_ucp_smo_properties_internal] REBUILD WITH (DATA_COMPRESSION = PAGE);
      ALTER TABLE [sysutility_ucp_core].[smo_servers_internal] REBUILD WITH (DATA_COMPRESSION = PAGE);
      ALTER TABLE [sysutility_ucp_core].[databases_internal] REBUILD WITH (DATA_COMPRESSION = PAGE);
      ALTER TABLE [sysutility_ucp_core].[filegroups_internal] REBUILD WITH (DATA_COMPRESSION = PAGE);
      ALTER TABLE [sysutility_ucp_core].[datafiles_internal] REBUILD WITH (DATA_COMPRESSION = PAGE);
      ALTER TABLE [sysutility_ucp_core].[logfiles_internal] REBUILD WITH (DATA_COMPRESSION = PAGE);
      ALTER TABLE [sysutility_ucp_core].[cpu_utilization_internal] REBUILD WITH (DATA_COMPRESSION = PAGE);
      ALTER TABLE [sysutility_ucp_core].[space_utilization_internal] REBUILD WITH (DATA_COMPRESSION = PAGE);
     
      -- Partitioning (and compression) on measure tables

      -- Create a partitioning scheme based on aggregationType. In effect, we'd like
      -- each aggregation-level to get its own partition. Currently, we only support
      -- 3 aggregation levels.
      --
      -- FUTURE: It would be nice to support composite partitioning in the future. 
      --   1. We could further create a sub-partition for each object-type (computer etc.)
      --   2. If we could have sliding partitions, we could avoid deletes altogether, and 
      --      instead work with truncate/drop-partition style operations
      --
      IF NOT EXISTS (SELECT name FROM sys.partition_functions WHERE name = N'sysutility_ucp_aggregation_type_partition_function')
      BEGIN
         RAISERROR ('Creating partition function [sysutility_ucp_aggregation_type_partition_function]', 0, 1) WITH NOWAIT;
         -- Use dynamic SQL here (and in the next create stmt) b/c otherwise SQL will fail the creation of this 
         -- proc on Workgroup or Standard edition. 
         EXEC ('
            CREATE PARTITION FUNCTION [sysutility_ucp_aggregation_type_partition_function](TINYINT)
               AS RANGE LEFT 
               FOR VALUES(0, 1, 2)');
      END;

      IF NOT EXISTS (SELECT name FROM sys.partition_schemes WHERE name = N'sysutility_ucp_aggregation_type_partition_scheme')
      BEGIN
         RAISERROR ('Creating partition scheme [sysutility_ucp_aggregation_type_partition_scheme]', 0, 1) WITH NOWAIT;
         EXEC ('
            CREATE PARTITION SCHEME [sysutility_ucp_aggregation_type_partition_scheme] 
               AS PARTITION [sysutility_ucp_aggregation_type_partition_function] 
               ALL TO ([PRIMARY])');
      END;

      -- ALTER INDEX can't change partition scheme.  Instead we must drop and recreate the clustered PKs. 
      IF OBJECT_ID ('[sysutility_ucp_core].[pk_cpu_utilization_internal]') IS NOT NULL
      BEGIN
         RAISERROR ('Dropping primary key [sysutility_ucp_core].[cpu_utilization_internal].[pk_cpu_utilization_internal]', 0, 1) WITH NOWAIT;
         ALTER TABLE [sysutility_ucp_core].[cpu_utilization_internal] DROP CONSTRAINT [pk_cpu_utilization_internal]; 
      END;
      RAISERROR ('Creating partitioned primary key [sysutility_ucp_core].[cpu_utilization_internal].[pk_cpu_utilization_internal]', 0, 1) WITH NOWAIT;
      ALTER TABLE [sysutility_ucp_core].[cpu_utilization_internal] ADD 
         CONSTRAINT pk_cpu_utilization_internal 
            PRIMARY KEY CLUSTERED (aggregation_type, processing_time, object_type, physical_server_name, server_instance_name, database_name) 
            WITH (DATA_COMPRESSION = PAGE)
            ON [sysutility_ucp_aggregation_type_partition_scheme](aggregation_type);
      
      IF OBJECT_ID ('[sysutility_ucp_core].[pk_storage_utilization]') IS NOT NULL
      BEGIN
         RAISERROR ('Dropping primary key [sysutility_ucp_core].[space_utilization_internal].[pk_storage_utilization]', 0, 1) WITH NOWAIT;
         ALTER TABLE [sysutility_ucp_core].[space_utilization_internal] DROP CONSTRAINT [pk_storage_utilization]; 
      END;
      RAISERROR ('Creating partitioned primary key [sysutility_ucp_core].[space_utilization_internal].[pk_storage_utilization]', 0, 1) WITH NOWAIT;
      ALTER TABLE [sysutility_ucp_core].[space_utilization_internal] ADD 
         CONSTRAINT pk_storage_utilization 
            PRIMARY KEY CLUSTERED(
               aggregation_type,
               processing_time, 
               object_type, 
               virtual_server_name, 
               volume_device_id, 
               server_instance_name, 
               database_name, 
               [filegroup_name], 
               dbfile_name) 
            WITH (DATA_COMPRESSION = PAGE) 
            ON [sysutility_ucp_aggregation_type_partition_scheme](aggregation_type);
   END;
END;
GO

EXEC sp_addextendedproperty 
   @name = 'MS_UtilityObjectType', @value = 'CORE', 
   @level0type = 'SCHEMA', @level0name = 'sysutility_ucp_core', 
   @level1type = 'PROCEDURE', @level1name = 'sp_initialize_mdw_internal';
GO



BEGIN TRANSACTION

   -----------------------------------------------------------------------
   --  Remove the sysutility_get_views_data_into_cache_tables job if it is already existing.
   -----------------------------------------------------------------------
   DECLARE @job_id UNIQUEIDENTIFIER
   SELECT @job_id = job_id FROM msdb.dbo.sysjobs_view WHERE name = N'sysutility_get_views_data_into_cache_tables'
   IF  EXISTS (SELECT job_id FROM msdb.dbo.sysjobs_view WHERE name = N'sysutility_get_views_data_into_cache_tables')
      EXEC msdb.dbo.sp_delete_job @job_id=@job_id, @delete_unused_schedule=1
   
   DECLARE @ReturnCode INT

   -- Get the current logged in user name.
   DECLARE @CurrentLoggedIn nvarchar(128)
   Set @CurrentLoggedIn=SUSER_Name(0x1)

   SELECT @ReturnCode = 0
   -----------------------------------------------------------------------
   --  'sysutility_get_views_data_into_cache_tables job' has following steps:
   --  1. Insert [sysutility_ucp_core].[fn_get_cpu_utilizations](0) data into 
   --     [sysutility_ucp_core].[computer_cpu_utilizations_internal] cache table.
   --  2. Insert [snapshots].[dac_view] data into 
   --     [snapshots].[dac_table] 
   --  3. Insert storage live information into
   --     storage cache table
   --  4. Insert SMO live information into
   --     SMO cache table
   -----------------------------------------------------------------------
   IF NOT EXISTS (SELECT name FROM msdb.dbo.syscategories WHERE name=N'[Uncategorized (Local)]' AND category_class=1)
      BEGIN
         EXEC @ReturnCode = msdb.dbo.sp_add_category @class=N'JOB', @type=N'LOCAL', @name=N'[Uncategorized (Local)]'
         IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
      END

   DECLARE @mdwDBName NVARCHAR(256)
   SELECT  @mdwDBName = DB_NAME()
   DECLARE @jobId BINARY(16)
   EXEC @ReturnCode =  msdb.dbo.sp_add_job @job_name=N'sysutility_get_views_data_into_cache_tables', 
      @enabled=1, 
      @notify_level_eventlog=0, 
      @notify_level_email=0, 
      @notify_level_netsend=0, 
      @notify_level_page=0, 
      @delete_level=0, 
      @description=N'Gets all the views data into corresponding cache tables after every 15 minutes', 
      @category_name=N'[Uncategorized (Local)]', 
      @owner_login_name= @CurrentLoggedIn , @job_id = @jobId OUTPUT
   IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
   
      EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Insert latest data from live tables into cache tables',  
      @step_id=1, 
      @cmdexec_success_code=0, 
      @on_success_action=3, 
      @on_success_step_id=0, 
      @on_fail_action=2, 
      @on_fail_step_id=0, 
      @retry_attempts=0, 
      @retry_interval=0, 
      @os_run_priority=0, @subsystem=N'TSQL', 
      @command=N'EXEC [sysutility_ucp_staging].sp_copy_live_table_data_into_cache_tables', 
      @database_name = @mdwDBName,
      @flags=0
   
    DECLARE @execute_policy_script NVARCHAR(MAX)

    -- Powershell script to evaluate utility resource health policies
    -- Note 1: The following line uses SQL Agent tokens to set the server name
    -- ESCAPE_SQUOTE(SRVR) with a $ sign in front is a special token to SQL Agent
    -- When the job is run, SQL Agent will expand the string to the server name
    -- Use single quotes so that PS considers the string a literal and will not
    -- try to expand the $ reference and the script will not fail in a test environment
    -- Note 2: the current approach filters on policy name to identify the resource health policy
    -- this might turn out to be an issue if we start localizing the resource health policies.

    SET @execute_policy_script = N'$serverName = ''$(ESCAPE_SQUOTE(SRVR))'';
                                   $path = Convert-UrnToPath "PolicyStore[@Name=`''$serverName`'']";
                                   dir $path\Policies -FORCE | where { $_.IsSystemObject -eq $true -and $_.Name -like ''Utility*'' } | Invoke-PolicyEvaluation -TargetServerName $serverName;'
    
    EXEC msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Execute resource health policy evaluation job', 
            @step_id=2, 
            @cmdexec_success_code=0, 
            @on_success_action=3, 
            @on_success_step_id=0, 
            @on_fail_action=2, 
            @on_fail_step_id=0, 
            @retry_attempts=0, 
            @retry_interval=0, 
            @os_run_priority=0, @subsystem=N'PowerShell', 
            @command=@execute_policy_script, 
            @database_name=N'msdb', 
            @flags=0
               
    EXEC msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Compute resource health states', 
            @step_id=3, 
            @cmdexec_success_code=0, 
            @on_success_action=1, 
            @on_success_step_id=0, 
            @on_fail_action=2, 
            @on_fail_step_id=0, 
            @retry_attempts=0, 
            @retry_interval=0, 
            @os_run_priority=0, @subsystem=N'TSQL', 
            @command=N'EXEC msdb.dbo.sp_sysutility_ucp_calculate_health', 
            @database_name=N'msdb', 
            @flags=0

   EXEC @ReturnCode = msdb.dbo.sp_update_job @job_id = @jobId, @start_step_id = 1
   IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
   
   EXEC @ReturnCode = msdb.dbo.sp_add_jobschedule @job_id=@jobId, @name=N'OccursEvery15Minutes', 
      @enabled=1, 
      @freq_type=4, 
      @freq_interval=1, 
      @freq_subday_type=4, 
      @freq_subday_interval=15, 
      @freq_relative_interval=0, 
      @freq_recurrence_factor=0, 
      @schedule_uid=N'7c3e972b-6e4b-4c61-9061-715d8b9ba531'
   IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
   
   EXEC @ReturnCode = msdb.dbo.sp_add_jobserver @job_id = @jobId, @server_name = N'(local)'
   IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
   
   -----------------------------------------------------------------------
   --  Remove the sysutility_get_cache_tables_data_into_aggregate_tables_hourly job if it is already existing.
   -----------------------------------------------------------------------
   DECLARE @job_id1 UNIQUEIDENTIFIER
   SELECT @job_id1 = job_id 
   FROM msdb.dbo.sysjobs_view 
   WHERE name = N'sysutility_get_cache_tables_data_into_aggregate_tables_hourly'

   IF EXISTS (SELECT job_id FROM msdb.dbo.sysjobs_view WHERE name = N'sysutility_get_cache_tables_data_into_aggregate_tables_hourly')
      EXEC msdb.dbo.sp_delete_job @job_id=@job_id1, @delete_unused_schedule=1
   
   -----------------------------------------------------------------------
   --  sysutility_get_cache_tables_data_into_aggregate_tables_hourly job has following steps:
   -- 1.  Aggregate current days data from [sysutility_ucp_core].[computer_cpu_utilizations_internal] 
   --     and put it into [sysutility_ucp_core].[aggregated_computer_cpu_utilizations_internal].
   -- 2.  Aggregate current days data from [sysutility_ucp_core].[dac_execution_statistics_internal] 
   --     and put it into [sysutility_ucp_core].[aggregated_dac_cpu_utilizations_internal].
   -- 3.  Aggregate current days data storage cache table into
   --     storage aggregation table
   -----------------------------------------------------------------------
   IF NOT EXISTS (SELECT name FROM msdb.dbo.syscategories WHERE name=N'[Uncategorized (Local)]' AND category_class=1)
   BEGIN
      EXEC @ReturnCode = msdb.dbo.sp_add_category @class=N'JOB', @type=N'LOCAL', @name=N'[Uncategorized (Local)]'
      IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
   END

   DECLARE @jobId1 BINARY(16)
   EXEC @ReturnCode =  msdb.dbo.sp_add_job @job_name=N'sysutility_get_cache_tables_data_into_aggregate_tables_hourly', 
         @enabled=1, 
         @notify_level_eventlog=0, 
         @notify_level_email=0, 
         @notify_level_netsend=0, 
         @notify_level_page=0, 
         @delete_level=0,
         @description=N'At every hour''s stroke, the data of the cache tables get aggregated and put into corresponding aggregate by hour tables.', 
         @category_name=N'[Uncategorized (Local)]', 
         @owner_login_name=@CurrentLoggedIn, @job_id = @jobId1 OUTPUT
   IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback

   EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId1, @step_name=N'Aggregate current hours data from the cache tables into the server aggregation table', 
         @step_id=1, 
         @cmdexec_success_code=0, 
         @on_success_action=1, 
         @on_success_step_id=0, 
         @on_fail_action=2, 
         @on_fail_step_id=0, 
         @retry_attempts=0, 
         @retry_interval=0, 
         @os_run_priority=0, @subsystem=N'TSQL', 
         @command=N'DECLARE @now DATETIMEOFFSET(7); SELECT @now = SYSDATETIMEOFFSET(); EXEC [sysutility_ucp_core].sp_copy_cache_table_data_into_aggregate_tables @aggregation_type=1, @endTime=@now',
         @database_name = @mdwDBName,
         @flags=0
   IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
   
   
   EXEC @ReturnCode = msdb.dbo.sp_add_jobschedule @job_id=@jobId1, @name=N'OccursEveryOneHour', 
         @enabled=1, 
         @freq_type=4, 
         @freq_interval=1, 
         @freq_subday_type=8, 
         @freq_subday_interval=1, 
         @freq_relative_interval=0, 
         @freq_recurrence_factor=0,
         @active_start_time=100
   IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
   
   
   -----------------------------------------------------------------------
   --  Remove the sysutility_get_cache_tables_data_into_aggregate_tables_daily job if it is already existing.
   -----------------------------------------------------------------------
   SELECT @job_id1 = job_id 
   FROM msdb.dbo.sysjobs_view 
   WHERE name = N'sysutility_get_cache_tables_data_into_aggregate_tables_daily'

   IF EXISTS (SELECT job_id FROM msdb.dbo.sysjobs_view WHERE name = N'sysutility_get_cache_tables_data_into_aggregate_tables_daily')
      EXEC msdb.dbo.sp_delete_job @job_id=@job_id1, @delete_unused_schedule=1
   
   -----------------------------------------------------------------------
   --  sysutility_get_cache_tables_data_into_aggregate_tables_daily job has following steps:
   -- 1. Aggregate current days data from [sysutility_ucp_core].[computer_cpu_utilizations_internal] 
   --    and put it into [sysutility_ucp_core].[aggregated_computer_cpu_utilizations_internal].
   -- 2. Aggregate current days data from [sysutility_ucp_core].[dac_execution_statistics_internal] 
   --    and put it into [sysutility_ucp_core].[aggregated_dac_cpu_utilizations_internal].
   -- 3. Aggregate current days data from storage cache table into
   --    storage aggregation table
   -----------------------------------------------------------------------
   IF NOT EXISTS (SELECT name FROM msdb.dbo.syscategories WHERE name=N'[Uncategorized (Local)]' AND category_class=1)
   BEGIN
      EXEC @ReturnCode = msdb.dbo.sp_add_category @class=N'JOB', @type=N'LOCAL', @name=N'[Uncategorized (Local)]'
      IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
   END

   DECLARE @jobId2 BINARY(16)
   EXEC @ReturnCode =  msdb.dbo.sp_add_job @job_name=N'sysutility_get_cache_tables_data_into_aggregate_tables_daily', 
         @enabled=1, 
         @notify_level_eventlog=0, 
         @notify_level_email=0, 
         @notify_level_netsend=0, 
         @notify_level_page=0, 
         @delete_level=0, 
         @description=N'At every 12:01 AM stroke, the data of the cache tables get aggregated and put into corresponding aggregate by day tables.', 
         @category_name=N'[Uncategorized (Local)]', 
         @owner_login_name=@CurrentLoggedIn, @job_id = @jobId2 OUTPUT
   IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback

   EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId2, @step_name=N'Aggregate current days data from the cache tables into the server aggregation table', 
         @step_id=1, 
         @cmdexec_success_code=0, 
         @on_success_action=3, 
         @on_success_step_id=0, 
         @on_fail_action=2, 
         @on_fail_step_id=0, 
         @retry_attempts=0, 
         @retry_interval=0, 
         @os_run_priority=0, @subsystem=N'TSQL', 
         @command=N'DECLARE @now DATETIME; SELECT @now = GETUTCDATE(); EXEC [sysutility_ucp_core].sp_copy_cache_table_data_into_aggregate_tables @aggregation_type=2, @endTime=@now',
         @database_name = @mdwDBName,
         @flags=0
   IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
   
   
   EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId2, @step_name=N'Purge cache table history data based on retention period', 
      @step_id=2, 
      @cmdexec_success_code=0, 
      @on_success_action=3, 
      @on_success_step_id=0, 
      @on_fail_action=2, 
      @on_fail_step_id=0, 
      @retry_attempts=0, 
      @retry_interval=0, 
      @os_run_priority=0, @subsystem=N'TSQL', 
      @command=N'EXEC [sysutility_ucp_core].[sp_purge_cache_tables];', 
      @database_name = @mdwDBName,
      @flags=0
   IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback

   EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId2, @step_name=N'Purge resource health policy evaluation history based on trailing window', 
      @step_id=3, 
      @cmdexec_success_code=0, 
      @on_success_action=1, 
      @on_success_step_id=0, 
      @on_fail_action=2, 
      @on_fail_step_id=0, 
      @retry_attempts=0, 
      @retry_interval=0, 
      @os_run_priority=0, @subsystem=N'TSQL', 
      @command=N'EXEC msdb.dbo.sp_sysutility_ucp_delete_policy_history', 
      @database_name=N'msdb', 
      @flags=0      
   IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback

   EXEC @ReturnCode = msdb.dbo.sp_update_job @job_id = @jobId1, @start_step_id = 1
   IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback

   EXEC @ReturnCode = msdb.dbo.sp_add_jobschedule @job_id=@jobId2, @name=N'OccursOnceADayAt12:01AM', 
         @enabled=1, 
         @freq_type=4, 
         @freq_interval=1, 
         @freq_subday_type=1, 
         @freq_subday_interval=0, 
         @freq_relative_interval=0, 
         @freq_recurrence_factor=0, 
         @active_start_date=20080218, 
         @active_end_date=99991231, 
         @active_start_time=100, 
         @active_end_time=235959, 
         @schedule_uid=N'acb4d2d5-d2ee-4d33-b82e-a296a41fc225'
   IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback

   EXEC @ReturnCode = msdb.dbo.sp_add_jobserver @job_id = @jobId1, @server_name = N'(local)'
   IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
   
   EXEC @ReturnCode = msdb.dbo.sp_add_jobserver @job_id = @jobId2, @server_name = N'(local)'
   IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback


-- Commit our transaction
COMMIT TRANSACTION

GOTO EndSave
QuitWithRollback:
IF (@@TRANCOUNT > 0) ROLLBACK TRANSACTION
EndSave:
GO

-- Commit our transaction
COMMIT TRANSACTION
GO


/**********************************************************************/
/* Setup Utility object permissions                                   */
/**********************************************************************/

/**********************************************************************
   Create the UtilityMDWWriter role. This role is granted to proxy 
   accounts that run the DC upload step on each MI.  It allows these 
   accounts to insert data into the Utility "live" tables in MDW. 
***********************************************************************/
RAISERROR ('Create UtilityMDWWriter role...', 0, 1) WITH NOWAIT;
IF ( NOT EXISTS (SELECT * FROM sys.database_principals 
                    WHERE name = N'UtilityMDWWriter' AND type = 'R'))
BEGIN
    CREATE ROLE [UtilityMDWWriter]
END
ELSE -- if the role exists check to see if it has members
BEGIN
    IF NOT EXISTS (SELECT rm.member_principal_id
                FROM sys.database_principals dp 
                INNER JOIN sys.database_role_members rm ON rm.role_principal_id = dp.principal_id
                WHERE name = N'UtilityMDWWriter' AND type = 'R')
    BEGIN
        -- if the role has no members drop and recreate it
        DROP ROLE [UtilityMDWWriter]
        CREATE ROLE [UtilityMDWWriter]
    END
END
GO

-- UtilityMDWWriter is a member of the mdw_writer role; this will give it 
-- INSERT, EXEC, and SELECT permissions on anything in the [snapshots] schema. 
EXECUTE sp_addrolemember @rolename = 'mdw_writer' , 
                   @membername = 'UtilityMDWWriter' 
GO


/**********************************************************************
   Create the UtilityMDWCacheReader role. This sysutility_mdw role       
   corresponds to the UtilityCMRReader role in msdb.  A "CMRReader" 
   can select from objects in msdb that expose data in cache tables 
   in sysutility_mdw. The "CacheReader" role in sysutility_mdw secures 
   these cache tables/functions in the MDW db. A login should be a 
   member of both roles in order to run the UCP data processing job. 
***********************************************************************/
RAISERROR ('Create UtilityMDWCacheReader role...', 0, 1) WITH NOWAIT;
IF ( NOT EXISTS (SELECT * FROM sys.database_principals 
                    WHERE name = N'UtilityMDWCacheReader' AND type = 'R'))
BEGIN
    CREATE ROLE [UtilityMDWCacheReader];
END
ELSE -- if the role exists check to see if it has members
BEGIN
    IF NOT EXISTS (SELECT rm.member_principal_id
                FROM sys.database_principals dp 
                INNER JOIN sys.database_role_members rm ON rm.role_principal_id = dp.principal_id
                WHERE name = N'UtilityMDWCacheReader' AND type = 'R')
    BEGIN
        -- if the role has no members drop and recreate it
        DROP ROLE [UtilityMDWCacheReader];
        CREATE ROLE [UtilityMDWCacheReader];
    END;
END;
GO

-- Grant SELECT or EXECUTE on the cache functions/views at the boundary of msdb and 
-- sysutility_mdw.  These objects are directly referenced by objects in msdb.  
RAISERROR ('Granting permission on MDW cache objects to UtilityMDWCacheReader role', 0, 1) WITH NOWAIT;

-- Dimensions
GRANT SELECT  ON [sysutility_ucp_core].[latest_dacs]            TO [UtilityMDWCacheReader];
GRANT SELECT  ON [sysutility_ucp_core].[latest_computers]       TO [UtilityMDWCacheReader];
GRANT SELECT  ON [sysutility_ucp_core].[latest_volumes]         TO [UtilityMDWCacheReader];

GRANT SELECT  ON [sysutility_ucp_core].[latest_smo_servers]     TO [UtilityMDWCacheReader];
GRANT SELECT  ON [sysutility_ucp_core].[latest_databases]       TO [UtilityMDWCacheReader];
GRANT SELECT  ON [sysutility_ucp_core].[latest_filegroups]      TO [UtilityMDWCacheReader];
GRANT SELECT  ON [sysutility_ucp_core].[latest_datafiles]       TO [UtilityMDWCacheReader];
GRANT SELECT  ON [sysutility_ucp_core].[latest_logfiles]        TO [UtilityMDWCacheReader];

-- Measures
GRANT SELECT  ON [sysutility_ucp_core].[cpu_utilization]        TO [UtilityMDWCacheReader];
GRANT SELECT  ON [sysutility_ucp_core].[space_utilization]      TO [UtilityMDWCacheReader];

GO

-- Put the database back into multi user mode
RAISERROR('', 0, 1)  WITH NOWAIT;
RAISERROR('Restoring database to multi user mode', 0, 1)  WITH NOWAIT;
DECLARE @dbname sysname
SET @dbname = QUOTENAME(DB_NAME())

DECLARE @sql_db_multi_mode nvarchar(256)
SET @sql_db_multi_mode = 'ALTER DATABASE ' + @dbname +
                                    ' SET MULTI_USER WITH ROLLBACK IMMEDIATE'
EXEC sp_executesql @sql_db_multi_mode

-- check if sync auto stats was on
IF (EXISTS (SELECT * FROM #tmp_auto_mode))  -- if yes, turn it back on
BEGIN
    RAISERROR('', 0, 1)  WITH NOWAIT;
    RAISERROR('Re-enabling asynchronous auto statistics ...', 0, 1)  WITH NOWAIT;
    DECLARE @sql_async_autostat_on nvarchar(256)
    SET @sql_async_autostat_on = 'ALTER DATABASE ' + @dbname +
                                        ' SET AUTO_UPDATE_STATISTICS_ASYNC ON'
    EXEC sp_executesql @sql_async_autostat_on
END
DROP TABLE #tmp_auto_mode

RAISERROR('', 0, 1)  WITH NOWAIT;
RAISERROR('----------------------------------', 0, 1)  WITH NOWAIT;
RAISERROR('Execution of INSTMDW.SQL complete', 0, 1)  WITH NOWAIT;
RAISERROR('----------------------------------', 0, 1)  WITH NOWAIT;
GO


