USE [DD_SYS]
GO

/****** Object:  StoredProcedure [dbo].[PRDD_MIGRATE_DATABASE]    Script Date: 24.12.2025 14:51:05 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO


-- [PRDD_MIGRATE_DATABASE]
-- =================================================================
-- Clone a source database into a new target database with a new collation.
-- The source database remains unchanged; all objects and data are copied.
-- Minimum requirement: MS SQL Server 2016 SP1 (CREATE OR ALTER)
--
-- Returns: INT Value - 0 = Everything worked well
-- =================================================================
-- SUPPORTED OBJECTS:
--   Schemas, Partition Functions/Schemes, Sequences, User-Defined Types,
--   Table Types, Tables (with Collation conversion), Data, Default/Check
--   Constraints, Primary/Unique Keys, Indexes, Foreign Keys, Triggers,
--   Views, Functions, Stored Procedures, Statistics, Synonyms
--
-- NOT SUPPORTED (must be migrated manually):
--   - Memory-optimized tables (In-Memory OLTP)
--   - Filestream columns
--   - Filetables
--   - XML Schema Collections
--   - Full-Text Indexes / Full-Text Catalogs
--   - Extended Properties
--   - Database-level Triggers (DDL Triggers)
--   - CLR Assemblies
--   - Service Broker objects
--   - Linked Server references
--   - Database Mail / Alerts / Jobs
--   - Triggers/Views/Functions/Procedures with external database references (3-part names like [OtherDB].[dbo].[Table])
--     -> These objects are SKIPPED with WARN logging to avoid collation conflicts
--     -> References to the SOURCE database are automatically replaced with TARGET database
-- =================================================================
-- Copyright (c) 2025 by Digital Data GmbH
--
-- Digital Data GmbH • Ludwig-Rinn-Strasse 16 • D-35452 Heuchelheim
-- Tel.: 0641/202360 • E-Mail: info-flow@digitaldata.works
-- =================================================================
-- Creation Date / Author:    22.12.2025 / MK
-- Version Date / Editor:     24.12.2025 / MK
-- Version Number:            1.1.4.0
-- =================================================================
-- History:
-- 22.12.2025 / MK - First Version
-- 23.12.2025 / MK - Added @pPREVALIDATE parameter for SQL syntax pre-validation
-- 23.12.2025 / MK - Code Review fixes: Phase names, parameter output, skip phases
-- 23.12.2025 / MK - Code Review fixes: @PREVALIDATE default=1, kill_cursor error handling,
--                   Temporal Tables/Columnstore Index warnings, LOGLEVEL validation
-- 23.12.2025 / MK - Pre-validation fix: USE [TargetDB] replaced with USE [SourceDB] for
--                   syntax validation when target database doesn't exist yet
-- 24.12.2025 / MK - Bugfix: #SkippedObjects PRIMARY KEY violation when object has multiple
--                   external DB references - added IGNORE_DUP_KEY and NOT EXISTS checks
-- 24.12.2025 / MK - @pNEWCOLLATION now optional - if NULL, source collation is used
--                   (allows database cloning without collation change)

CREATE OR ALTER   PROCEDURE [dbo].[PRDD_MIGRATE_DATABASE] (
	@pDATABASE           NVARCHAR(128)  = NULL,     -- Source database (unchanged)
	@pTARGETDATABASE     NVARCHAR(128)  = NULL,     -- Target database to create
	@pNEWCOLLATION       NVARCHAR(128)  = NULL,     -- New collation for target (e.g. 'Latin1_General_CI_AS'), NULL = use source collation
	@pMODE               NVARCHAR(10)   = 'FULL',   -- 'FULL' = All, 'SCHEMA' = Structure only, 'DATA' = Data only
	@pEXECUTE            BIT            = 0,        -- 0 = Only output commands, 1 = Execute commands
	@pDROPTARGETIFEXISTS BIT            = 0,        -- 1 = Drop target DB if it already exists
	@pSTOPONERROR        BIT            = 1,        -- Stop execution on first error when @pEXECUTE = 1
	@pBATCHSIZE          INT            = 1000000,  -- Batch size for data copy (0 = no batching, default 1000000)
	@pDATAPATH           NVARCHAR(260)  = NULL,     -- Optional: Path for MDF data file (e.g. 'D:\Data\')
	@pLOGPATH            NVARCHAR(260)  = NULL,     -- Optional: Path for LDF log file (e.g. 'D:\Logs\')
	@pEXCLUDEOBJECTS     NVARCHAR(MAX)  = NULL,     -- Comma-separated list of objects to exclude (supports * as wildcard for LIKE)
	@pVALIDATE           NVARCHAR(10)   = 'NONE',   -- 'ALL' = Validate schema+data, 'SCHEMA' = Only objects, 'DATA' = Only row counts, 'NONE' = Skip
	@pPREVALIDATE        BIT            = 1,        -- 1 = Validate all SQL commands syntax BEFORE execution (default), 0 = Skip pre-validation
	@pLOGLEVEL           NVARCHAR(25)   = 'INFO',   -- Set to 'INFO','WARN','ERROR' or NULL to disable logging
	@pDEBUG              BIT            = 0         -- 0 = No debug output, 1 = Print debug messages
)
AS
BEGIN

	--=================================================-- Set session options --==================================================--
	SET NOCOUNT ON;

	SET ANSI_NULLS ON;
	SET QUOTED_IDENTIFIER ON;
	SET ANSI_PADDING ON;
	SET ANSI_WARNINGS ON;
	SET CONCAT_NULL_YIELDS_NULL ON;
	SET ARITHABORT ON;
	SET NUMERIC_ROUNDABORT OFF;
	----------------------------------------------------------------------------------------------------------------------------

	--=========================================-- Parameter copies (avoid sniffing) --========================================--
	DECLARE @SOURCEDATABASE      NVARCHAR(128)  = NULLIF(LTRIM(RTRIM(ISNULL(@pDATABASE,N''))),N''),
	        @TARGETDATABASE      NVARCHAR(128)  = NULLIF(LTRIM(RTRIM(ISNULL(@pTARGETDATABASE,N''))),N''),
	        @NEWCOLLATION        NVARCHAR(128)  = NULLIF(LTRIM(RTRIM(ISNULL(@pNEWCOLLATION,N''))),N''),
	        @MODE                NVARCHAR(10)   = UPPER(ISNULL(NULLIF(LTRIM(RTRIM(@pMODE)),N''), N'FULL')),
	        @EXECUTECOMMANDS     BIT            = ISNULL(@pEXECUTE,0),
	        @DROPTARGETIFEXISTS  BIT            = ISNULL(@pDROPTARGETIFEXISTS,0),
	        @STOPONERROR         BIT            = ISNULL(@pSTOPONERROR,1),
	        @BATCHSIZE           INT            = CASE WHEN ISNULL(@pBATCHSIZE,1000000) < 1 THEN 0 ELSE ISNULL(@pBATCHSIZE,1000000) END,
	        @DATAFILEPATH        NVARCHAR(260)  = NULLIF(LTRIM(RTRIM(ISNULL(@pDATAPATH,N''))),N''),
	        @LOGFILEPATH         NVARCHAR(260)  = NULLIF(LTRIM(RTRIM(ISNULL(@pLOGPATH,N''))),N''),
	        @EXCLUDEOBJECTS      NVARCHAR(MAX)  = NULLIF(LTRIM(RTRIM(ISNULL(@pEXCLUDEOBJECTS,N''))),N''),
	        @VALIDATION_MODE     NVARCHAR(10)   = UPPER(ISNULL(NULLIF(LTRIM(RTRIM(@pVALIDATE)),N''), N'NONE')),
	        @PREVALIDATE         BIT            = ISNULL(@pPREVALIDATE, 1),
	        @LOGLEVEL            NVARCHAR(25)   = CASE WHEN @pLOGLEVEL IS NULL THEN NULL ELSE UPPER(LTRIM(RTRIM(@pLOGLEVEL))) END,
	        @DEBUG               BIT            = ISNULL(@pDEBUG, 0);
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Declare runtime variables --========================================--
	DECLARE @MY_PROCEDURE_NAME            NVARCHAR(128)  	= OBJECT_NAME(@@PROCID);
	
	-- Platzhalter für dynamisches SQL (vermeidet komplexes Apostrophe-Escaping)
	DECLARE @PH_SOURCEDB                  NVARCHAR(20)   	= N'{#SOURCEDB#}',      -- Platzhalter für Quell-Datenbank (unquoted)
	        @PH_TARGETDB                  NVARCHAR(20)   	= N'{#TARGETDB#}',      -- Platzhalter für Ziel-Datenbank (unquoted)
	        @PH_SOURCEDB_QUOTED           NVARCHAR(20)   	= N'{#SOURCEDB_Q#}',    -- Platzhalter für [Quell-Datenbank] (quoted)
	        @PH_TARGETDB_QUOTED           NVARCHAR(20)   	= N'{#TARGETDB_Q#}',    -- Platzhalter für [Ziel-Datenbank] (quoted)
	        @PH_BATCHSIZE                 NVARCHAR(20)   	= N'{#BATCHSIZE#}';     -- Platzhalter für Batch-Größe

	DECLARE @RUN_ID                       UNIQUEIDENTIFIER	= NEWID(),
	        @STARTTIME                    DATETIME       	= GETDATE(),
	        @STEPSTARTTIME                DATETIME       	= NULL,
	        @ROWCOUNT_AFFECTED            BIGINT         	= 0,
	        @TOTAL_ROWS_MIGRATED          BIGINT         	= 0,
	        @SOURCEDATABASE_QUOTED        SYSNAME        	= NULL,
	        @TARGETDATABASE_QUOTED        SYSNAME        	= NULL,
	        @DETECTED_SOURCE_COLLATION    SYSNAME        	= NULL,
	        @DETECTSQL                    NVARCHAR(MAX)  	= NULL,
	        @COMPATIBILITY_LEVEL          INT            	= NULL,
	        @DYNAMICSQL                   NVARCHAR(MAX)  	= NULL,
	        @CURRENTSTEP                  INT            	= NULL,
	        @CURRENTPHASE                 NVARCHAR(50)   	= NULL,
	        @CURRENTCOMMAND               NVARCHAR(MAX)  	= NULL,
	        @ERRORMESSAGE                 NVARCHAR(4000) 	= NULL,
	        @SOURCE_USER_ACCESS_ORIGINAL  NVARCHAR(20)   	= NULL,
	        @KILLSESSIONID                INT            	= NULL,
	        @VALIDATION_PASSED            BIT            	= 1,
	        @VALIDATION_ERRORS            INT            	= 0,
	        @VALIDATION_WARNINGS          INT            	= 0,
	        @VALIDATION_SQL               NVARCHAR(MAX)  	= NULL,
	        @VALIDATION_CURSOR_SQL        NVARCHAR(MAX)  	= NULL,
	        @VALIDATION_SOURCE_COUNT      INT            	= 0,
	        @VALIDATION_TARGET_COUNT      INT            	= 0,
	        @VALIDATION_OBJECT_TYPE       NVARCHAR(50)   	= NULL,
	        @VALIDATION_TABLE_NAME        NVARCHAR(256)  	= NULL,
	        @VALIDATION_SCHEMA_NAME       NVARCHAR(128)  	= NULL,
	        @VALIDATION_SOURCE_ROWS       BIGINT         	= 0,
	        @VALIDATION_TARGET_ROWS       BIGINT         	= 0,
	        @SOURCE_LOCKED                BIT            	= 0,
	        @HAS_SOURCE_CONTROL           BIT            	= 0,
	        @HAS_CREATE_DATABASE          BIT            	= 0,
	        @HAS_ALTER_ANY_DATABASE       BIT            	= 0,
	        @HAS_VIEW_SERVER_STATE        BIT            	= 0,
	        @PERMISSION_SQL               NVARCHAR(500)  	= NULL,
	        @MISSING_PERMISSIONS          NVARCHAR(MAX)  	= N'',
	        @RETURN_STATUS                INT            	= 0,
	        @RETURN_STATUS_TEXT           NVARCHAR(MAX)  	= 'START PROCEDURE [' + @MY_PROCEDURE_NAME + '] @ ' + CONVERT(VARCHAR(50),GETDATE(),120);

	DECLARE @tmp                          TABLE (x INT);
	----------------------------------------------------------------------------------------------------------------------------

	BEGIN TRY

	--=================================================-- Output parameters --================================================--
	PRINT '====================================================================================================';
	PRINT @RETURN_STATUS_TEXT;
	PRINT '';
	PRINT 'PARAMETER01 - @DATABASE (source):        ' + ISNULL(@SOURCEDATABASE, '(NULL)');
	PRINT 'PARAMETER02 - @TARGETDATABASE:           ' + ISNULL(@TARGETDATABASE, '(NULL)');
	PRINT 'PARAMETER03 - @NEWCOLLATION:             ' + ISNULL(@NEWCOLLATION, '(NULL)');
	PRINT 'PARAMETER04 - @MODE:                     ' + @MODE + ' (FULL=All, SCHEMA=Structure, DATA=Data only)';
	PRINT 'PARAMETER05 - @EXECUTE:                  ' + CONVERT(NVARCHAR(1),@EXECUTECOMMANDS);
	PRINT 'PARAMETER06 - @DROPTARGETIFEXISTS:       ' + CONVERT(NVARCHAR(1),@DROPTARGETIFEXISTS);
	PRINT 'PARAMETER07 - @STOPONERROR:              ' + CONVERT(NVARCHAR(1),@STOPONERROR);
	PRINT 'PARAMETER08 - @BATCHSIZE:                ' + CASE WHEN @BATCHSIZE = 0 THEN 'disabled' ELSE CONVERT(NVARCHAR(20),@BATCHSIZE) END;
	PRINT 'PARAMETER09 - @DATAPATH:                 ' + ISNULL(@DATAFILEPATH, '(default)');
	PRINT 'PARAMETER10 - @LOGPATH:                  ' + ISNULL(@LOGFILEPATH, '(default)');
	PRINT 'PARAMETER11 - @EXCLUDEOBJECTS:           ' + ISNULL(@EXCLUDEOBJECTS, '(none)');
	PRINT 'PARAMETER12 - @VALIDATE:                 ' + @VALIDATION_MODE + ' (ALL=Schema+Data, SCHEMA=Objects, DATA=Rows, NONE=Skip)';
	PRINT 'PARAMETER13 - @PREVALIDATE:              ' + CONVERT(NVARCHAR(1), @PREVALIDATE) + ' (1=Syntax check before execution)';
	PRINT 'PARAMETER14 - @LOGLEVEL:                 ' + ISNULL(@LOGLEVEL, '(disabled)');
	PRINT 'PARAMETER15 - @DEBUG:                    ' + CONVERT(NVARCHAR(1), @DEBUG);
	PRINT '';
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Prepare the log table --===========================================--
	IF (@LOGLEVEL IS NOT NULL) BEGIN
		IF EXISTS (SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE [TABLE_NAME] = N'TBDD_MIGRATE_DATABASE_LOG') BEGIN
			PRINT 'INFO: Log table [TBDD_MIGRATE_DATABASE_LOG] already exists.';
		END; ELSE BEGIN
			PRINT 'INFO: Log table does not exist, creating [TBDD_MIGRATE_DATABASE_LOG]...';

			CREATE TABLE [dbo].[TBDD_MIGRATE_DATABASE_LOG] (
				[LOG_ID]           BIGINT IDENTITY(1,1) NOT NULL,
				[RUN_ID]           UNIQUEIDENTIFIER     NOT NULL,
				[LOG_LEVEL]        NVARCHAR(25)         NOT NULL,
				[STEP]             INT                  NULL,
				[PHASE]            NVARCHAR(50)         NULL,
				[SOURCE_DATABASE]  NVARCHAR(128)        NULL,
				[TARGET_DATABASE]  NVARCHAR(128)        NULL,
				[NEW_COLLATION]    NVARCHAR(128)        NULL,
				[MESSAGE]          NVARCHAR(MAX)        NOT NULL,
				[COMMAND]          NVARCHAR(MAX)        NULL,
				[ROWS_AFFECTED]    BIGINT               NULL,
				[DURATION_MS]      BIGINT               NULL,
				[ERROR_MESSAGE]    NVARCHAR(MAX)        NULL,
				[ADDED_WHO]        NVARCHAR(128)        NOT NULL,
				[ADDED_WHEN]       DATETIME             NOT NULL,
				CONSTRAINT [PK_TBDD_MIGRATE_DATABASE_LOG_ID] PRIMARY KEY CLUSTERED
			(			
				[LOG_ID] ASC
			) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, DATA_COMPRESSION = PAGE) ON [PRIMARY]
			) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY];

			ALTER TABLE [dbo].[TBDD_MIGRATE_DATABASE_LOG] ADD CONSTRAINT [DF_TBDD_MIGRATE_DATABASE_LOG_ADDED_WHO]  DEFAULT (SUSER_SNAME()) FOR [ADDED_WHO];
			ALTER TABLE [dbo].[TBDD_MIGRATE_DATABASE_LOG] ADD CONSTRAINT [DF_TBDD_MIGRATE_DATABASE_LOG_ADDED_WHEN] DEFAULT (SYSDATETIME()) FOR [ADDED_WHEN];

			--=================================================-- Create index for efficient queries by RUN_ID --============--
			CREATE NONCLUSTERED INDEX [IX_TBDD_MIGRATE_DATABASE_LOG_RUN_ID] ON [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID] ASC);

			PRINT 'INFO: Log table created successfully.';
		END;
	END; ELSE BEGIN
		PRINT 'WARN: Logging to table is disabled (@pLOGLEVEL = NULL).';
	END;
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Check for log table --=============================================--
	IF (@LOGLEVEL IS NOT NULL) AND NOT EXISTS (SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE [TABLE_NAME] = N'TBDD_MIGRATE_DATABASE_LOG') BEGIN
		SET @LOGLEVEL = NULL;
		PRINT 'ERROR: Log table [TBDD_MIGRATE_DATABASE_LOG] does not exist. Logging disabled for this run!';
	END;
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Log procedure start --=============================================--
	IF (@LOGLEVEL IN ('INFO')) BEGIN
		INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ADDED_WHO], [ADDED_WHEN])
		VALUES (@RUN_ID, 'INFO', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, @RETURN_STATUS_TEXT, @MY_PROCEDURE_NAME, GETDATE());
	END;
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Validate parameters --=============================================--
	IF (@SOURCEDATABASE IS NULL) BEGIN
		IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'ERROR', 'Parameter validation failed', 'Parameter @pDATABASE (source) is required!', @MY_PROCEDURE_NAME, GETDATE());
		END;
		RAISERROR('ERROR: Parameter @pDATABASE (source) is required!',16,1);
		RETURN 1;
	END;

	IF (@TARGETDATABASE IS NULL) BEGIN
		IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'ERROR', @SOURCEDATABASE, 'Parameter validation failed', 'Parameter @pTARGETDATABASE is required!', @MY_PROCEDURE_NAME, GETDATE());
		END;
		RAISERROR('ERROR: Parameter @pTARGETDATABASE is required!',16,1);
		RETURN 1;
	END;

	IF (DB_ID(@SOURCEDATABASE) IS NULL) BEGIN
		IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Parameter validation failed', 'Source database not found: ' + @SOURCEDATABASE, @MY_PROCEDURE_NAME, GETDATE());
		END;
		RAISERROR('ERROR: Source database not found: %s',16,1,@SOURCEDATABASE);
		RETURN 1;
	END;

	IF (@NEWCOLLATION IS NOT NULL) AND NOT EXISTS (SELECT 1 FROM sys.fn_helpcollations() WHERE [name] = @NEWCOLLATION) BEGIN
		IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Parameter validation failed', 'Unknown collation: ' + @NEWCOLLATION, @MY_PROCEDURE_NAME, GETDATE());
		END;
		RAISERROR('ERROR: Unknown collation: %s',16,1,@NEWCOLLATION);
		RETURN 1;
	END;

	IF (@SOURCEDATABASE = @TARGETDATABASE) BEGIN
		IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Parameter validation failed', 'Source and target database must differ.', @MY_PROCEDURE_NAME, GETDATE());
		END;
		RAISERROR('ERROR: Source and target database must differ.',16,1);
		RETURN 1;
	END;

	IF (DB_ID(@TARGETDATABASE) IS NOT NULL AND @DROPTARGETIFEXISTS = 0) BEGIN
		IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Parameter validation failed', 'Target database already exists. Set @pDROPTARGETIFEXISTS=1 to drop it.', @MY_PROCEDURE_NAME, GETDATE());
		END;
		RAISERROR('ERROR: Target database already exists. Set @pDROPTARGETIFEXISTS=1 to drop it.',16,1);
		RETURN 1;
	END;

	IF (@MODE NOT IN ('FULL', 'SCHEMA', 'DATA')) BEGIN
		IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Parameter validation failed', 'Invalid @pMODE. Use FULL, SCHEMA, or DATA.', @MY_PROCEDURE_NAME, GETDATE());
		END;
		RAISERROR('ERROR: Invalid @pMODE. Use FULL, SCHEMA, or DATA.',16,1);
		RETURN 1;
	END;

	IF (@VALIDATION_MODE NOT IN ('ALL', 'SCHEMA', 'DATA', 'NONE')) BEGIN
		IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Parameter validation failed', 'Invalid @pVALIDATE. Use ALL, SCHEMA, DATA, or NONE.', @MY_PROCEDURE_NAME, GETDATE());
		END;
		RAISERROR('ERROR: Invalid @pVALIDATE. Use ALL, SCHEMA, DATA, or NONE.',16,1);
		RETURN 1;
	END;

	IF (@LOGLEVEL IS NOT NULL AND @LOGLEVEL NOT IN ('INFO', 'WARN', 'ERROR')) BEGIN
		RAISERROR('ERROR: Invalid @pLOGLEVEL. Use INFO, WARN, ERROR, or NULL to disable.',16,1);
		RETURN 1;
	END;

	-- DATA mode requires target database to exist
	IF (@MODE = 'DATA' AND DB_ID(@TARGETDATABASE) IS NULL) BEGIN
		IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Parameter validation failed', 'DATA mode requires target database to exist.', @MY_PROCEDURE_NAME, GETDATE());
		END;
		RAISERROR('ERROR: DATA mode requires target database to exist. Run with @pMODE=SCHEMA first.',16,1);
		RETURN 1;
	END;

	SET @SOURCEDATABASE_QUOTED = QUOTENAME(@SOURCEDATABASE);
	SET @TARGETDATABASE_QUOTED = QUOTENAME(@TARGETDATABASE);
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Check required permissions --======================================--
	SET @MISSING_PERMISSIONS = N'';

	-- Check CONTROL permission on source database (required to read all objects and lock DB)
	SET @PERMISSION_SQL = N'USE {#SOURCEDB_Q#}; SELECT @result = ISNULL(HAS_PERMS_BY_NAME(DB_NAME(), ''DATABASE'', ''CONTROL''), 0);';
	SET @PERMISSION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@PERMISSION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);
	EXEC sys.sp_executesql @PERMISSION_SQL, N'@result BIT OUTPUT', @result = @HAS_SOURCE_CONTROL OUTPUT;

	-- Check CREATE DATABASE permission on server level
	SET @HAS_CREATE_DATABASE = ISNULL(HAS_PERMS_BY_NAME(NULL, NULL, 'CREATE ANY DATABASE'), 0);

	-- Check ALTER ANY DATABASE permission (required to set RESTRICTED_USER, RECOVERY model, etc.)
	SET @HAS_ALTER_ANY_DATABASE = ISNULL(HAS_PERMS_BY_NAME(NULL, NULL, 'ALTER ANY DATABASE'), 0);

	-- Check VIEW SERVER STATE permission (required for sys.dm_exec_sessions to kill connections)
	SET @HAS_VIEW_SERVER_STATE = ISNULL(HAS_PERMS_BY_NAME(NULL, NULL, 'VIEW SERVER STATE'), 0);

	-- Build list of missing permissions
	IF @HAS_SOURCE_CONTROL = 0 
		SET @MISSING_PERMISSIONS = @MISSING_PERMISSIONS + N'CONTROL on [' + @SOURCEDATABASE + N'], ';
	IF @HAS_CREATE_DATABASE = 0 AND @MODE IN ('FULL', 'SCHEMA')
		SET @MISSING_PERMISSIONS = @MISSING_PERMISSIONS + N'CREATE ANY DATABASE, ';
	IF @HAS_ALTER_ANY_DATABASE = 0 
		SET @MISSING_PERMISSIONS = @MISSING_PERMISSIONS + N'ALTER ANY DATABASE, ';
	IF @HAS_VIEW_SERVER_STATE = 0 
		SET @MISSING_PERMISSIONS = @MISSING_PERMISSIONS + N'VIEW SERVER STATE, ';

	-- Remove trailing comma and space
	IF LEN(@MISSING_PERMISSIONS) > 2
		SET @MISSING_PERMISSIONS = LEFT(@MISSING_PERMISSIONS, LEN(@MISSING_PERMISSIONS) - 2);

	-- Report missing permissions
	IF @MISSING_PERMISSIONS <> N'' BEGIN
		IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Permission check failed', 'Missing permissions: ' + @MISSING_PERMISSIONS, @MY_PROCEDURE_NAME, GETDATE());
		END;
		RAISERROR('ERROR: Missing required permissions: %s', 16, 1, @MISSING_PERMISSIONS);
		RETURN 1;
	END;

	PRINT 'INFO: Permission check passed.';
	IF @DEBUG = 1 BEGIN
		PRINT 'DEBUG: CONTROL on source DB: ' + CASE WHEN @HAS_SOURCE_CONTROL = 1 THEN 'YES' ELSE 'NO' END;
		PRINT 'DEBUG: CREATE ANY DATABASE: ' + CASE WHEN @HAS_CREATE_DATABASE = 1 THEN 'YES' ELSE 'NO' END;
		PRINT 'DEBUG: ALTER ANY DATABASE: ' + CASE WHEN @HAS_ALTER_ANY_DATABASE = 1 THEN 'YES' ELSE 'NO' END;
		PRINT 'DEBUG: VIEW SERVER STATE: ' + CASE WHEN @HAS_VIEW_SERVER_STATE = 1 THEN 'YES' ELSE 'NO' END;
	END;
	PRINT '';
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Parse excluded objects into temp table --=========================--
	CREATE TABLE #ExcludedObjects
	(
		Pattern       NVARCHAR(256) COLLATE DATABASE_DEFAULT NOT NULL,
		IsWildcard    BIT           NOT NULL DEFAULT 0
	);

	IF @EXCLUDEOBJECTS IS NOT NULL BEGIN
		-- Parse comma-separated list and convert * to % for LIKE
		INSERT INTO #ExcludedObjects (Pattern, IsWildcard)
		SELECT 
			REPLACE(LTRIM(RTRIM(value)), N'*', N'%') AS Pattern,
			CASE WHEN CHARINDEX(N'*', value) > 0 THEN 1 ELSE 0 END AS IsWildcard
		FROM STRING_SPLIT(@EXCLUDEOBJECTS, N',')
		WHERE LTRIM(RTRIM(value)) <> N'';

		PRINT 'INFO: Excluded object patterns:';
		DECLARE @ExclPattern NVARCHAR(256);
		DECLARE excl_cursor CURSOR FAST_FORWARD FOR SELECT Pattern FROM #ExcludedObjects;
		OPEN excl_cursor;
		FETCH NEXT FROM excl_cursor INTO @ExclPattern;
		WHILE @@FETCH_STATUS = 0 BEGIN
			PRINT '  - ' + @ExclPattern;
			FETCH NEXT FROM excl_cursor INTO @ExclPattern;
		END;
		CLOSE excl_cursor;
		DEALLOCATE excl_cursor;
		PRINT '';

		IF (@LOGLEVEL IN ('INFO')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'INFO', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Excluded objects: ' + @EXCLUDEOBJECTS, @MY_PROCEDURE_NAME, GETDATE());
		END;
	END;

	-- Create helper function to check if an object is excluded (schema.name format)
	-- We'll use a subquery pattern instead of a function for compatibility
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Normalize paths --=================================================--
	IF @DATAFILEPATH IS NOT NULL AND RIGHT(@DATAFILEPATH, 1) <> N'\' SET @DATAFILEPATH = @DATAFILEPATH + N'\';
	IF @LOGFILEPATH IS NOT NULL AND RIGHT(@LOGFILEPATH, 1) <> N'\' SET @LOGFILEPATH = @LOGFILEPATH + N'\';
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Detect source collation --=========================================--
	SET @DETECTSQL = N'USE {#SOURCEDB_Q#};
	  SELECT @c = CONVERT(sysname, DATABASEPROPERTYEX(DB_NAME(), N''Collation'')),
	         @cl = compatibility_level
	  FROM sys.databases WHERE [name] = DB_NAME();';
	SET @DETECTSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DETECTSQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);
	EXEC sys.sp_executesql @DETECTSQL, N'@c sysname OUTPUT, @cl int OUTPUT', @c = @DETECTED_SOURCE_COLLATION OUTPUT, @cl = @COMPATIBILITY_LEVEL OUTPUT;

	PRINT 'INFO: Source collation: ' + ISNULL(@DETECTED_SOURCE_COLLATION,'(unknown)');
	PRINT 'INFO: Source compatibility level: ' + ISNULL(CONVERT(nvarchar(10),@COMPATIBILITY_LEVEL),'(unknown)');

	-- If no new collation specified, use source collation (migration without collation change)
	IF @NEWCOLLATION IS NULL BEGIN
		SET @NEWCOLLATION = @DETECTED_SOURCE_COLLATION;
		PRINT 'INFO: No target collation specified - using source collation: ' + @NEWCOLLATION;
		IF (@LOGLEVEL IN ('INFO')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'INFO', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'No target collation specified - using source collation', @MY_PROCEDURE_NAME, GETDATE());
		END;
	END;
	PRINT '';
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Basic preflight for unsupported features --=======================--
	-- Check for Memory-optimized tables
	SET @DYNAMICSQL = N'
SELECT TOP (1) 1
FROM {#SOURCEDB_Q#}.sys.tables
WHERE is_memory_optimized = 1;';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
		@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
		@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
		@PH_SOURCEDB, @SOURCEDATABASE),
		@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	DELETE FROM @tmp;
	BEGIN TRY
		INSERT INTO @tmp(x) EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	IF EXISTS (SELECT 1 FROM @tmp) BEGIN
		IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Preflight check failed', 'Memory-optimized tables detected. Handle manually.', @MY_PROCEDURE_NAME, GETDATE());
		END;
		RAISERROR(N'Unsupported: memory-optimized tables detected. Handle manually.', 16, 1);
		RETURN 1;
	END;

	-- Check for Filestream columns
	SET @DYNAMICSQL = N'
SELECT TOP (1) 1
FROM {#SOURCEDB_Q#}.sys.columns AS c
JOIN {#SOURCEDB_Q#}.sys.tables AS t ON t.object_id = c.object_id
WHERE c.is_filestream = 1;';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
		@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
		@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
		@PH_SOURCEDB, @SOURCEDATABASE),
		@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	DELETE FROM @tmp;
	BEGIN TRY
		INSERT INTO @tmp(x) EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	IF EXISTS (SELECT 1 FROM @tmp) BEGIN
		IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Preflight check failed', 'Filestream columns detected. Handle manually.', @MY_PROCEDURE_NAME, GETDATE());
		END;
		RAISERROR(N'Unsupported: Filestream columns detected. Handle manually.', 16, 1);
		RETURN 1;
	END;

	-- Check for Filetables
	SET @DYNAMICSQL = N'
SELECT TOP (1) 1
FROM {#SOURCEDB_Q#}.sys.tables
WHERE is_filetable = 1;';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
		@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
		@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
		@PH_SOURCEDB, @SOURCEDATABASE),
		@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	DELETE FROM @tmp;
	BEGIN TRY
		INSERT INTO @tmp(x) EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	IF EXISTS (SELECT 1 FROM @tmp) BEGIN
		IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Preflight check failed', 'Filetables detected. Handle manually.', @MY_PROCEDURE_NAME, GETDATE());
		END;
		RAISERROR(N'Unsupported: Filetables detected. Handle manually.', 16, 1);
		RETURN 1;
	END;

	-- Check for Temporal Tables (system-versioned)
	SET @DYNAMICSQL = N'
SELECT TOP (1) 1
FROM {#SOURCEDB_Q#}.sys.tables
WHERE temporal_type IN (1, 2);';  -- 1 = History table, 2 = System-versioned table
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
		@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
		@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
		@PH_SOURCEDB, @SOURCEDATABASE),
		@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	DELETE FROM @tmp;
	BEGIN TRY
		INSERT INTO @tmp(x) EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	IF EXISTS (SELECT 1 FROM @tmp) BEGIN
		PRINT N'WARN: Temporal (system-versioned) tables detected. These will be migrated as regular tables without versioning.';
		IF (@LOGLEVEL IN ('INFO', 'WARN')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'WARN', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Temporal tables detected - will be migrated as regular tables without system versioning', @MY_PROCEDURE_NAME, GETDATE());
		END;
	END;

	-- Check for Columnstore Indexes
	SET @DYNAMICSQL = N'
SELECT TOP (1) 1
FROM {#SOURCEDB_Q#}.sys.indexes
WHERE type IN (5, 6);';  -- 5 = Clustered columnstore, 6 = Nonclustered columnstore
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
		@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
		@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
		@PH_SOURCEDB, @SOURCEDATABASE),
		@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	DELETE FROM @tmp;
	BEGIN TRY
		INSERT INTO @tmp(x) EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	IF EXISTS (SELECT 1 FROM @tmp) BEGIN
		PRINT N'WARN: Columnstore indexes detected. These will NOT be migrated - recreate manually after migration.';
		IF (@LOGLEVEL IN ('INFO', 'WARN')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'WARN', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Columnstore indexes detected - NOT migrated, recreate manually', @MY_PROCEDURE_NAME, GETDATE());
		END;
	END;
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Lock source database (RESTRICTED_USER) --=========================--
	-- Save original user access mode
	SELECT @SOURCE_USER_ACCESS_ORIGINAL = user_access_desc
	FROM sys.databases
	WHERE name = @SOURCEDATABASE;

	PRINT 'INFO: Source database original user access mode: ' + ISNULL(@SOURCE_USER_ACCESS_ORIGINAL, '(unknown)');

	-- Kill all active connections to source database (except own session)
	PRINT 'INFO: Killing active connections to source database...';
	BEGIN TRY
		DECLARE kill_cursor CURSOR FAST_FORWARD FOR
			SELECT session_id
			FROM sys.dm_exec_sessions
			WHERE database_id = DB_ID(@SOURCEDATABASE)
			  AND session_id <> @@SPID;

		OPEN kill_cursor;
		FETCH NEXT FROM kill_cursor INTO @KILLSESSIONID;
		WHILE @@FETCH_STATUS = 0 BEGIN
			BEGIN TRY
				SET @DYNAMICSQL = N'KILL ' + CONVERT(NVARCHAR(10), @KILLSESSIONID) + N';';
				PRINT 'INFO: Killing session ' + CONVERT(NVARCHAR(10), @KILLSESSIONID);
				IF @DEBUG = 1 PRINT @DYNAMICSQL;
				EXEC sys.sp_executesql @DYNAMICSQL;
			END TRY BEGIN CATCH
				PRINT 'WARN: Could not kill session ' + CONVERT(NVARCHAR(10), @KILLSESSIONID) + ': ' + ERROR_MESSAGE();
			END CATCH;
			FETCH NEXT FROM kill_cursor INTO @KILLSESSIONID;
		END;
		CLOSE kill_cursor;
		DEALLOCATE kill_cursor;
	END TRY BEGIN CATCH
		-- Ensure cursor is cleaned up even on error
		IF CURSOR_STATUS('local', 'kill_cursor') >= 0 BEGIN
			CLOSE kill_cursor;
			DEALLOCATE kill_cursor;
		END;
		PRINT 'WARN: Error during connection cleanup: ' + ERROR_MESSAGE();
	END CATCH;

	-- Set source database to RESTRICTED_USER mode
	PRINT 'INFO: Setting source database to RESTRICTED_USER mode...';
	BEGIN TRY
		SET @DYNAMICSQL = N'ALTER DATABASE {#SOURCEDB_Q#} SET RESTRICTED_USER WITH ROLLBACK IMMEDIATE;';
		-- Ersetze Platzhalter durch tatsächliche Werte
		SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
		IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
		IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
		BEGIN TRY
			EXEC sys.sp_executesql @DYNAMICSQL;
		END TRY BEGIN CATCH
			PRINT N'ERROR in dynamic SQL:';
			PRINT @DYNAMICSQL;
			THROW;
		END CATCH;
		PRINT 'INFO: Source database is now in RESTRICTED_USER mode.';
		SET @SOURCE_LOCKED = 1;
	END TRY BEGIN CATCH
		SET @ERRORMESSAGE = ERROR_MESSAGE();
		PRINT 'ERROR: Could not set source database to RESTRICTED_USER: ' + @ERRORMESSAGE;
		IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Failed to lock source database', @ERRORMESSAGE, @MY_PROCEDURE_NAME, GETDATE());
		END;
		RAISERROR(N'Failed to set source database to RESTRICTED_USER mode.', 16, 1);
		RETURN 1;
	END CATCH;
	PRINT '';
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Prepare command list --============================================--
	IF @DEBUG = 1 PRINT 'DEBUG: Creating #Commands table...';
	CREATE TABLE #Commands
	(
		Step		int IDENTITY(1,1) NOT NULL PRIMARY KEY,
		Phase		nvarchar(50)	  NOT NULL,
		Command	nvarchar(max)	  NOT NULL
	);
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Collect objects to skip (external DB refs + dependents) --========--
	-- This temp table collects all objects that must be skipped:
	-- 1. Objects with external database references (other than source DB)
	-- 2. Objects that depend on skipped objects (cascade)
	CREATE TABLE #SkippedObjects
	(
		ObjectId     INT            NOT NULL,
		SchemaName   NVARCHAR(128)  NOT NULL,
		ObjectName   NVARCHAR(128)  NOT NULL,
		ObjectType   NVARCHAR(10)   NOT NULL,  -- 'VIEW', 'FUNCTION', 'PROCEDURE', 'TRIGGER'
		SkipReason   NVARCHAR(500)  NOT NULL,
		CONSTRAINT PK_SkippedObjects PRIMARY KEY CLUSTERED (ObjectId) WITH (IGNORE_DUP_KEY = ON)
	);

	-- Step 1: Insert objects with DIRECT external database references
	SET @DYNAMICSQL = N'
	-- Views with external DB references
	INSERT INTO #SkippedObjects (ObjectId, SchemaName, ObjectName, ObjectType, SkipReason)
	SELECT v.object_id, s.name, v.name, N''VIEW'', N''References external database(s)''
	FROM {#SOURCEDB_Q#}.sys.views AS v
	JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = v.schema_id
	WHERE EXISTS (
		SELECT 1 FROM {#SOURCEDB_Q#}.sys.sql_expression_dependencies AS d
		WHERE d.referencing_id = v.object_id
		  AND d.referenced_database_name IS NOT NULL
		  AND d.referenced_database_name <> N''{#SOURCEDB#}''
	)
	  AND NOT EXISTS (SELECT 1 FROM #SkippedObjects WHERE ObjectId = v.object_id);

	-- Functions with external DB references
	INSERT INTO #SkippedObjects (ObjectId, SchemaName, ObjectName, ObjectType, SkipReason)
	SELECT o.object_id, s.name, o.name, N''FUNCTION'', N''References external database(s)''
	FROM {#SOURCEDB_Q#}.sys.objects AS o
	JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = o.schema_id
	WHERE o.[type] IN (N''FN'',N''TF'',N''IF'',N''FS'',N''FT'')
	  AND EXISTS (
		SELECT 1 FROM {#SOURCEDB_Q#}.sys.sql_expression_dependencies AS d
		WHERE d.referencing_id = o.object_id
		  AND d.referenced_database_name IS NOT NULL
		  AND d.referenced_database_name <> N''{#SOURCEDB#}''
	)
	  AND NOT EXISTS (SELECT 1 FROM #SkippedObjects WHERE ObjectId = o.object_id);

	-- Procedures with external DB references
	INSERT INTO #SkippedObjects (ObjectId, SchemaName, ObjectName, ObjectType, SkipReason)
	SELECT p.object_id, s.name, p.name, N''PROCEDURE'', N''References external database(s)''
	FROM {#SOURCEDB_Q#}.sys.procedures AS p
	JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = p.schema_id
	WHERE EXISTS (
		SELECT 1 FROM {#SOURCEDB_Q#}.sys.sql_expression_dependencies AS d
		WHERE d.referencing_id = p.object_id
		  AND d.referenced_database_name IS NOT NULL
		  AND d.referenced_database_name <> N''{#SOURCEDB#}''
	)
	  AND NOT EXISTS (SELECT 1 FROM #SkippedObjects WHERE ObjectId = p.object_id);

	-- Triggers with external DB references (using sys.sql_expression_dependencies)
	INSERT INTO #SkippedObjects (ObjectId, SchemaName, ObjectName, ObjectType, SkipReason)
	SELECT tr.object_id, s.name, tr.name, N''TRIGGER'', N''References external database(s)''
	FROM {#SOURCEDB_Q#}.sys.triggers AS tr
	JOIN {#SOURCEDB_Q#}.sys.objects AS o ON o.object_id = tr.object_id
	JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = o.schema_id
	WHERE tr.parent_id IS NOT NULL
	  AND EXISTS (
		SELECT 1 FROM {#SOURCEDB_Q#}.sys.sql_expression_dependencies AS d
		WHERE d.referencing_id = tr.object_id
		  AND d.referenced_database_name IS NOT NULL
		  AND d.referenced_database_name <> N''{#SOURCEDB#}''
	  )
	  AND NOT EXISTS (SELECT 1 FROM #SkippedObjects WHERE ObjectId = tr.object_id);';
	IF @DEBUG = 1 PRINT 'DEBUG: Collecting objects with external DB references...';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
		@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
		@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
		@PH_SOURCEDB, @SOURCEDATABASE),
		@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;

	-- Step 2: Recursively add dependent objects (cascade)
	-- Loop until no more dependents are found
	DECLARE @ROWCOUNT_ADDED INT = 1;
	WHILE @ROWCOUNT_ADDED > 0 BEGIN
		SET @DYNAMICSQL = N'
		INSERT INTO #SkippedObjects (ObjectId, SchemaName, ObjectName, ObjectType, SkipReason)
		SELECT DISTINCT o.object_id, s.name, o.name,
			CASE 
				WHEN o.[type] = N''V'' THEN N''VIEW''
				WHEN o.[type] IN (N''FN'',N''TF'',N''IF'',N''FS'',N''FT'') THEN N''FUNCTION''
				WHEN o.[type] = N''P'' THEN N''PROCEDURE''
				ELSE N''OTHER''
			END,
			N''Depends on skipped object: '' + sk.SchemaName + N''.'' + sk.ObjectName
		FROM {#SOURCEDB_Q#}.sys.sql_expression_dependencies AS d
		JOIN {#SOURCEDB_Q#}.sys.objects AS o ON o.object_id = d.referencing_id
		JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = o.schema_id
		JOIN #SkippedObjects AS sk ON sk.ObjectId = d.referenced_id
		WHERE o.[type] IN (N''V'', N''FN'', N''TF'', N''IF'', N''FS'', N''FT'', N''P'')
		  AND NOT EXISTS (SELECT 1 FROM #SkippedObjects WHERE ObjectId = o.object_id);';
		-- Ersetze Platzhalter durch tatsächliche Werte
		SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
		IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
		IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
		BEGIN TRY
			EXEC sys.sp_executesql @DYNAMICSQL;
			SET @ROWCOUNT_ADDED = @@ROWCOUNT;  -- Muss sofort nach EXEC gelesen werden
		END TRY BEGIN CATCH
			PRINT N'ERROR in dynamic SQL:';
			PRINT @DYNAMICSQL;
			THROW;
		END CATCH;
		IF @DEBUG = 1 PRINT 'DEBUG: Cascade dependency iteration added ' + CONVERT(NVARCHAR(10), @ROWCOUNT_ADDED) + ' objects.';
	END;

	-- Log count of skipped objects
	DECLARE @SKIP_COUNT INT = (SELECT COUNT(*) FROM #SkippedObjects);
	IF @SKIP_COUNT > 0 BEGIN
		PRINT N'INFO: ' + CONVERT(NVARCHAR(10), @SKIP_COUNT) + N' objects will be SKIPPED due to external database references or dependencies.';
		
		-- Log each skipped object
		IF (@LOGLEVEL IN ('INFO', 'WARN')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] 
				([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			SELECT @RUN_ID, 'WARN', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION,
				N'SKIP ' + ObjectType + N': ' + SchemaName + N'.' + ObjectName + N' - ' + SkipReason,
				@MY_PROCEDURE_NAME, GETDATE()
			FROM #SkippedObjects
			ORDER BY ObjectType, SchemaName, ObjectName;
		END;
	END;
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- 00: Drop target if requested --===================================--
	IF (DB_ID(@TARGETDATABASE) IS NOT NULL AND @DROPTARGETIFEXISTS = 1) BEGIN
		INSERT INTO #Commands(Phase, Command)
		VALUES
		(N'00-DropTarget', N'USE [master]; ALTER DATABASE {#TARGETDB_Q#} SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE {#TARGETDB_Q#};');
	END;
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- 01: Create target database --=====================================--
	IF @DATAFILEPATH IS NULL AND @LOGFILEPATH IS NULL BEGIN
		-- Default paths (SQL Server decides)
		INSERT INTO #Commands(Phase, Command)
		VALUES
		(N'01-CreateDb', N'USE [master]; CREATE DATABASE {#TARGETDB_Q#} COLLATE ' + @NEWCOLLATION + N';');
	END; ELSE BEGIN
		-- Custom paths specified
		DECLARE @MDFFILEPATH  NVARCHAR(520) = ISNULL(@DATAFILEPATH, N'') + @TARGETDATABASE + N'.mdf';
		DECLARE @LDFFILEPATH  NVARCHAR(520) = ISNULL(@LOGFILEPATH, @DATAFILEPATH) + @TARGETDATABASE + N'_log.ldf';
		
		INSERT INTO #Commands(Phase, Command)
		VALUES
		(N'01-CreateDb', 
		 N'USE [master]; CREATE DATABASE {#TARGETDB_Q#}
		 ON PRIMARY (NAME = N''' + @TARGETDATABASE + N''', FILENAME = N''' + @MDFFILEPATH + N''', SIZE = 64MB, FILEGROWTH = 64MB)
		 LOG ON (NAME = N''' + @TARGETDATABASE + N'_log'', FILENAME = N''' + @LDFFILEPATH + N''', SIZE = 64MB, FILEGROWTH = 64MB)
		 COLLATE ' + @NEWCOLLATION + N';');
	END;

	-- Set SIMPLE recovery model during migration to avoid transaction log issues
	INSERT INTO #Commands(Phase, Command)
	VALUES
	(N'01-CreateDb', N'USE [master]; ALTER DATABASE {#TARGETDB_Q#} SET RECOVERY SIMPLE;');

	-- Set Restricted Access (RESTRICTED_USER) to avoid external Session on target database
	INSERT INTO #Commands(Phase, Command)
	VALUES
	(N'01-CreateDb', N'USE [master]; ALTER DATABASE {#TARGETDB_Q#} SET RESTRICTED_USER WITH ROLLBACK IMMEDIATE;');

	IF (@COMPATIBILITY_LEVEL IS NOT NULL) BEGIN
		INSERT INTO #Commands(Phase, Command)
		VALUES
		(N'01-CreateDb', N'USE [master]; ALTER DATABASE {#TARGETDB_Q#} SET COMPATIBILITY_LEVEL = ' + CONVERT(nvarchar(10), @COMPATIBILITY_LEVEL) + N';');
	END;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 02-Schemas commands...';
	--=================================================-- 02: Schemas --=====================================================--
	SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''02-Schemas'',
''USE {#TARGETDB_Q#}; IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = N'''''' + REPLACE(s.name, '''''''', '''''''''''') + N'''''') EXEC(''''CREATE SCHEMA '' + QUOTENAME(s.name) + N'''''');''
FROM {#SOURCEDB_Q#}.sys.schemas AS s
WHERE s.name NOT IN (N''sys'', N''INFORMATION_SCHEMA'')
  -- Exclude schemas matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND s.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern)
       OR (ex.IsWildcard = 0 AND s.name COLLATE DATABASE_DEFAULT = ex.Pattern)
  );';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 02a-PartitionFunctions commands...';
	--=================================================-- 02a: Partition Functions --=======================================--
	SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''02a-PartitionFunctions'',
  N''USE {#TARGETDB_Q#}; CREATE PARTITION FUNCTION '' + QUOTENAME(pf.name) + N''('' +
  ty.name +
  CASE
    WHEN ty.name IN (N''varchar'',N''char'',N''varbinary'',N''binary'') THEN N''('' + CASE WHEN prm.max_length = -1 THEN N''max'' ELSE CONVERT(nvarchar(10), prm.max_length) END + N'')''
    WHEN ty.name IN (N''nvarchar'',N''nchar'') THEN N''('' + CASE WHEN prm.max_length = -1 THEN N''max'' ELSE CONVERT(nvarchar(10), prm.max_length/2) END + N'')''
    WHEN ty.name IN (N''decimal'',N''numeric'') THEN N''('' + CONVERT(nvarchar(10), prm.precision) + N'','' + CONVERT(nvarchar(10), prm.scale) + N'')''
    WHEN ty.name IN (N''datetime2'',N''datetimeoffset'',N''time'') THEN N''('' + CONVERT(nvarchar(10), prm.scale) + N'')''
    ELSE N''''
  END +
  N'') AS RANGE '' + CASE WHEN pf.boundary_value_on_right = 1 THEN N''RIGHT'' ELSE N''LEFT'' END +
  N'' FOR VALUES ('' +
  ISNULL(STUFF((
    SELECT N'', '' + 
      CASE 
        WHEN SQL_VARIANT_PROPERTY(prv.value, ''BaseType'') IN (''char'',''varchar'',''nchar'',''nvarchar'') THEN N'''''''' + REPLACE(CONVERT(nvarchar(max), prv.value), '''''''', '''''''''''') + N''''''''
        WHEN SQL_VARIANT_PROPERTY(prv.value, ''BaseType'') IN (''datetime'',''datetime2'',''date'',''smalldatetime'') THEN N'''''''' + CONVERT(nvarchar(50), prv.value, 121) + N''''''''
        WHEN SQL_VARIANT_PROPERTY(prv.value, ''BaseType'') IN (''binary'',''varbinary'') THEN N''0x'' + CONVERT(nvarchar(max), CONVERT(varbinary(max), prv.value), 2)
        ELSE CONVERT(nvarchar(max), prv.value)
      END
    FROM {#SOURCEDB_Q#}.sys.partition_range_values AS prv
    WHERE prv.function_id = pf.function_id
    ORDER BY prv.boundary_id
    FOR XML PATH(''''), TYPE
  ).value(''.'', ''nvarchar(max)''), 1, 2, N''''), N'''') +
  N'');''
FROM {#SOURCEDB_Q#}.sys.partition_functions AS pf
CROSS APPLY (
  SELECT TOP 1 pp.parameter_id, pp.system_type_id, pp.max_length, pp.precision, pp.scale
  FROM {#SOURCEDB_Q#}.sys.partition_parameters AS pp
  WHERE pp.function_id = pf.function_id
) AS prm
JOIN {#SOURCEDB_Q#}.sys.types AS ty ON ty.system_type_id = prm.system_type_id AND ty.user_type_id = prm.system_type_id
-- Exclude partition functions matching patterns in #ExcludedObjects
WHERE NOT EXISTS (
  SELECT 1 FROM #ExcludedObjects AS ex
  WHERE (ex.IsWildcard = 1 AND pf.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern)
     OR (ex.IsWildcard = 0 AND pf.name COLLATE DATABASE_DEFAULT = ex.Pattern)
);';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 02b-PartitionSchemes commands...';
	--=================================================-- 02b: Partition Schemes --=========================================--
	SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''02b-PartitionSchemes'',
  N''USE {#TARGETDB_Q#}; CREATE PARTITION SCHEME '' + QUOTENAME(ps.name) +
  N'' AS PARTITION '' + QUOTENAME(pf.name) +
  N'' TO ('' +
  STUFF((
    SELECT N'', '' + QUOTENAME(ds.name)
    FROM {#SOURCEDB_Q#}.sys.destination_data_spaces AS dds
    JOIN {#SOURCEDB_Q#}.sys.data_spaces AS ds ON ds.data_space_id = dds.data_space_id
    WHERE dds.partition_scheme_id = ps.data_space_id
    ORDER BY dds.destination_id
    FOR XML PATH(''''), TYPE
  ).value(''.'', ''nvarchar(max)''), 1, 2, N'''') +
  N'');''
FROM {#SOURCEDB_Q#}.sys.partition_schemes AS ps
JOIN {#SOURCEDB_Q#}.sys.partition_functions AS pf ON pf.function_id = ps.function_id
-- Exclude partition schemes matching patterns in #ExcludedObjects
WHERE NOT EXISTS (
  SELECT 1 FROM #ExcludedObjects AS ex
  WHERE (ex.IsWildcard = 1 AND ps.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern)
     OR (ex.IsWildcard = 0 AND ps.name COLLATE DATABASE_DEFAULT = ex.Pattern)
);';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 02c-Sequences commands...';
	--=================================================-- 02c: Sequences --=================================================--
	-- NOTE: For sequences, we use current_value + increment to get the next value that will be issued
	-- This handles both positive and negative increments correctly
	SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''02c-Sequences'',
  N''USE {#TARGETDB_Q#}; CREATE SEQUENCE '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(sq.name) +
  N'' AS '' + ty.name +
  -- Use current_value + increment to get next value (handles positive and negative increments)
  -- If sequence was never used (current_value = start_value - increment), this still works
  -- Added overflow protection: if at boundary, use start_value for cycling or current for non-cycling
  N'' START WITH '' + CONVERT(nvarchar(30), 
    CASE 
      WHEN sq.is_exhausted = 1 THEN sq.start_value  -- Exhausted sequence, restart from beginning
      WHEN sq.is_cycling = 0 AND sq.increment > 0 AND CONVERT(bigint, sq.current_value) >= sq.maximum_value THEN CONVERT(bigint, sq.current_value)  -- At max, non-cycling
      WHEN sq.is_cycling = 0 AND sq.increment < 0 AND CONVERT(bigint, sq.current_value) <= sq.minimum_value THEN CONVERT(bigint, sq.current_value)  -- At min, non-cycling
      WHEN sq.is_cycling = 1 AND sq.increment > 0 AND CONVERT(bigint, sq.current_value) >= sq.maximum_value THEN sq.minimum_value  -- At max, cycling -> wrap to min
      WHEN sq.is_cycling = 1 AND sq.increment < 0 AND CONVERT(bigint, sq.current_value) <= sq.minimum_value THEN sq.maximum_value  -- At min, cycling -> wrap to max
      ELSE CONVERT(bigint, sq.current_value) + CONVERT(bigint, sq.increment)  -- Normal: next value
    END) +
  N'' INCREMENT BY '' + CONVERT(nvarchar(30), sq.increment) +
  CASE WHEN sq.minimum_value IS NOT NULL THEN N'' MINVALUE '' + CONVERT(nvarchar(30), sq.minimum_value) ELSE N'' NO MINVALUE'' END +
  CASE WHEN sq.maximum_value IS NOT NULL THEN N'' MAXVALUE '' + CONVERT(nvarchar(30), sq.maximum_value) ELSE N'' NO MAXVALUE'' END +
  CASE WHEN sq.is_cycling = 1 THEN N'' CYCLE'' ELSE N'' NO CYCLE'' END +
  CASE WHEN sq.is_cached = 1 THEN N'' CACHE '' + CONVERT(nvarchar(30), sq.cache_size) ELSE N'' NO CACHE'' END +
  N'';''
FROM {#SOURCEDB_Q#}.sys.sequences AS sq
JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = sq.schema_id
JOIN {#SOURCEDB_Q#}.sys.types AS ty ON ty.user_type_id = sq.user_type_id
WHERE s.name <> N''sys''
  -- Exclude sequences matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (sq.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + sq.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (sq.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + sq.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 02d-UserDefinedTypes commands...';
	--=================================================-- 02d: User-Defined Types (Alias Types) --==========================--
	SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''02d-UDT'',
  N''USE {#TARGETDB_Q#}; CREATE TYPE '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) +
  N'' FROM '' + bty.name +
  CASE
    WHEN bty.name IN (N''varchar'',N''char'',N''varbinary'',N''binary'') THEN N''('' + CASE WHEN t.max_length = -1 THEN N''max'' ELSE CONVERT(nvarchar(10), t.max_length) END + N'')''
    WHEN bty.name IN (N''nvarchar'',N''nchar'') THEN N''('' + CASE WHEN t.max_length = -1 THEN N''max'' ELSE CONVERT(nvarchar(10), t.max_length/2) END + N'')''
    WHEN bty.name IN (N''decimal'',N''numeric'') THEN N''('' + CONVERT(nvarchar(10), t.precision) + N'','' + CONVERT(nvarchar(10), t.scale) + N'')''
    WHEN bty.name IN (N''datetime2'',N''datetimeoffset'',N''time'') THEN N''('' + CONVERT(nvarchar(10), t.scale) + N'')''
    ELSE N''''
  END +
  -- Add COLLATE for string-based types
  CASE WHEN bty.name IN (N''char'',N''varchar'',N''nchar'',N''nvarchar'',N''text'',N''ntext'') THEN N'' COLLATE ' + @NEWCOLLATION + N''' ELSE N'''' END +
  CASE WHEN t.is_nullable = 1 THEN N'' NULL'' ELSE N'' NOT NULL'' END +
  N'';''
FROM {#SOURCEDB_Q#}.sys.types AS t
JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = t.schema_id
JOIN {#SOURCEDB_Q#}.sys.types AS bty ON bty.user_type_id = t.system_type_id
WHERE t.is_user_defined = 1
  AND t.is_table_type = 0
  AND t.is_assembly_type = 0
  AND s.name <> N''sys''
  -- Exclude UDTs matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (t.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (t.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 02e-TableTypes commands...';
	--=================================================-- 02e: Table Types --===============================================--
	SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''02e-TableTypes'',
  N''USE {#TARGETDB_Q#}; CREATE TYPE '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) + N'' AS TABLE ('' +
  STUFF((
    SELECT N'', '' +
      QUOTENAME(c.name) + N'' '' +
      CASE
        WHEN bty.name IN (N''varchar'',N''char'',N''varbinary'',N''binary'') THEN bty.name + N''('' + CASE WHEN c.max_length = -1 THEN N''max'' ELSE CONVERT(nvarchar(10), c.max_length) END + N'')''
        WHEN bty.name IN (N''nvarchar'',N''nchar'') THEN bty.name + N''('' + CASE WHEN c.max_length = -1 THEN N''max'' ELSE CONVERT(nvarchar(10), c.max_length/2) END + N'')''
        WHEN bty.name IN (N''decimal'',N''numeric'') THEN bty.name + N''('' + CONVERT(nvarchar(10), c.precision) + N'','' + CONVERT(nvarchar(10), c.scale) + N'')''
        WHEN bty.name IN (N''datetime2'',N''datetimeoffset'',N''time'') THEN bty.name + N''('' + CONVERT(nvarchar(10), c.scale) + N'')''
        ELSE bty.name
      END +
      CASE WHEN bty.name IN (N''char'',N''varchar'',N''nchar'',N''nvarchar'',N''text'',N''ntext'') THEN N'' COLLATE ' + @NEWCOLLATION + N''' ELSE N'''' END +
      CASE WHEN c.is_nullable = 1 THEN N'' NULL'' ELSE N'' NOT NULL'' END
    FROM {#SOURCEDB_Q#}.sys.table_types AS tt2
    JOIN {#SOURCEDB_Q#}.sys.columns AS c ON c.object_id = tt2.type_table_object_id
    JOIN {#SOURCEDB_Q#}.sys.types AS bty ON bty.user_type_id = c.system_type_id
    WHERE tt2.user_type_id = t.user_type_id
    ORDER BY c.column_id
    FOR XML PATH(''''), TYPE
  ).value(''.'', ''nvarchar(max)''), 1, 2, N'''') + N'');''
FROM {#SOURCEDB_Q#}.sys.table_types AS t
JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = t.schema_id
WHERE s.name <> N''sys''
  -- Exclude table types matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (t.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (t.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 03-Tables commands...';
	--=================================================-- 03: Tables --=====================================================--
	SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''03-Tables'',
  N''USE {#TARGETDB_Q#}; CREATE TABLE '' + QUOTENAME(sc.name) + N''.'' + QUOTENAME(t.name) + N''('' +
  STUFF((
    SELECT N'', '' +
      CASE WHEN c.is_computed = 1 THEN
        QUOTENAME(c.name) + N'' AS '' + REPLACE(REPLACE(cc.definition, N''[{#SOURCEDB#}].'', N''[{#TARGETDB#}].''), N''{#SOURCEDB#}.'', N''{#TARGETDB#}.'' ) + CASE WHEN cc.is_persisted = 1 THEN N'' PERSISTED'' ELSE N'''' END
      ELSE
        QUOTENAME(c.name) + N'' '' +
        -- Use UDT name if user-defined, otherwise system type with size specs
        CASE
          WHEN ty.is_user_defined = 1 THEN QUOTENAME(tys.name) + N''.'' + QUOTENAME(ty.name)
          WHEN ty.name IN (N''varchar'',N''char'',N''varbinary'',N''binary'') THEN ty.name + N''('' + CASE WHEN c.max_length = -1 THEN N''max'' ELSE CONVERT(nvarchar(10), c.max_length) END + N'')''
          WHEN ty.name IN (N''nvarchar'',N''nchar'') THEN ty.name + N''('' + CASE WHEN c.max_length = -1 THEN N''max'' ELSE CONVERT(nvarchar(10), c.max_length/2) END + N'')''
          WHEN ty.name IN (N''decimal'',N''numeric'') THEN ty.name + N''('' + CONVERT(nvarchar(10), c.precision) + N'','' + CONVERT(nvarchar(10), c.scale) + N'')''
          WHEN ty.name IN (N''datetime2'',N''datetimeoffset'',N''time'') THEN ty.name + N''('' + CONVERT(nvarchar(10), c.scale) + N'')''
          ELSE ty.name
        END +
        -- Only add COLLATE for non-UDT string types (UDTs already have collation from CREATE TYPE)
        CASE WHEN ty.is_user_defined = 0 AND ty.name IN (N''char'',N''varchar'',N''nchar'',N''nvarchar'',N''text'',N''ntext'') THEN N'' COLLATE ' + @NEWCOLLATION + N''' ELSE N'''' END +
        CASE WHEN c.is_sparse = 1 THEN N'' SPARSE'' ELSE N'''' END +
        CASE WHEN ic.object_id IS NOT NULL THEN N'' IDENTITY('' + CONVERT(nvarchar(30), ic.seed_value) + N'','' + CONVERT(nvarchar(30), ic.increment_value) + N'')'' ELSE N'''' END +
        CASE WHEN c.is_rowguidcol = 1 THEN N'' ROWGUIDCOL'' ELSE N'''' END +
        CASE WHEN c.is_nullable = 1 THEN N'' NULL'' ELSE N'' NOT NULL'' END
      END
    FROM {#SOURCEDB_Q#}.sys.columns AS c
    LEFT JOIN {#SOURCEDB_Q#}.sys.identity_columns AS ic ON ic.object_id = c.object_id AND ic.column_id = c.column_id
    LEFT JOIN {#SOURCEDB_Q#}.sys.computed_columns AS cc ON cc.object_id = c.object_id AND cc.column_id = c.column_id
    JOIN {#SOURCEDB_Q#}.sys.types AS ty ON ty.user_type_id = c.user_type_id
    LEFT JOIN {#SOURCEDB_Q#}.sys.schemas AS tys ON tys.schema_id = ty.schema_id
    WHERE c.object_id = t.object_id
    ORDER BY c.column_id
    FOR XML PATH(''''), TYPE).value(''.'', ''nvarchar(max)''), 1, 2, N'''') + N'');''
FROM {#SOURCEDB_Q#}.sys.tables AS t
JOIN {#SOURCEDB_Q#}.sys.schemas AS sc ON sc.schema_id = t.schema_id
WHERE sc.name <> N''sys'' AND t.is_ms_shipped = 0
  -- Exclude tables matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (t.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (sc.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (t.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (sc.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 04-Data commands...';
	--=================================================-- 04: Data copy --==================================================--
	-- Using TABLOCK hint for minimal logging in SIMPLE recovery mode
	-- With batching support to prevent transaction log overflow
	-- Strategy: 
	--   - Tables with single-column INT/BIGINT PK: Keyset pagination (no tempdb usage)
	--   - Tables without suitable PK: Temp table approach (uses tempdb)
	IF @BATCHSIZE = 0 BEGIN
		-- No batching - single INSERT per table
		SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''04-Data'',
  N''USE {#TARGETDB_Q#}; '' +
  CASE WHEN EXISTS (SELECT 1 FROM {#SOURCEDB_Q#}.sys.identity_columns AS ic WHERE ic.object_id = t.object_id) THEN N''SET IDENTITY_INSERT '' + QUOTENAME(sc.name) + N''.'' + QUOTENAME(t.name) + N'' ON; '' ELSE N'''' END +
  N''INSERT INTO '' + QUOTENAME(sc.name) + N''.'' + QUOTENAME(t.name) + N'' WITH (TABLOCK) ('' +
    STUFF((
      SELECT N'', '' + QUOTENAME(c.name)
      FROM {#SOURCEDB_Q#}.sys.columns AS c
      JOIN {#SOURCEDB_Q#}.sys.types AS cty ON cty.user_type_id = c.user_type_id
      WHERE c.object_id = t.object_id
        AND c.is_computed = 0
        AND cty.name NOT IN (N''timestamp'', N''rowversion'')
      ORDER BY c.column_id
      FOR XML PATH(''''), TYPE
    ).value(''.'', ''nvarchar(max)''), 1, 2, N'''') + N'') '' +
  N''SELECT '' +
    STUFF((
      SELECT N'', '' + QUOTENAME(c.name)
      FROM {#SOURCEDB_Q#}.sys.columns AS c
      JOIN {#SOURCEDB_Q#}.sys.types AS cty ON cty.user_type_id = c.user_type_id
      WHERE c.object_id = t.object_id
        AND c.is_computed = 0
        AND cty.name NOT IN (N''timestamp'', N''rowversion'')
      ORDER BY c.column_id
      FOR XML PATH(''''), TYPE
    ).value(''.'', ''nvarchar(max)''), 1, 2, N'''') +
  N'' FROM {#SOURCEDB_Q#}.'' + QUOTENAME(sc.name) + N''.'' + QUOTENAME(t.name) + N''; '' +
  CASE WHEN EXISTS (SELECT 1 FROM {#SOURCEDB_Q#}.sys.identity_columns AS ic WHERE ic.object_id = t.object_id) THEN N''SET IDENTITY_INSERT '' + QUOTENAME(sc.name) + N''.'' + QUOTENAME(t.name) + N'' OFF; '' ELSE N'''' END +
  N''CHECKPOINT;''
FROM {#SOURCEDB_Q#}.sys.tables AS t
JOIN {#SOURCEDB_Q#}.sys.schemas AS sc ON sc.schema_id = t.schema_id
WHERE sc.name <> N''sys'' AND t.is_ms_shipped = 0
  -- Exclude tables matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (t.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (sc.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (t.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (sc.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );';
		-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	END; ELSE BEGIN
		-- Batching enabled - use Keyset Pagination for tables with single-column INT/BIGINT PK (no tempdb usage)
		-- This is the most efficient approach for large tables
		SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''04-Data'',
  N''USE {#TARGETDB_Q#}; '' +
  CASE WHEN EXISTS (SELECT 1 FROM {#SOURCEDB_Q#}.sys.identity_columns AS ic WHERE ic.object_id = t.object_id) THEN N''SET IDENTITY_INSERT '' + QUOTENAME(sc.name) + N''.'' + QUOTENAME(t.name) + N'' ON; '' ELSE N'''' END +
  -- Keyset pagination: Use PK column for efficient batching without tempdb
  -- Optimized: Calculate batch MAX key BEFORE insert to avoid full table scans
  N''DECLARE @LastKey BIGINT = NULL, @BatchMaxKey BIGINT, @Rows INT = 1; '' +
  N''WHILE @Rows > 0 BEGIN '' +
  -- Find the max key for this batch using index seek on source PK
  N''SELECT @BatchMaxKey = MAX('' + QUOTENAME(pkc.pk_column_name) + N'') '' +
  N''FROM (SELECT TOP ({#BATCHSIZE#}) '' + QUOTENAME(pkc.pk_column_name) + N'' '' +
  N''      FROM {#SOURCEDB_Q#}.'' + QUOTENAME(sc.name) + N''.'' + QUOTENAME(t.name) + N'' '' +
  N''      WHERE (@LastKey IS NULL OR '' + QUOTENAME(pkc.pk_column_name) + N'' > @LastKey) '' +
  N''      ORDER BY '' + QUOTENAME(pkc.pk_column_name) + N'') AS __batch; '' +
  N''IF @BatchMaxKey IS NULL SET @Rows = 0; ELSE BEGIN '' +
  N''INSERT INTO '' + QUOTENAME(sc.name) + N''.'' + QUOTENAME(t.name) + N'' WITH (TABLOCK) ('' +
    STUFF((
      SELECT N'', '' + QUOTENAME(c.name)
      FROM {#SOURCEDB_Q#}.sys.columns AS c
      JOIN {#SOURCEDB_Q#}.sys.types AS cty ON cty.user_type_id = c.user_type_id
      WHERE c.object_id = t.object_id
        AND c.is_computed = 0
        AND cty.name NOT IN (N''timestamp'', N''rowversion'')
      ORDER BY c.column_id
      FOR XML PATH(''''), TYPE
    ).value(''.'', ''nvarchar(max)''), 1, 2, N'''') + N'') '' +
  N''SELECT '' +
    STUFF((
      SELECT N'', '' + QUOTENAME(c.name)
      FROM {#SOURCEDB_Q#}.sys.columns AS c
      JOIN {#SOURCEDB_Q#}.sys.types AS cty ON cty.user_type_id = c.user_type_id
      WHERE c.object_id = t.object_id
        AND c.is_computed = 0
        AND cty.name NOT IN (N''timestamp'', N''rowversion'')
      ORDER BY c.column_id
      FOR XML PATH(''''), TYPE
    ).value(''.'', ''nvarchar(max)''), 1, 2, N'''') +
  N'' FROM {#SOURCEDB_Q#}.'' + QUOTENAME(sc.name) + N''.'' + QUOTENAME(t.name) +
  N'' WHERE (@LastKey IS NULL OR '' + QUOTENAME(pkc.pk_column_name) + N'' > @LastKey) '' +
  N''   AND '' + QUOTENAME(pkc.pk_column_name) + N'' <= @BatchMaxKey; '' +
  N''SET @Rows = @@ROWCOUNT; SET @LastKey = @BatchMaxKey; '' +
  N''CHECKPOINT; '' +
  N''END; END; '' +
  CASE WHEN EXISTS (SELECT 1 FROM {#SOURCEDB_Q#}.sys.identity_columns AS ic WHERE ic.object_id = t.object_id) THEN N''SET IDENTITY_INSERT '' + QUOTENAME(sc.name) + N''.'' + QUOTENAME(t.name) + N'' OFF; '' ELSE N'''' END
FROM {#SOURCEDB_Q#}.sys.tables AS t
JOIN {#SOURCEDB_Q#}.sys.schemas AS sc ON sc.schema_id = t.schema_id
-- Find single-column INT/BIGINT primary key
CROSS APPLY (
  SELECT c.name AS pk_column_name
  FROM {#SOURCEDB_Q#}.sys.indexes AS i
  JOIN {#SOURCEDB_Q#}.sys.index_columns AS ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
  JOIN {#SOURCEDB_Q#}.sys.columns AS c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
  JOIN {#SOURCEDB_Q#}.sys.types AS ty ON ty.user_type_id = c.user_type_id
  WHERE i.object_id = t.object_id
    AND i.is_primary_key = 1
    AND ty.name IN (N''int'', N''bigint'', N''smallint'', N''tinyint'')
    -- Only select if PK has exactly one column
    AND (SELECT COUNT(*) FROM {#SOURCEDB_Q#}.sys.index_columns AS ic2 WHERE ic2.object_id = i.object_id AND ic2.index_id = i.index_id) = 1
) AS pkc
WHERE sc.name <> N''sys'' AND t.is_ms_shipped = 0
  -- Exclude tables matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (t.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (sc.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (t.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (sc.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;

		-- Fallback for tables WITHOUT single-column INT/BIGINT PK: Use temp table approach
		-- This uses tempdb but is necessary for tables without a suitable PK
		SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''04-Data'',
  N''USE {#TARGETDB_Q#}; '' +
  CASE WHEN EXISTS (SELECT 1 FROM {#SOURCEDB_Q#}.sys.identity_columns AS ic WHERE ic.object_id = t.object_id) THEN N''SET IDENTITY_INSERT '' + QUOTENAME(sc.name) + N''.'' + QUOTENAME(t.name) + N'' ON; '' ELSE N'''' END +
  -- Step 1: Create temp table with ROW_NUMBER computed only ONCE
  N''SELECT '' +
    STUFF((
      SELECT N'', '' + QUOTENAME(c.name)
      FROM {#SOURCEDB_Q#}.sys.columns AS c
      JOIN {#SOURCEDB_Q#}.sys.types AS cty ON cty.user_type_id = c.user_type_id
      WHERE c.object_id = t.object_id
        AND c.is_computed = 0
        AND cty.name NOT IN (N''timestamp'', N''rowversion'')
      ORDER BY c.column_id
      FOR XML PATH(''''), TYPE
    ).value(''.'', ''nvarchar(max)''), 1, 2, N'''') +
  N'', ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS __rn INTO #__BatchTemp FROM {#SOURCEDB_Q#}.'' + QUOTENAME(sc.name) + N''.'' + QUOTENAME(t.name) + N''; '' +
  -- Step 2: Create clustered index on __rn for fast range seeks
  N''CREATE CLUSTERED INDEX __IX_BatchTemp_rn ON #__BatchTemp (__rn); '' +
  -- Step 3: Batch loop using indexed seeks on temp table
  N''DECLARE @Offset BIGINT = 0, @Rows INT = 1; '' +
  N''WHILE @Rows > 0 BEGIN '' +
  N''INSERT INTO '' + QUOTENAME(sc.name) + N''.'' + QUOTENAME(t.name) + N'' WITH (TABLOCK) ('' +
    STUFF((
      SELECT N'', '' + QUOTENAME(c.name)
      FROM {#SOURCEDB_Q#}.sys.columns AS c
      JOIN {#SOURCEDB_Q#}.sys.types AS cty ON cty.user_type_id = c.user_type_id
      WHERE c.object_id = t.object_id
        AND c.is_computed = 0
        AND cty.name NOT IN (N''timestamp'', N''rowversion'')
      ORDER BY c.column_id
      FOR XML PATH(''''), TYPE
    ).value(''.'', ''nvarchar(max)''), 1, 2, N'''') + N'') '' +
  N''SELECT '' +
    STUFF((
      SELECT N'', '' + QUOTENAME(c.name)
      FROM {#SOURCEDB_Q#}.sys.columns AS c
      JOIN {#SOURCEDB_Q#}.sys.types AS cty ON cty.user_type_id = c.user_type_id
      WHERE c.object_id = t.object_id
        AND c.is_computed = 0
        AND cty.name NOT IN (N''timestamp'', N''rowversion'')
      ORDER BY c.column_id
      FOR XML PATH(''''), TYPE
    ).value(''.'', ''nvarchar(max)''), 1, 2, N'''') +
  N'' FROM #__BatchTemp WHERE __rn > @Offset AND __rn <= @Offset + {#BATCHSIZE#}; '' +
  N''SET @Rows = @@ROWCOUNT; SET @Offset = @Offset + {#BATCHSIZE#}; '' +
  N''CHECKPOINT; '' +
  N''END; '' +
  -- Step 4: Cleanup temp table (DBCC SHRINKFILE runs once after all temp-table operations in phase 04a)
  N''DROP TABLE #__BatchTemp; '' +
  CASE WHEN EXISTS (SELECT 1 FROM {#SOURCEDB_Q#}.sys.identity_columns AS ic WHERE ic.object_id = t.object_id) THEN N''SET IDENTITY_INSERT '' + QUOTENAME(sc.name) + N''.'' + QUOTENAME(t.name) + N'' OFF; '' ELSE N'''' END
FROM {#SOURCEDB_Q#}.sys.tables AS t
JOIN {#SOURCEDB_Q#}.sys.schemas AS sc ON sc.schema_id = t.schema_id
WHERE sc.name <> N''sys'' AND t.is_ms_shipped = 0
  -- Exclude tables that already have keyset pagination (single-column INT/BIGINT PK)
  AND NOT EXISTS (
    SELECT 1
    FROM {#SOURCEDB_Q#}.sys.indexes AS i
    JOIN {#SOURCEDB_Q#}.sys.index_columns AS ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
    JOIN {#SOURCEDB_Q#}.sys.columns AS c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
    JOIN {#SOURCEDB_Q#}.sys.types AS ty ON ty.user_type_id = c.user_type_id
    WHERE i.object_id = t.object_id
      AND i.is_primary_key = 1
      AND ty.name IN (N''int'', N''bigint'', N''smallint'', N''tinyint'')
      AND (SELECT COUNT(*) FROM {#SOURCEDB_Q#}.sys.index_columns AS ic2 WHERE ic2.object_id = i.object_id AND ic2.index_id = i.index_id) = 1
  )
  -- Exclude tables matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (t.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (sc.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (t.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (sc.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );';
		-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;

		-- Add DBCC SHRINKFILE once after all temp-table data operations (if any temp tables were used)
		-- Must execute in tempdb context to find the tempdev file
		SET @DYNAMICSQL = N'
IF EXISTS (
  SELECT 1 FROM {#SOURCEDB_Q#}.sys.tables AS t
  JOIN {#SOURCEDB_Q#}.sys.schemas AS sc ON sc.schema_id = t.schema_id
  WHERE sc.name <> N''sys'' AND t.is_ms_shipped = 0
    AND NOT EXISTS (
      SELECT 1
      FROM {#SOURCEDB_Q#}.sys.indexes AS i
      JOIN {#SOURCEDB_Q#}.sys.index_columns AS ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
      JOIN {#SOURCEDB_Q#}.sys.columns AS c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
      JOIN {#SOURCEDB_Q#}.sys.types AS ty ON ty.user_type_id = c.user_type_id
      WHERE i.object_id = t.object_id AND i.is_primary_key = 1 AND ty.name IN (N''int'', N''bigint'', N''smallint'', N''tinyint'')
        AND (SELECT COUNT(*) FROM {#SOURCEDB_Q#}.sys.index_columns AS ic2 WHERE ic2.object_id = i.object_id AND ic2.index_id = i.index_id) = 1
    )
)
  INSERT INTO #Commands(Phase, Command) VALUES (N''04a-ShrinkTempdb'', N''USE tempdb; BEGIN TRY DECLARE @f sysname = (SELECT TOP (1) name FROM tempdb.sys.database_files WHERE type_desc = ''''ROWS''''); IF @f IS NOT NULL BEGIN DBCC SHRINKFILE(@f, 1); END END TRY BEGIN CATCH PRINT N''''WARN: DBCC SHRINKFILE skipped: '''' + ERROR_MESSAGE(); END CATCH;'');';
		-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	END;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 05-DefaultConstraints commands...';
	--=================================================-- 05: Default constraints --========================================--
	SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''05-Defaults'',
  N''USE {#TARGETDB_Q#}; ALTER TABLE '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) +
  N'' ADD CONSTRAINT '' + QUOTENAME(dc.name) + N'' DEFAULT '' + REPLACE(REPLACE(dc.definition, N''[{#SOURCEDB#}].'', N''[{#TARGETDB#}].''), N''{#SOURCEDB#}.'', N''{#TARGETDB#}.'' ) +
  N'' FOR '' + QUOTENAME(c.name) + N'';''
FROM {#SOURCEDB_Q#}.sys.default_constraints AS dc
JOIN {#SOURCEDB_Q#}.sys.tables AS t ON t.object_id = dc.parent_object_id
JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = t.schema_id
JOIN {#SOURCEDB_Q#}.sys.columns AS c ON c.object_id = dc.parent_object_id AND c.column_id = dc.parent_column_id
WHERE s.name <> N''sys''
  -- Exclude constraints on tables matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (t.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (t.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 06-CheckConstraints commands...';
	--=================================================-- 06: Check constraints --==========================================--
	SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''06-Checks'',
  N''USE {#TARGETDB_Q#}; ALTER TABLE '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) +
  N'' WITH NOCHECK ADD CONSTRAINT '' + QUOTENAME(cc.name) + N'' CHECK '' + REPLACE(REPLACE(cc.definition, N''[{#SOURCEDB#}].'', N''[{#TARGETDB#}].''), N''{#SOURCEDB#}.'', N''{#TARGETDB#}.'' ) + N'';'' +
  CASE WHEN cc.is_disabled = 1 THEN
    N'' ALTER TABLE '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) + N'' NOCHECK CONSTRAINT '' + QUOTENAME(cc.name) + N'';''
  ELSE N'''' END
FROM {#SOURCEDB_Q#}.sys.check_constraints AS cc
JOIN {#SOURCEDB_Q#}.sys.tables AS t ON t.object_id = cc.parent_object_id
JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = t.schema_id
WHERE s.name <> N''sys''
  -- Exclude constraints on tables matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (t.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (t.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 07-PrimaryUniqueConstraints commands...';
	--=================================================-- 07: Primary/Unique constraints (PK/UQ) --=========================--
	SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''07-PK_UQ'',
  N''USE {#TARGETDB_Q#}; ALTER TABLE '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) +
  N'' ADD CONSTRAINT '' + QUOTENAME(k.name) + N'' '' +
  CASE WHEN k.[type] = ''PK'' THEN N''PRIMARY KEY '' ELSE N''UNIQUE '' END +
  CASE WHEN i.[type] = 1 THEN N''CLUSTERED '' ELSE N''NONCLUSTERED '' END +
  N''('' + kc.cols + N'')'' +
  -- PK/UQ options (WITH clause)
  N'' WITH ('' +
  N''PAD_INDEX = '' + CASE WHEN i.is_padded = 1 THEN N''ON'' ELSE N''OFF'' END +
  N'', FILLFACTOR = '' + CASE WHEN i.fill_factor = 0 THEN N''100'' ELSE CONVERT(nvarchar(3), i.fill_factor) END +
  N'', IGNORE_DUP_KEY = '' + CASE WHEN i.ignore_dup_key = 1 THEN N''ON'' ELSE N''OFF'' END +
  N'', ALLOW_ROW_LOCKS = '' + CASE WHEN i.allow_row_locks = 1 THEN N''ON'' ELSE N''OFF'' END +
  N'', ALLOW_PAGE_LOCKS = '' + CASE WHEN i.allow_page_locks = 1 THEN N''ON'' ELSE N''OFF'' END +
  ISNULL(N'', DATA_COMPRESSION = '' + pc.compression_desc, N'''') +
  N'')'' +
  CASE WHEN on_target.on_target IS NULL THEN N'''' ELSE N'' ON '' + on_target.on_target END +
  N'';''
FROM {#SOURCEDB_Q#}.sys.key_constraints AS k
JOIN {#SOURCEDB_Q#}.sys.tables AS t ON t.object_id = k.parent_object_id
JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = t.schema_id
JOIN {#SOURCEDB_Q#}.sys.indexes AS i ON i.object_id = k.parent_object_id AND i.index_id = k.unique_index_id
-- Get data compression for partition 1 (or NULL if not applicable)
OUTER APPLY (
  SELECT TOP 1 p.data_compression_desc AS compression_desc
  FROM {#SOURCEDB_Q#}.sys.partitions AS p
  WHERE p.object_id = i.object_id AND p.index_id = i.index_id AND p.partition_number = 1
) AS pc
CROSS APPLY
(
  SELECT STUFF((
    SELECT N'', '' + QUOTENAME(c.name) + CASE WHEN ic.is_descending_key = 1 THEN N'' DESC'' ELSE N'' ASC'' END
    FROM {#SOURCEDB_Q#}.sys.index_columns AS ic
    JOIN {#SOURCEDB_Q#}.sys.columns AS c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
    WHERE ic.object_id = k.parent_object_id
      AND ic.index_id = k.unique_index_id
      AND ic.is_included_column = 0
    ORDER BY ic.key_ordinal
    FOR XML PATH(''''), TYPE
  ).value(''.'', ''nvarchar(max)''), 1, 2, N'''') AS cols
) AS kc
OUTER APPLY
(
  SELECT
    CASE
      WHEN ds.[type] = ''PS'' THEN QUOTENAME(ps.name) + N''('' + QUOTENAME(pc2.name) + N'')''
      ELSE QUOTENAME(ds.name)
    END AS on_target
  FROM {#SOURCEDB_Q#}.sys.data_spaces AS ds
  LEFT JOIN {#SOURCEDB_Q#}.sys.partition_schemes AS ps ON ps.data_space_id = ds.data_space_id
  LEFT JOIN {#SOURCEDB_Q#}.sys.index_columns AS icp ON icp.object_id = i.object_id AND icp.index_id = i.index_id AND icp.partition_ordinal = 1
  LEFT JOIN {#SOURCEDB_Q#}.sys.columns AS pc2 ON pc2.object_id = icp.object_id AND pc2.column_id = icp.column_id
  WHERE ds.data_space_id = i.data_space_id
) AS on_target
WHERE s.name <> N''sys''
  -- Exclude PK/UQ on tables matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (t.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (t.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 08-Indexes commands...';
	--=================================================-- 08: Non-constraint indexes --=====================================--
	SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''08-Indexes'',
  N''USE {#TARGETDB_Q#}; CREATE '' +
  CASE WHEN i.is_unique = 1 THEN N''UNIQUE '' ELSE N'''' END +
  CASE WHEN i.[type] = 1 THEN N''CLUSTERED '' ELSE N''NONCLUSTERED '' END +
  N''INDEX '' + QUOTENAME(i.name) +
  N'' ON '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) +
  N'' ('' + kc.key_cols + N'')'' +
  CASE WHEN inc.included_cols IS NULL THEN N'''' ELSE N'' INCLUDE ('' + inc.included_cols + N'')'' END +
  CASE WHEN i.has_filter = 1 THEN N'' WHERE '' + REPLACE(REPLACE(i.filter_definition, N''[{#SOURCEDB#}].'', N''[{#TARGETDB#}].''), N''{#SOURCEDB#}.'', N''{#TARGETDB#}.'' ) ELSE N'''' END +
  -- Index options (WITH clause)
  N'' WITH ('' +
  N''PAD_INDEX = '' + CASE WHEN i.is_padded = 1 THEN N''ON'' ELSE N''OFF'' END +
  N'', FILLFACTOR = '' + CASE WHEN i.fill_factor = 0 THEN N''100'' ELSE CONVERT(nvarchar(3), i.fill_factor) END +
  N'', IGNORE_DUP_KEY = '' + CASE WHEN i.ignore_dup_key = 1 THEN N''ON'' ELSE N''OFF'' END +
  N'', ALLOW_ROW_LOCKS = '' + CASE WHEN i.allow_row_locks = 1 THEN N''ON'' ELSE N''OFF'' END +
  N'', ALLOW_PAGE_LOCKS = '' + CASE WHEN i.allow_page_locks = 1 THEN N''ON'' ELSE N''OFF'' END +
  ISNULL(N'', DATA_COMPRESSION = '' + pc.compression_desc, N'''') +
  N'')'' +
  CASE WHEN on_target.on_target IS NULL THEN N'''' ELSE N'' ON '' + on_target.on_target END +
  N'';''
FROM {#SOURCEDB_Q#}.sys.indexes AS i
JOIN {#SOURCEDB_Q#}.sys.tables AS t ON t.object_id = i.object_id
JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = t.schema_id
-- Get data compression for partition 1 (or NULL if not applicable)
OUTER APPLY (
  SELECT TOP 1 p.data_compression_desc AS compression_desc
  FROM {#SOURCEDB_Q#}.sys.partitions AS p
  WHERE p.object_id = i.object_id AND p.index_id = i.index_id AND p.partition_number = 1
) AS pc
OUTER APPLY
(
  SELECT STUFF((
    SELECT N'', '' + QUOTENAME(c.name) + CASE WHEN ic.is_descending_key = 1 THEN N'' DESC'' ELSE N'' ASC'' END
    FROM {#SOURCEDB_Q#}.sys.index_columns AS ic
    JOIN {#SOURCEDB_Q#}.sys.columns AS c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
    WHERE ic.object_id = i.object_id
      AND ic.index_id = i.index_id
      AND ic.is_included_column = 0
    ORDER BY ic.key_ordinal
    FOR XML PATH(''''), TYPE
  ).value(''.'', ''nvarchar(max)''), 1, 2, N'''') AS key_cols
) AS kc
OUTER APPLY
(
  SELECT STUFF((
    SELECT N'', '' + QUOTENAME(c.name)
    FROM {#SOURCEDB_Q#}.sys.index_columns AS ic
    JOIN {#SOURCEDB_Q#}.sys.columns AS c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
    WHERE ic.object_id = i.object_id
      AND ic.index_id = i.index_id
      AND ic.is_included_column = 1
    ORDER BY ic.index_column_id
    FOR XML PATH(''''), TYPE
  ).value(''.'', ''nvarchar(max)''), 1, 2, N'''') AS included_cols
) AS inc
OUTER APPLY
(
  SELECT
    CASE
      WHEN ds.[type] = ''PS'' THEN QUOTENAME(ps.name) + N''('' + QUOTENAME(pc.name) + N'')''
      ELSE QUOTENAME(ds.name)
    END AS on_target
  FROM {#SOURCEDB_Q#}.sys.data_spaces AS ds
  LEFT JOIN {#SOURCEDB_Q#}.sys.partition_schemes AS ps ON ps.data_space_id = ds.data_space_id
  LEFT JOIN {#SOURCEDB_Q#}.sys.index_columns AS icp ON icp.object_id = i.object_id AND icp.index_id = i.index_id AND icp.partition_ordinal = 1
  LEFT JOIN {#SOURCEDB_Q#}.sys.columns AS pc ON pc.object_id = icp.object_id AND pc.column_id = icp.column_id
  WHERE ds.data_space_id = i.data_space_id
) AS on_target
WHERE s.name <> N''sys''
  AND i.index_id > 0
  AND i.is_primary_key = 0
  AND i.is_unique_constraint = 0
  -- Exclude indexes on tables matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (t.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (t.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 09-ForeignKeys commands...';
	--=================================================-- 09: Foreign keys --===============================================--
	SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''09-FK'',
  N''USE {#TARGETDB_Q#}; ALTER TABLE '' + QUOTENAME(spt.name) + N''.'' + QUOTENAME(pt.name) +
  N'' ADD CONSTRAINT '' + QUOTENAME(fk.name) +
  N'' FOREIGN KEY ('' + fkc.parent_cols + N'') REFERENCES '' +
  QUOTENAME(srt.name) + N''.'' + QUOTENAME(rt.name) + N'' ('' + fkc.ref_cols + N'')'' +
  CASE WHEN fk.delete_referential_action_desc <> N''NO_ACTION'' THEN N'' ON DELETE '' + fk.delete_referential_action_desc ELSE N'''' END +
  CASE WHEN fk.update_referential_action_desc <> N''NO_ACTION'' THEN N'' ON UPDATE '' + fk.update_referential_action_desc ELSE N'''' END +
  CASE WHEN fk.is_not_for_replication = 1 THEN N'' NOT FOR REPLICATION'' ELSE N'''' END +
  N'';'' +
  CASE WHEN fk.is_disabled = 1 THEN N'' ALTER TABLE '' + QUOTENAME(spt.name) + N''.'' + QUOTENAME(pt.name) + N'' NOCHECK CONSTRAINT '' + QUOTENAME(fk.name) + N'';'' ELSE N'''' END
FROM {#SOURCEDB_Q#}.sys.foreign_keys AS fk
JOIN {#SOURCEDB_Q#}.sys.tables AS pt ON pt.object_id = fk.parent_object_id
JOIN {#SOURCEDB_Q#}.sys.tables AS rt ON rt.object_id = fk.referenced_object_id
JOIN {#SOURCEDB_Q#}.sys.schemas AS spt ON spt.schema_id = pt.schema_id
JOIN {#SOURCEDB_Q#}.sys.schemas AS srt ON srt.schema_id = rt.schema_id
CROSS APPLY
(
  SELECT
    STUFF((SELECT N'', '' + QUOTENAME(pc.name)
           FROM {#SOURCEDB_Q#}.sys.foreign_key_columns AS fkc2
           JOIN {#SOURCEDB_Q#}.sys.columns AS pc ON pc.object_id = fkc2.parent_object_id AND pc.column_id = fkc2.parent_column_id
           WHERE fkc2.constraint_object_id = fk.object_id
           ORDER BY fkc2.constraint_column_id
           FOR XML PATH(''''), TYPE).value(''.'', ''nvarchar(max)''), 1, 2, N'''') AS parent_cols,
    STUFF((SELECT N'', '' + QUOTENAME(rc.name)
           FROM {#SOURCEDB_Q#}.sys.foreign_key_columns AS fkc2
           JOIN {#SOURCEDB_Q#}.sys.columns AS rc ON rc.object_id = fkc2.referenced_object_id AND rc.column_id = fkc2.referenced_column_id
           WHERE fkc2.constraint_object_id = fk.object_id
           ORDER BY fkc2.constraint_column_id
           FOR XML PATH(''''), TYPE).value(''.'', ''nvarchar(max)''), 1, 2, N'''') AS ref_cols
) AS fkc
WHERE spt.name <> N''sys''
  -- Exclude FK on tables matching patterns in #ExcludedObjects (parent OR referenced table)
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (pt.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (spt.name + N''.'' + pt.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (pt.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (spt.name + N''.'' + pt.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  )
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (rt.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (srt.name + N''.'' + rt.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (rt.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (srt.name + N''.'' + rt.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 10-Triggers commands...';
	--=================================================-- 10: Table triggers --=============================================--
	-- CREATE TRIGGER must be the first statement in a batch, so we use sp_executesql with target DB context
	-- Robust approach: Find "CREATE...TRIGGER" pattern and insert " OR ALTER" after CREATE (if not already present)
	-- Triggers in #SkippedObjects are excluded (external DB refs or dependencies on skipped objects)
	-- NOTE: References to source database are replaced with target database
	SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''10-Triggers'',
  N''EXEC {#TARGETDB_Q#}.sys.sp_executesql N'''''' +
  REPLACE(
    REPLACE(
      REPLACE(
        -- Only add OR ALTER if not already present
        CASE 
          WHEN PATINDEX(N''%CREATE[^A-Za-z]%OR[^A-Za-z]%ALTER[^A-Za-z]%TRIGGER[^A-Za-z]%'', m.definition + N'' '') > 0 
            THEN m.definition  -- Already has OR ALTER, keep as is
          ELSE
            -- Use STUFF to insert " OR ALTER" after "CREATE" in the "CREATE...TRIGGER" pattern
            STUFF(m.definition, 
                  PATINDEX(N''%CREATE[^A-Za-z]%TRIGGER[^A-Za-z]%'', m.definition + N'' '') + 6,  -- +6 = length of "CREATE"
                  0,  -- Delete 0 characters
                  N'' OR ALTER'')  -- Insert " OR ALTER"
        END,
        N''[{#SOURCEDB#}].'', N''[{#TARGETDB#}].''  -- Replace [SourceDB]. with [TargetDB].
      ),
      N''{#SOURCEDB#}.'', N''{#TARGETDB#}.''   -- Replace SourceDB. with TargetDB. (unquoted)
    ),
    N'''''''', N''''''''''''
  ) + N'''''';''
FROM {#SOURCEDB_Q#}.sys.triggers AS tr
JOIN {#SOURCEDB_Q#}.sys.objects AS o ON o.object_id = tr.object_id
JOIN {#SOURCEDB_Q#}.sys.sql_modules AS m ON m.object_id = tr.object_id
JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = o.schema_id
JOIN {#SOURCEDB_Q#}.sys.tables AS pt ON pt.object_id = tr.parent_id
JOIN {#SOURCEDB_Q#}.sys.schemas AS ps ON ps.schema_id = pt.schema_id
WHERE s.name <> N''sys'' AND tr.parent_id IS NOT NULL AND m.definition IS NOT NULL
  AND PATINDEX(N''%CREATE[^A-Za-z]%TRIGGER[^A-Za-z]%'', m.definition + N'' '') > 0
  -- Exclude triggers in #SkippedObjects (external DB refs or cascade)
  AND NOT EXISTS (SELECT 1 FROM #SkippedObjects WHERE ObjectId = tr.object_id)
  -- Exclude triggers on tables matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (pt.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (ps.name + N''.'' + pt.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (pt.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (ps.name + N''.'' + pt.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  )
  -- Also exclude triggers by their own name
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (tr.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + tr.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (tr.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + tr.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 11-Views commands...';
	--=================================================-- 11: Views (ordered by dependency level) --=========================--
	-- Use recursive CTE to determine view dependency order
	-- A view may appear at multiple levels; we take MAX(dep_level) to get the correct creation order
	-- CREATE VIEW must be the first statement in a batch, so we use sp_executesql with target DB context
	-- NOTE: References to source database are replaced with target database
	-- Views in #SkippedObjects are excluded (external DB refs or dependencies on skipped objects)
	SET @DYNAMICSQL = N'
;WITH ViewDeps AS (
  -- Base: Views with no dependencies on other views
  SELECT v.object_id, 0 AS dep_level
  FROM {#SOURCEDB_Q#}.sys.views AS v
  WHERE NOT EXISTS (
    SELECT 1 FROM {#SOURCEDB_Q#}.sys.sql_expression_dependencies AS d
    JOIN {#SOURCEDB_Q#}.sys.views AS rv ON rv.object_id = d.referenced_id
    WHERE d.referencing_id = v.object_id
  )
  UNION ALL
  -- Recursive: Views that depend on views at the current level
  SELECT v.object_id, vd.dep_level + 1
  FROM {#SOURCEDB_Q#}.sys.views AS v
  JOIN {#SOURCEDB_Q#}.sys.sql_expression_dependencies AS d ON d.referencing_id = v.object_id
  JOIN ViewDeps AS vd ON vd.object_id = d.referenced_id
),
ViewMaxLevel AS (
  SELECT object_id, MAX(dep_level) AS max_level
  FROM ViewDeps
  GROUP BY object_id
)
INSERT INTO #Commands(Phase, Command)
SELECT
  N''11-Views'',
  N''EXEC {#TARGETDB_Q#}.sys.sp_executesql N'''''' +
  REPLACE(
    REPLACE(
      REPLACE(
        N''CREATE OR ALTER VIEW '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(v.name) + N'' '' +
        -- Extract everything after the view name (AS SELECT... or WITH ...)
        LTRIM(SUBSTRING(m.definition, body_pos.pos, LEN(m.definition))),
        N''[{#SOURCEDB#}].'', N''[{#TARGETDB#}].''  -- Replace [SourceDB]. with [TargetDB].
      ),
      N''{#SOURCEDB#}.'', N''{#TARGETDB#}.''   -- Replace SourceDB. with TargetDB. (unquoted)
    ),
    N'''''''', N''''''''''''
  ) + N'''''';''
FROM {#SOURCEDB_Q#}.sys.views AS v
JOIN {#SOURCEDB_Q#}.sys.sql_modules AS m ON m.object_id = v.object_id
JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = v.schema_id
LEFT JOIN ViewMaxLevel AS vml ON vml.object_id = v.object_id
CROSS APPLY (
  -- Find position of view name (quoted or unquoted) in definition
  SELECT COALESCE(
    NULLIF(CHARINDEX(QUOTENAME(v.name), m.definition), 0),
    NULLIF(CHARINDEX(v.name, m.definition), 0),
    1
  ) AS name_pos
) AS name_search
CROSS APPLY (
  -- Find position after view name: look for WITH or AS keyword after the view name
  SELECT
    name_search.name_pos + LEN(v.name) - 1 +
    CASE
      WHEN PATINDEX(N''%[^A-Za-z0-9_]WITH[^A-Za-z0-9_]%'', UPPER(SUBSTRING(m.definition, name_search.name_pos + LEN(v.name), 500))) > 0
       AND PATINDEX(N''%[^A-Za-z0-9_]WITH[^A-Za-z0-9_]%'', UPPER(SUBSTRING(m.definition, name_search.name_pos + LEN(v.name), 500))) 
         < ISNULL(NULLIF(PATINDEX(N''%[^A-Za-z0-9_]AS[^A-Za-z0-9_]%'', UPPER(SUBSTRING(m.definition, name_search.name_pos + LEN(v.name), 500))), 0), 999999)
        THEN PATINDEX(N''%[^A-Za-z0-9_]WITH[^A-Za-z0-9_]%'', UPPER(SUBSTRING(m.definition, name_search.name_pos + LEN(v.name), 500)))
      WHEN PATINDEX(N''%[^A-Za-z0-9_]AS[^A-Za-z0-9_]%'', UPPER(SUBSTRING(m.definition, name_search.name_pos + LEN(v.name), 500))) > 0
        THEN PATINDEX(N''%[^A-Za-z0-9_]AS[^A-Za-z0-9_]%'', UPPER(SUBSTRING(m.definition, name_search.name_pos + LEN(v.name), 500)))
      ELSE 1
    END AS pos
) AS body_pos
WHERE s.name <> N''sys''
  -- Exclude views in #SkippedObjects (external DB refs or cascade)
  AND NOT EXISTS (SELECT 1 FROM #SkippedObjects WHERE ObjectId = v.object_id)
  -- Exclude views matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (v.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + v.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (v.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + v.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  )
ORDER BY ISNULL(vml.max_level, 999)
OPTION (MAXRECURSION 0);';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 12-Functions commands...';
	--=================================================-- 12: Functions (ordered by dependency level) --=====================--
	-- Use recursive CTE to determine function dependency order
	-- A function may appear at multiple levels; we take MAX(dep_level) to get the correct creation order
	-- CREATE FUNCTION must be the first statement in a batch, so we use sp_executesql with target DB context
	-- NOTE: References to source database are replaced with target database
	-- NOTE: Functions with external DB refs OR cascade dependencies are already in #SkippedObjects

	SET @DYNAMICSQL = N'
;WITH FuncDeps AS (
  -- Base: Functions with no dependencies on other functions
  SELECT o.object_id, 0 AS dep_level
  FROM {#SOURCEDB_Q#}.sys.objects AS o
  WHERE o.[type] IN (N''FN'',N''TF'',N''IF'',N''FS'',N''FT'')
    AND NOT EXISTS (
      SELECT 1 FROM {#SOURCEDB_Q#}.sys.sql_expression_dependencies AS d
      JOIN {#SOURCEDB_Q#}.sys.objects AS ro ON ro.object_id = d.referenced_id AND ro.[type] IN (N''FN'',N''TF'',N''IF'',N''FS'',N''FT'')
      WHERE d.referencing_id = o.object_id
    )
  UNION ALL
  -- Recursive: Functions that depend on functions at the current level
  SELECT o.object_id, fd.dep_level + 1
  FROM {#SOURCEDB_Q#}.sys.objects AS o
  JOIN {#SOURCEDB_Q#}.sys.sql_expression_dependencies AS d ON d.referencing_id = o.object_id
  JOIN FuncDeps AS fd ON fd.object_id = d.referenced_id
  WHERE o.[type] IN (N''FN'',N''TF'',N''IF'',N''FS'',N''FT'')
),
FuncMaxLevel AS (
  SELECT object_id, MAX(dep_level) AS max_level
  FROM FuncDeps
  GROUP BY object_id
)
INSERT INTO #Commands(Phase, Command)
SELECT
  N''12-Functions'',
  N''EXEC {#TARGETDB_Q#}.sys.sp_executesql N'''''' +
  REPLACE(
    REPLACE(
      REPLACE(
        N''CREATE OR ALTER FUNCTION '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(o.name) + N'' '' +
        -- Extract everything after the function name (parameters and body)
        LTRIM(SUBSTRING(m.definition, body_pos.pos, LEN(m.definition))),
        N''[{#SOURCEDB#}].'', N''[{#TARGETDB#}].''  -- Replace [SourceDB]. with [TargetDB].
      ),
      N''{#SOURCEDB#}.'', N''{#TARGETDB#}.''   -- Replace SourceDB. with TargetDB. (unquoted)
    ),
    N'''''''', N''''''''''''
  ) + N'''''';''
FROM {#SOURCEDB_Q#}.sys.objects AS o
JOIN {#SOURCEDB_Q#}.sys.sql_modules AS m ON m.object_id = o.object_id
JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = o.schema_id
LEFT JOIN FuncMaxLevel AS fml ON fml.object_id = o.object_id
CROSS APPLY (
  -- Find position of function name (quoted or unquoted) in definition
  SELECT COALESCE(
    NULLIF(CHARINDEX(QUOTENAME(o.name), m.definition), 0),
    NULLIF(CHARINDEX(o.name, m.definition), 0),
    1
  ) AS name_pos
) AS name_search
CROSS APPLY (
  -- Find position of opening parenthesis AFTER the function name
  -- This avoids false positives from comments or schema names before the function
  SELECT name_search.name_pos + LEN(o.name) - 1 +
    ISNULL(NULLIF(CHARINDEX(N''('', SUBSTRING(m.definition, name_search.name_pos + LEN(o.name), 500)), 0), 1) AS pos
) AS body_pos
WHERE s.name <> N''sys'' AND o.[type] IN (N''FN'',N''TF'',N''IF'',N''FS'',N''FT'') AND body_pos.pos > name_search.name_pos
  -- EXCLUDE functions in #SkippedObjects (external DB refs or cascade dependencies)
  AND NOT EXISTS (SELECT 1 FROM #SkippedObjects WHERE ObjectId = o.object_id)
  -- Exclude functions matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (o.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + o.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (o.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + o.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  )
ORDER BY ISNULL(fml.max_level, 999)
OPTION (MAXRECURSION 0);';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 13-Procedures commands...';
	--=================================================-- 13: Stored procedures --==========================================--
	-- CREATE PROCEDURE must be the first statement in a batch, so we use sp_executesql with target DB context
	-- NOTE: References to source database are replaced with target database
	-- NOTE: Procedures with external DB refs OR cascade dependencies are already in #SkippedObjects
	-- Approach: Use STUFF to replace original CREATE PROCEDURE header with CREATE OR ALTER PROCEDURE [schema].[name]
	-- Robust: Search for procedure name AFTER the PROC/PROCEDURE keyword to handle comments and varying whitespace

	SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''13-Procedures'',
  N''EXEC {#TARGETDB_Q#}.sys.sp_executesql N'''''' +
  REPLACE(
    REPLACE(
      REPLACE(
        -- Use STUFF to replace from position 1 up to end of procedure name with new header
        STUFF(
          m.definition,
          1,
          -- Length to replace: from start to end of procedure name (searching AFTER "PROC" keyword - covers PROC and PROCEDURE)
          CASE 
            -- First try: find [Name] after PROC keyword
            WHEN CHARINDEX(QUOTENAME(p.name), m.definition, PATINDEX(N''%[^A-Za-z]PROC[^A-Za-z]%'', UPPER(N'' '' + m.definition))) > 0 
            THEN CHARINDEX(QUOTENAME(p.name), m.definition, PATINDEX(N''%[^A-Za-z]PROC[^A-Za-z]%'', UPPER(N'' '' + m.definition))) + LEN(QUOTENAME(p.name)) - 1
            -- Fallback: find unquoted Name after PROC keyword
            ELSE CHARINDEX(p.name, m.definition, PATINDEX(N''%[^A-Za-z]PROC[^A-Za-z]%'', UPPER(N'' '' + m.definition)) + 4) + LEN(p.name) - 1
          END,
          -- New header (replaces everything from CREATE to proc name)
          N''CREATE OR ALTER PROCEDURE '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(p.name)
        ),
        N''[{#SOURCEDB#}].'', N''[{#TARGETDB#}].''
      ),
      N''{#SOURCEDB#}.'', N''{#TARGETDB#}.'' 
    ),
    N'''''''', N''''''''''''
  ) + N'''''';''
FROM {#SOURCEDB_Q#}.sys.procedures AS p
JOIN {#SOURCEDB_Q#}.sys.sql_modules AS m ON m.object_id = p.object_id
JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = p.schema_id
WHERE s.name <> N''sys''
  -- EXCLUDE procedures in #SkippedObjects (external DB refs or cascade dependencies)
  AND NOT EXISTS (SELECT 1 FROM #SkippedObjects WHERE ObjectId = p.object_id)
  -- Exclude procedures matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (p.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + p.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (p.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + p.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 14-Statistics commands...';
	--=================================================-- 14: User-created statistics --====================================--
	SET @DYNAMICSQL = N'
INSERT INTO #Commands(Phase, Command)
SELECT
  N''14-Statistics'',
  N''USE {#TARGETDB_Q#}; CREATE STATISTICS '' + QUOTENAME(st.name) +
  N'' ON '' + QUOTENAME(s.name) + N''.'' + QUOTENAME(t.name) +
  N'' ('' + cols.stats_columns + N'')'' +
  CASE WHEN st.has_filter = 1 THEN N'' WHERE '' + REPLACE(REPLACE(st.filter_definition, N''[{#SOURCEDB#}].'', N''[{#TARGETDB#}].''), N''{#SOURCEDB#}.'', N''{#TARGETDB#}.'' ) ELSE N'''' END +
  CASE WHEN (st.no_recompute = 1 OR st.is_incremental = 1) THEN
    N'' WITH '' +
    STUFF(
      (CASE WHEN st.no_recompute = 1 THEN N'', NORECOMPUTE'' ELSE N'''' END) +
      (CASE WHEN st.is_incremental = 1 THEN N'', INCREMENTAL = ON'' ELSE N'''' END),
      1, 2, N''''
    )
  ELSE N'''' END +
  N'';''
FROM {#SOURCEDB_Q#}.sys.stats AS st
JOIN {#SOURCEDB_Q#}.sys.tables AS t ON t.object_id = st.object_id
JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = t.schema_id
LEFT JOIN {#SOURCEDB_Q#}.sys.indexes AS i ON i.object_id = st.object_id AND i.index_id = st.stats_id
CROSS APPLY
(
  SELECT STUFF((
    SELECT N'', '' + QUOTENAME(c.name)
    FROM {#SOURCEDB_Q#}.sys.stats_columns AS sc
    JOIN {#SOURCEDB_Q#}.sys.columns AS c ON c.object_id = sc.object_id AND c.column_id = sc.column_id
    WHERE sc.object_id = st.object_id AND sc.stats_id = st.stats_id
    ORDER BY sc.stats_column_id
    FOR XML PATH(''''), TYPE
  ).value(''.'', ''nvarchar(max)''), 1, 2, N'''') AS stats_columns
) AS cols
WHERE i.index_id IS NULL -- only user-created stats
  AND st.user_created = 1
  AND s.name <> N''sys''
  -- Exclude statistics on tables matching patterns in #ExcludedObjects
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (t.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (t.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	IF @DEBUG = 1 PRINT 'DEBUG: Generating 15-Synonyms commands...';
	--=================================================-- 15: Synonyms --===================================================--
	-- NOTE: Synonyms pointing to external databases (other than source) are skipped
	-- PARSENAME automatically removes brackets, so we only compare unquoted names
	-- NOTE: Synonyms pointing to source DB are included; base_object_name used as-is
	SET @DYNAMICSQL = N'
DECLARE @SYN_SCHEMA SYSNAME, @SYN_NAME SYSNAME, @SYN_TARGET NVARCHAR(1035);
DECLARE syn_cursor CURSOR LOCAL FAST_FORWARD FOR
SELECT s.name, sn.name, sn.base_object_name
FROM {#SOURCEDB_Q#}.sys.synonyms AS sn
JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = sn.schema_id
WHERE s.name <> N''sys''
  AND (PARSENAME(sn.base_object_name, 3) IS NULL OR PARSENAME(sn.base_object_name, 3) = N''{#SOURCEDB#}'')
  AND NOT EXISTS (
    SELECT 1 FROM #ExcludedObjects AS ex
    WHERE (ex.IsWildcard = 1 AND (sn.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + sn.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern))
       OR (ex.IsWildcard = 0 AND (sn.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + sn.name) COLLATE DATABASE_DEFAULT = ex.Pattern))
  );
OPEN syn_cursor;
FETCH NEXT FROM syn_cursor INTO @SYN_SCHEMA, @SYN_NAME, @SYN_TARGET;
WHILE @@FETCH_STATUS = 0
BEGIN
  -- base_object_name wird direkt verwendet - kein Escaping nötig da EXEC() verwendet wird
  INSERT INTO #Commands(Phase, Command)
  VALUES (N''15-Synonyms'', N''USE {#TARGETDB_Q#}; CREATE SYNONYM '' + QUOTENAME(@SYN_SCHEMA) + N''.'' + QUOTENAME(@SYN_NAME) + N'' FOR '' + @SYN_TARGET + N'';'');
  FETCH NEXT FROM syn_cursor INTO @SYN_SCHEMA, @SYN_NAME, @SYN_TARGET;
END;
CLOSE syn_cursor;
DEALLOCATE syn_cursor;';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;

	-- Log warnings for skipped synonyms with external DB references (using cursor approach)
	SET @DYNAMICSQL = N'
DECLARE @SYN_SCHEMA SYSNAME, @SYN_NAME SYSNAME, @SYN_TARGET NVARCHAR(1035);
DECLARE syn_warn_cursor CURSOR LOCAL FAST_FORWARD FOR
SELECT s.name, sn.name, sn.base_object_name
FROM {#SOURCEDB_Q#}.sys.synonyms AS sn
JOIN {#SOURCEDB_Q#}.sys.schemas AS s ON s.schema_id = sn.schema_id
WHERE s.name <> N''sys''
  AND PARSENAME(sn.base_object_name, 3) IS NOT NULL 
  AND PARSENAME(sn.base_object_name, 3) <> N''{#SOURCEDB#}'';
OPEN syn_warn_cursor;
FETCH NEXT FROM syn_warn_cursor INTO @SYN_SCHEMA, @SYN_NAME, @SYN_TARGET;
WHILE @@FETCH_STATUS = 0
BEGIN
  INSERT INTO #Commands(Phase, Command)
  VALUES (N''15-Synonyms-WARN'', N''PRINT N''''WARN: Synonym '' + QUOTENAME(@SYN_SCHEMA) + N''.'' + QUOTENAME(@SYN_NAME) + N'' points to external database and was SKIPPED: '' + @SYN_TARGET + N'''''';'');
  FETCH NEXT FROM syn_warn_cursor INTO @SYN_SCHEMA, @SYN_NAME, @SYN_TARGET;
END;
CLOSE syn_warn_cursor;
DEALLOCATE syn_warn_cursor;';
	-- Ersetze Platzhalter durch tatsächliche Werte
	SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
			@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
			@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
			@PH_SOURCEDB, @SOURCEDATABASE),
			@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- 99: Finalize (Set FULL recovery model, restore source access) --===--
	INSERT INTO #Commands(Phase, Command)
	VALUES
	(N'99-Finalize', N'USE [master]; ALTER DATABASE {#TARGETDB_Q#} SET RECOVERY FULL;');

	-- Restore source database to original user access mode
	INSERT INTO #Commands(Phase, Command)
	VALUES
	(N'99-Finalize', N'USE [master]; ALTER DATABASE {#SOURCEDB_Q#} SET ' + ISNULL(@SOURCE_USER_ACCESS_ORIGINAL, N'MULTI_USER') + N';');
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Output plan --====================================================--
	-- Replace placeholders in all commands before output/execution
	UPDATE #Commands SET Command = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(Command,
		@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
		@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
		@PH_SOURCEDB, @SOURCEDATABASE),
		@PH_TARGETDB, @TARGETDATABASE),
		@PH_BATCHSIZE, CONVERT(NVARCHAR(20), @BATCHSIZE));
	IF @DEBUG = 1 PRINT 'DEBUG: Placeholders in #Commands replaced with actual database names.';

	-- Filter commands based on MODE
	-- FULL: All phases
	-- SCHEMA: All phases except 04-Data and 04a-ShrinkTempdb
	-- DATA: Only 04-Data and 04a-ShrinkTempdb (target DB must exist)
	SELECT Step, Phase, Command
	FROM #Commands
	WHERE (@MODE = 'FULL')
	   OR (@MODE = 'SCHEMA' AND Phase NOT IN ('04-Data', '04a-ShrinkTempdb'))
	   OR (@MODE = 'DATA' AND Phase IN ('04-Data', '04a-ShrinkTempdb'))
	ORDER BY Step;
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Pre-Validation (compile all commands without execution) --=========--
	IF @PREVALIDATE = 1 AND @EXECUTECOMMANDS = 1 BEGIN
		PRINT '';
		PRINT '=================================================================';
		PRINT '=== PRE-VALIDATION: Checking SQL syntax of all commands...   ===';
		PRINT '=================================================================';
		PRINT '';

		-- Create temp table for validation errors
		CREATE TABLE #PreValidationErrors
		(
			Step         INT            NOT NULL,
			Phase        NVARCHAR(50)   NOT NULL,
			ErrorNumber  INT            NOT NULL,
			ErrorMessage NVARCHAR(4000) NOT NULL,
			Command      NVARCHAR(MAX)  NOT NULL
		);

		DECLARE @PREVAL_STEP          INT            = NULL;
		DECLARE @PREVAL_PHASE         NVARCHAR(50)   = NULL;
		DECLARE @PREVAL_COMMAND       NVARCHAR(MAX)  = NULL;
		DECLARE @PREVAL_ERRORS        INT            = 0;
		DECLARE @PREVAL_TOTAL         INT            = 0;
		DECLARE @PREVAL_SKIPPED       INT            = 0;
		DECLARE @PREVAL_ERRNUM        INT            = NULL;
		DECLARE @PREVAL_ERRMSG        NVARCHAR(4000) = NULL;

		-- Cursor through all commands to validate
		DECLARE preval_cur CURSOR FAST_FORWARD FOR
			SELECT Step, Phase, Command
			FROM #Commands
			WHERE (@MODE = 'FULL')
			   OR (@MODE = 'SCHEMA' AND Phase NOT IN ('04-Data', '04a-ShrinkTempdb'))
			   OR (@MODE = 'DATA' AND Phase IN ('04-Data', '04a-ShrinkTempdb'))
			ORDER BY Step;

		OPEN preval_cur;
		FETCH NEXT FROM preval_cur INTO @PREVAL_STEP, @PREVAL_PHASE, @PREVAL_COMMAND;

		WHILE @@FETCH_STATUS = 0 BEGIN
			SET @PREVAL_TOTAL = @PREVAL_TOTAL + 1;

			-- Skip certain phases that cannot be pre-validated:
			-- - 00-DropTarget: DROP/ALTER DATABASE cannot be validated
			-- - 01-CreateDb: CREATE DATABASE cannot run in a transaction
			-- - 99-Finalize: ALTER DATABASE statements may fail without proper context
			-- - 04-Data: INSERT statements may reference tables that don't exist yet
			-- - 15-Synonyms-WARN: Only PRINT statements, no SQL to validate
			-- - Phases that depend on previous phases having run successfully
			IF @PREVAL_PHASE IN (N'00-DropTarget', N'01-CreateDb', N'99-Finalize', N'04-Data', N'04a-ShrinkTempdb', N'15-Synonyms-WARN') BEGIN
				SET @PREVAL_SKIPPED = @PREVAL_SKIPPED + 1;
				IF @DEBUG = 1 PRINT N'DEBUG: Skipping pre-validation for ' + @PREVAL_PHASE + N' (cannot be validated before dependent objects exist)';
			END
			ELSE BEGIN
				-- Use SET PARSEONLY to check SQL syntax without execution
				-- PARSEONLY validates T-SQL syntax but doesn't verify object existence
				-- This catches syntax errors like malformed statements, incorrect escaping, etc.
				BEGIN TRY
					DECLARE @PREVAL_WRAPPER NVARCHAR(MAX);
					DECLARE @PREVAL_COMMAND_MODIFIED NVARCHAR(MAX);
					
					-- For syntax validation, replace USE [TargetDB] with USE [SourceDB]
					-- This allows validation even when target database doesn't exist yet
					-- The source database structure is identical, so syntax errors will be caught
					SET @PREVAL_COMMAND_MODIFIED = REPLACE(@PREVAL_COMMAND, N'USE ' + @TARGETDATABASE_QUOTED, N'USE ' + @SOURCEDATABASE_QUOTED);
					
					-- Method 1: SET PARSEONLY ON - validates basic T-SQL syntax
					-- This will catch: syntax errors, malformed quotes, incorrect keywords
					-- This will NOT catch: missing objects, wrong column names (those need runtime)
					SET @PREVAL_WRAPPER = N'SET PARSEONLY ON; ' + @PREVAL_COMMAND_MODIFIED;
					EXEC sys.sp_executesql @PREVAL_WRAPPER;
					
					IF @DEBUG = 1 PRINT N'DEBUG: Step ' + CONVERT(NVARCHAR(10), @PREVAL_STEP) + N' (' + @PREVAL_PHASE + N') - Syntax OK';
				END TRY
				BEGIN CATCH
					SET @PREVAL_ERRNUM = ERROR_NUMBER();
					SET @PREVAL_ERRMSG = ERROR_MESSAGE();
					
					-- Insert error into tracking table
					INSERT INTO #PreValidationErrors (Step, Phase, ErrorNumber, ErrorMessage, Command)
					VALUES (@PREVAL_STEP, @PREVAL_PHASE, @PREVAL_ERRNUM, @PREVAL_ERRMSG, @PREVAL_COMMAND);
					
					SET @PREVAL_ERRORS = @PREVAL_ERRORS + 1;
					PRINT N'ERROR: Step ' + CONVERT(NVARCHAR(10), @PREVAL_STEP) + N' (' + @PREVAL_PHASE + N'): ' + @PREVAL_ERRMSG;
					IF @DEBUG = 1 BEGIN
						PRINT N'DEBUG: Failed command (first 2000 chars):';
						PRINT LEFT(@PREVAL_COMMAND, 2000);
					END;
				END CATCH;
			END;

			FETCH NEXT FROM preval_cur INTO @PREVAL_STEP, @PREVAL_PHASE, @PREVAL_COMMAND;
		END;

		CLOSE preval_cur;
		DEALLOCATE preval_cur;

		-- Summary
		PRINT '';
		PRINT '=================================================================';
		PRINT N'=== PRE-VALIDATION SUMMARY                                    ===';
		PRINT '=================================================================';
		PRINT N'Total commands:   ' + CONVERT(NVARCHAR(10), @PREVAL_TOTAL);
		PRINT N'Validated:        ' + CONVERT(NVARCHAR(10), @PREVAL_TOTAL - @PREVAL_SKIPPED);
		PRINT N'Skipped:          ' + CONVERT(NVARCHAR(10), @PREVAL_SKIPPED) + N' (CREATE/ALTER DATABASE statements)';
		PRINT N'Errors found:     ' + CONVERT(NVARCHAR(10), @PREVAL_ERRORS);
		PRINT '=================================================================';
		PRINT '';

		-- If errors were found, output detailed error list and abort
		IF @PREVAL_ERRORS > 0 BEGIN
			PRINT N'PRE-VALIDATION FAILED - The following commands have syntax errors:';
			PRINT '';

			-- Output all errors with details
			DECLARE @ERR_STEP INT, @ERR_PHASE NVARCHAR(50), @ERR_NUM INT, @ERR_MSG NVARCHAR(4000), @ERR_CMD NVARCHAR(MAX);
			DECLARE err_cur CURSOR FAST_FORWARD FOR
				SELECT Step, Phase, ErrorNumber, ErrorMessage, Command FROM #PreValidationErrors ORDER BY Step;
			
			OPEN err_cur;
			FETCH NEXT FROM err_cur INTO @ERR_STEP, @ERR_PHASE, @ERR_NUM, @ERR_MSG, @ERR_CMD;
			
			WHILE @@FETCH_STATUS = 0 BEGIN
				PRINT N'─────────────────────────────────────────────────────────────';
				PRINT N'Step ' + CONVERT(NVARCHAR(10), @ERR_STEP) + N' (' + @ERR_PHASE + N')';
				PRINT N'Error ' + CONVERT(NVARCHAR(10), @ERR_NUM) + N': ' + @ERR_MSG;
				PRINT N'Command (first 4000 chars):';
				PRINT LEFT(@ERR_CMD, 4000);
				IF LEN(@ERR_CMD) > 4000 PRINT N'... (truncated, total ' + CONVERT(NVARCHAR(20), LEN(@ERR_CMD)) + N' chars)';
				PRINT '';
				FETCH NEXT FROM err_cur INTO @ERR_STEP, @ERR_PHASE, @ERR_NUM, @ERR_MSG, @ERR_CMD;
			END;
			
			CLOSE err_cur;
			DEALLOCATE err_cur;

			-- Log pre-validation failure
			IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
				INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] 
					([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
				VALUES 
					(@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 
					 'Pre-validation FAILED - ' + CONVERT(NVARCHAR(10), @PREVAL_ERRORS) + ' syntax errors found',
					 'Migration aborted before execution due to pre-validation errors', @MY_PROCEDURE_NAME, GETDATE());
			END;

			-- Cleanup temp tables
			DROP TABLE #PreValidationErrors;

			-- Restore source database to original access mode before aborting
			BEGIN TRY
				SET @DYNAMICSQL = N'ALTER DATABASE ' + @SOURCEDATABASE_QUOTED + N' SET ' + ISNULL(@SOURCE_USER_ACCESS_ORIGINAL, N'MULTI_USER') + N';';
				IF @DEBUG = 1 PRINT @DYNAMICSQL;
				EXEC sys.sp_executesql @DYNAMICSQL;
				PRINT 'INFO: Source database restored to ' + ISNULL(@SOURCE_USER_ACCESS_ORIGINAL, 'MULTI_USER') + ' mode.';
			END TRY BEGIN CATCH
				PRINT 'WARN: Could not restore source database: ' + ERROR_MESSAGE();
			END CATCH;

			-- Cleanup
			IF OBJECT_ID('tempdb..#Commands') IS NOT NULL DROP TABLE #Commands;
			IF OBJECT_ID('tempdb..#ExcludedObjects') IS NOT NULL DROP TABLE #ExcludedObjects;
			IF OBJECT_ID('tempdb..#SkippedObjects') IS NOT NULL DROP TABLE #SkippedObjects;

			RAISERROR(N'Pre-validation failed with %d syntax error(s). Migration aborted. Please fix the errors and try again.', 16, 1, @PREVAL_ERRORS);
			RETURN 1;
		END;

		-- Pre-validation passed
		PRINT N'PRE-VALIDATION PASSED - All commands have valid SQL syntax.';
		PRINT N'Proceeding with execution...';
		PRINT '';

		-- Log pre-validation success
		IF (@LOGLEVEL IN ('INFO')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] 
				([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES 
				(@RUN_ID, 'INFO', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 
				 'Pre-validation PASSED - ' + CONVERT(NVARCHAR(10), @PREVAL_TOTAL - @PREVAL_SKIPPED) + ' commands validated', @MY_PROCEDURE_NAME, GETDATE());
		END;

		-- Cleanup
		DROP TABLE #PreValidationErrors;
	END;
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Execute plan --===================================================--
	IF @EXECUTECOMMANDS = 1 BEGIN
		-- Log execution start
		IF (@LOGLEVEL IN ('INFO')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] ([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES (@RUN_ID, 'INFO', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Starting execution of migration plan', @MY_PROCEDURE_NAME, GETDATE());
		END;

		DECLARE cmd_cur CURSOR FAST_FORWARD FOR
			SELECT Step, Phase, Command
			FROM #Commands
			WHERE (@MODE = 'FULL')
			   OR (@MODE = 'SCHEMA' AND Phase NOT IN ('04-Data', '04a-ShrinkTempdb'))
			   OR (@MODE = 'DATA' AND Phase IN ('04-Data', '04a-ShrinkTempdb'))
			ORDER BY Step;

		OPEN cmd_cur;
		FETCH NEXT FROM cmd_cur INTO @CURRENTSTEP, @CURRENTPHASE, @CURRENTCOMMAND;
		IF @DEBUG = 1 PRINT 'DEBUG: Starting command execution loop...';
		WHILE @@FETCH_STATUS = 0 BEGIN
			SET @STEPSTARTTIME = GETDATE();
			SET @ROWCOUNT_AFFECTED = 0;

			BEGIN TRY
				PRINT N'Executing step ' + CONVERT(NVARCHAR(20), @CURRENTSTEP) + N' (' + @CURRENTPHASE + N')';
				IF @DEBUG = 1 PRINT 'DEBUG: Command length: ' + CONVERT(NVARCHAR(20), LEN(@CURRENTCOMMAND)) + ' chars';
				EXEC (@CURRENTCOMMAND);
				SET @ROWCOUNT_AFFECTED = @@ROWCOUNT;
				IF @DEBUG = 1 PRINT 'DEBUG: Step completed, rows affected: ' + CONVERT(NVARCHAR(20), @ROWCOUNT_AFFECTED);

				-- Track total rows migrated for data copy phase
				IF @CURRENTPHASE = N'04-Data' BEGIN
					SET @TOTAL_ROWS_MIGRATED = @TOTAL_ROWS_MIGRATED + @ROWCOUNT_AFFECTED;
				END;

				-- Log successful step
				IF (@LOGLEVEL IN ('INFO')) BEGIN
					INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] 
						([RUN_ID], [LOG_LEVEL], [STEP], [PHASE], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [COMMAND], [ROWS_AFFECTED], [DURATION_MS], [ADDED_WHO], [ADDED_WHEN])
					VALUES 
						(@RUN_ID, 'INFO', @CURRENTSTEP, @CURRENTPHASE, @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 
						 'Step completed successfully', @CURRENTCOMMAND, @ROWCOUNT_AFFECTED, 
						 DATEDIFF(MILLISECOND, @STEPSTARTTIME, GETDATE()), @MY_PROCEDURE_NAME, GETDATE());
				END;
			END TRY BEGIN CATCH
				SET @ERRORMESSAGE = ERROR_MESSAGE();
				PRINT N'FAILED step ' + CONVERT(NVARCHAR(20), @CURRENTSTEP) + N' (' + @CURRENTPHASE + N'): ' + @ERRORMESSAGE;
				IF @DEBUG = 1 BEGIN
					PRINT N'DEBUG: FAILED COMMAND (first 4000 chars):';
					PRINT LEFT(@CURRENTCOMMAND, 4000);
					IF LEN(@CURRENTCOMMAND) > 4000 BEGIN
						PRINT N'DEBUG: ... (command truncated, total length: ' + CONVERT(NVARCHAR(20), LEN(@CURRENTCOMMAND)) + N' chars)';
					END;
				END;

				-- Log error with FULL command for manual rework
				IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
					INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] 
						([RUN_ID], [LOG_LEVEL], [STEP], [PHASE], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [COMMAND], [DURATION_MS], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
					VALUES 
						(@RUN_ID, 'ERROR', @CURRENTSTEP, @CURRENTPHASE, @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 
						 'Step failed with error', @CURRENTCOMMAND, 
						 DATEDIFF(MILLISECOND, @STEPSTARTTIME, GETDATE()), @ERRORMESSAGE, @MY_PROCEDURE_NAME, GETDATE());
				END;

				IF @STOPONERROR = 1 BEGIN
					-- Log abortion
					IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
						INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] 
							([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ROWS_AFFECTED], [DURATION_MS], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
						VALUES 
							(@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 
							 'Migration ABORTED due to error (STOPONERROR=1)', @TOTAL_ROWS_MIGRATED, 
							 DATEDIFF(MILLISECOND, @STARTTIME, GETDATE()), @ERRORMESSAGE, @MY_PROCEDURE_NAME, GETDATE());
					END;

					-- Restore source database to original user access mode before aborting
					BEGIN TRY
						SET @DYNAMICSQL = N'ALTER DATABASE {#SOURCEDB_Q#} SET ' + ISNULL(@SOURCE_USER_ACCESS_ORIGINAL, N'MULTI_USER') + N';';
						-- Ersetze Platzhalter durch tatsächliche Werte
						SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
								@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
								@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
								@PH_SOURCEDB, @SOURCEDATABASE),
								@PH_TARGETDB, @TARGETDATABASE);
							IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
							IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
							BEGIN TRY
								EXEC sys.sp_executesql @DYNAMICSQL;
							END TRY BEGIN CATCH
								PRINT N'ERROR in dynamic SQL:';
								PRINT @DYNAMICSQL;
								THROW;
							END CATCH;
						PRINT 'INFO: Source database restored to ' + ISNULL(@SOURCE_USER_ACCESS_ORIGINAL, 'MULTI_USER') + ' mode.';
					END TRY BEGIN CATCH
						PRINT 'WARN: Could not restore source database to ' + ISNULL(@SOURCE_USER_ACCESS_ORIGINAL, 'MULTI_USER') + ': ' + ERROR_MESSAGE();
					END CATCH;

					-- Cleanup
					IF OBJECT_ID('tempdb..#Commands') IS NOT NULL DROP TABLE #Commands;
					IF OBJECT_ID('tempdb..#ExcludedObjects') IS NOT NULL DROP TABLE #ExcludedObjects;
					IF OBJECT_ID('tempdb..#SkippedObjects') IS NOT NULL DROP TABLE #SkippedObjects;

					CLOSE cmd_cur;
					DEALLOCATE cmd_cur;
					THROW;
				END;
			END CATCH;

			FETCH NEXT FROM cmd_cur INTO @CURRENTSTEP, @CURRENTPHASE, @CURRENTCOMMAND;
		END;

		CLOSE cmd_cur;
		DEALLOCATE cmd_cur;

		-- Log successful completion
		IF (@LOGLEVEL IN ('INFO')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] 
				([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ROWS_AFFECTED], [DURATION_MS], [ADDED_WHO], [ADDED_WHEN])
			VALUES 
				(@RUN_ID, 'INFO', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 
				 'Migration completed successfully', @TOTAL_ROWS_MIGRATED, 
				 DATEDIFF(MILLISECOND, @STARTTIME, GETDATE()), @MY_PROCEDURE_NAME, GETDATE());
		END;

		PRINT N'';
		PRINT N'====================================================================================================';
		PRINT N'Migration completed successfully!';
		PRINT N'Total rows migrated: ' + CONVERT(NVARCHAR(20), @TOTAL_ROWS_MIGRATED);
		PRINT N'Total duration: ' + CONVERT(NVARCHAR(20), DATEDIFF(SECOND, @STARTTIME, GETDATE())) + N' seconds';
		PRINT N'====================================================================================================';

		--=================================================-- Validation Phase --==============================================--
		IF @VALIDATION_MODE <> 'NONE' BEGIN
			PRINT N'';
			PRINT N'====================================================================================================';
			PRINT N'Starting validation (' + @VALIDATION_MODE + ')...';
			PRINT N'====================================================================================================';

			-- Reset validation counters for this run
			SET @VALIDATION_PASSED = 1;
			SET @VALIDATION_ERRORS = 0;
			SET @VALIDATION_WARNINGS = 0;

			-- Create validation results table
			CREATE TABLE #ValidationResults (
				ValidationType   NVARCHAR(20)  NOT NULL,  -- 'SCHEMA' or 'DATA'
				ObjectType       NVARCHAR(50)  NOT NULL,
				ObjectName       NVARCHAR(256) NULL,
				SourceCount      BIGINT        NOT NULL,
				TargetCount      BIGINT        NOT NULL,
				Status           NVARCHAR(10)  NOT NULL,  -- 'OK', 'WARN', 'ERROR'
				Message          NVARCHAR(500) NULL
			);

			--===================== SCHEMA VALIDATION =====================--
			IF @VALIDATION_MODE IN ('ALL', 'SCHEMA') BEGIN
				PRINT N'';
				PRINT N'--- Schema Validation ---';

				-- Validate Tables
				SET @VALIDATION_SQL = N'
				SELECT @src = COUNT(*) FROM {#SOURCEDB_Q#}.sys.tables t
				JOIN {#SOURCEDB_Q#}.sys.schemas s ON s.schema_id = t.schema_id
				WHERE s.name <> N''sys'' AND t.is_ms_shipped = 0
				  AND NOT EXISTS (SELECT 1 FROM #ExcludedObjects ex WHERE (ex.IsWildcard = 1 AND (t.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern)) OR (ex.IsWildcard = 0 AND (t.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT = ex.Pattern)));';
				SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

				EXEC sys.sp_executesql @VALIDATION_SQL, N'@src INT OUTPUT', @src = @VALIDATION_SOURCE_COUNT OUTPUT;

				SET @VALIDATION_SQL = N'SELECT @tgt = COUNT(*) FROM {#TARGETDB_Q#}.sys.tables t JOIN {#TARGETDB_Q#}.sys.schemas s ON s.schema_id = t.schema_id WHERE s.name <> N''sys'' AND t.is_ms_shipped = 0;';
				SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

				EXEC sys.sp_executesql @VALIDATION_SQL, N'@tgt INT OUTPUT', @tgt = @VALIDATION_TARGET_COUNT OUTPUT;

				INSERT INTO #ValidationResults VALUES ('SCHEMA', 'Tables', NULL, @VALIDATION_SOURCE_COUNT, @VALIDATION_TARGET_COUNT, 
					CASE WHEN @VALIDATION_SOURCE_COUNT = @VALIDATION_TARGET_COUNT THEN 'OK' ELSE 'ERROR' END,
					CASE WHEN @VALIDATION_SOURCE_COUNT = @VALIDATION_TARGET_COUNT THEN NULL ELSE 'Mismatch: Source=' + CONVERT(NVARCHAR(10), @VALIDATION_SOURCE_COUNT) + ', Target=' + CONVERT(NVARCHAR(10), @VALIDATION_TARGET_COUNT) END);
				IF @VALIDATION_SOURCE_COUNT <> @VALIDATION_TARGET_COUNT BEGIN SET @VALIDATION_PASSED = 0; SET @VALIDATION_ERRORS = @VALIDATION_ERRORS + 1; END;
				PRINT 'Tables: Source=' + CONVERT(NVARCHAR(10), @VALIDATION_SOURCE_COUNT) + ', Target=' + CONVERT(NVARCHAR(10), @VALIDATION_TARGET_COUNT) + CASE WHEN @VALIDATION_SOURCE_COUNT = @VALIDATION_TARGET_COUNT THEN ' [OK]' ELSE ' [ERROR]' END;

				-- Validate Views (accounting for #SkippedObjects)
				SET @VALIDATION_SQL = N'
				SELECT @src = COUNT(*) FROM {#SOURCEDB_Q#}.sys.views v
				JOIN {#SOURCEDB_Q#}.sys.schemas s ON s.schema_id = v.schema_id
				WHERE s.name <> N''sys''
				  AND NOT EXISTS (SELECT 1 FROM #SkippedObjects sk WHERE sk.ObjectId = v.object_id)
				  AND NOT EXISTS (SELECT 1 FROM #ExcludedObjects ex WHERE (ex.IsWildcard = 1 AND (v.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + v.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern)) OR (ex.IsWildcard = 0 AND (v.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + v.name) COLLATE DATABASE_DEFAULT = ex.Pattern)));';
				SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

				EXEC sys.sp_executesql @VALIDATION_SQL, N'@src INT OUTPUT', @src = @VALIDATION_SOURCE_COUNT OUTPUT;

				SET @VALIDATION_SQL = N'SELECT @tgt = COUNT(*) FROM {#TARGETDB_Q#}.sys.views v JOIN {#TARGETDB_Q#}.sys.schemas s ON s.schema_id = v.schema_id WHERE s.name <> N''sys'';';
				SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

				EXEC sys.sp_executesql @VALIDATION_SQL, N'@tgt INT OUTPUT', @tgt = @VALIDATION_TARGET_COUNT OUTPUT;

				INSERT INTO #ValidationResults VALUES ('SCHEMA', 'Views', NULL, @VALIDATION_SOURCE_COUNT, @VALIDATION_TARGET_COUNT,
					CASE WHEN @VALIDATION_SOURCE_COUNT = @VALIDATION_TARGET_COUNT THEN 'OK' ELSE 'ERROR' END, NULL);
				IF @VALIDATION_SOURCE_COUNT <> @VALIDATION_TARGET_COUNT BEGIN SET @VALIDATION_PASSED = 0; SET @VALIDATION_ERRORS = @VALIDATION_ERRORS + 1; END;
				PRINT 'Views: Source=' + CONVERT(NVARCHAR(10), @VALIDATION_SOURCE_COUNT) + ', Target=' + CONVERT(NVARCHAR(10), @VALIDATION_TARGET_COUNT) + CASE WHEN @VALIDATION_SOURCE_COUNT = @VALIDATION_TARGET_COUNT THEN ' [OK]' ELSE ' [ERROR]' END;

				-- Validate Stored Procedures (accounting for #SkippedObjects)
				SET @VALIDATION_SQL = N'
				SELECT @src = COUNT(*) FROM {#SOURCEDB_Q#}.sys.procedures p
				JOIN {#SOURCEDB_Q#}.sys.schemas s ON s.schema_id = p.schema_id
				WHERE s.name <> N''sys''
				  AND NOT EXISTS (SELECT 1 FROM #SkippedObjects sk WHERE sk.ObjectId = p.object_id)
				  AND NOT EXISTS (SELECT 1 FROM #ExcludedObjects ex WHERE (ex.IsWildcard = 1 AND (p.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + p.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern)) OR (ex.IsWildcard = 0 AND (p.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + p.name) COLLATE DATABASE_DEFAULT = ex.Pattern)));';
				SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

				EXEC sys.sp_executesql @VALIDATION_SQL, N'@src INT OUTPUT', @src = @VALIDATION_SOURCE_COUNT OUTPUT;

				SET @VALIDATION_SQL = N'SELECT @tgt = COUNT(*) FROM {#TARGETDB_Q#}.sys.procedures p JOIN {#TARGETDB_Q#}.sys.schemas s ON s.schema_id = p.schema_id WHERE s.name <> N''sys'';';
				SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

				EXEC sys.sp_executesql @VALIDATION_SQL, N'@tgt INT OUTPUT', @tgt = @VALIDATION_TARGET_COUNT OUTPUT;

				INSERT INTO #ValidationResults VALUES ('SCHEMA', 'Procedures', NULL, @VALIDATION_SOURCE_COUNT, @VALIDATION_TARGET_COUNT,
					CASE WHEN @VALIDATION_SOURCE_COUNT = @VALIDATION_TARGET_COUNT THEN 'OK' ELSE 'ERROR' END, NULL);
				IF @VALIDATION_SOURCE_COUNT <> @VALIDATION_TARGET_COUNT BEGIN SET @VALIDATION_PASSED = 0; SET @VALIDATION_ERRORS = @VALIDATION_ERRORS + 1; END;
				PRINT 'Procedures: Source=' + CONVERT(NVARCHAR(10), @VALIDATION_SOURCE_COUNT) + ', Target=' + CONVERT(NVARCHAR(10), @VALIDATION_TARGET_COUNT) + CASE WHEN @VALIDATION_SOURCE_COUNT = @VALIDATION_TARGET_COUNT THEN ' [OK]' ELSE ' [ERROR]' END;

				-- Validate Functions (accounting for #SkippedObjects)
				SET @VALIDATION_SQL = N'
				SELECT @src = COUNT(*) FROM {#SOURCEDB_Q#}.sys.objects o
				JOIN {#SOURCEDB_Q#}.sys.schemas s ON s.schema_id = o.schema_id
				WHERE s.name <> N''sys'' AND o.[type] IN (N''FN'',N''TF'',N''IF'',N''FS'',N''FT'')
				  AND NOT EXISTS (SELECT 1 FROM #SkippedObjects sk WHERE sk.ObjectId = o.object_id)
				  AND NOT EXISTS (SELECT 1 FROM #ExcludedObjects ex WHERE (ex.IsWildcard = 1 AND (o.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + o.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern)) OR (ex.IsWildcard = 0 AND (o.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + o.name) COLLATE DATABASE_DEFAULT = ex.Pattern)));';
				SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

				EXEC sys.sp_executesql @VALIDATION_SQL, N'@src INT OUTPUT', @src = @VALIDATION_SOURCE_COUNT OUTPUT;

				SET @VALIDATION_SQL = N'SELECT @tgt = COUNT(*) FROM {#TARGETDB_Q#}.sys.objects o JOIN {#TARGETDB_Q#}.sys.schemas s ON s.schema_id = o.schema_id WHERE s.name <> N''sys'' AND o.[type] IN (N''FN'',N''TF'',N''IF'',N''FS'',N''FT'');';
				SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

				EXEC sys.sp_executesql @VALIDATION_SQL, N'@tgt INT OUTPUT', @tgt = @VALIDATION_TARGET_COUNT OUTPUT;

				INSERT INTO #ValidationResults VALUES ('SCHEMA', 'Functions', NULL, @VALIDATION_SOURCE_COUNT, @VALIDATION_TARGET_COUNT,
					CASE WHEN @VALIDATION_SOURCE_COUNT = @VALIDATION_TARGET_COUNT THEN 'OK' ELSE 'ERROR' END, NULL);
				IF @VALIDATION_SOURCE_COUNT <> @VALIDATION_TARGET_COUNT BEGIN SET @VALIDATION_PASSED = 0; SET @VALIDATION_ERRORS = @VALIDATION_ERRORS + 1; END;
				PRINT 'Functions: Source=' + CONVERT(NVARCHAR(10), @VALIDATION_SOURCE_COUNT) + ', Target=' + CONVERT(NVARCHAR(10), @VALIDATION_TARGET_COUNT) + CASE WHEN @VALIDATION_SOURCE_COUNT = @VALIDATION_TARGET_COUNT THEN ' [OK]' ELSE ' [ERROR]' END;

				-- Validate Triggers (accounting for #SkippedObjects)
				SET @VALIDATION_SQL = N'
				SELECT @src = COUNT(*) FROM {#SOURCEDB_Q#}.sys.triggers tr
				JOIN {#SOURCEDB_Q#}.sys.objects o ON o.object_id = tr.object_id
				JOIN {#SOURCEDB_Q#}.sys.schemas s ON s.schema_id = o.schema_id
				JOIN {#SOURCEDB_Q#}.sys.tables pt ON pt.object_id = tr.parent_id
				JOIN {#SOURCEDB_Q#}.sys.schemas ps ON ps.schema_id = pt.schema_id
				WHERE s.name <> N''sys'' AND tr.parent_id IS NOT NULL
				  AND NOT EXISTS (SELECT 1 FROM #SkippedObjects sk WHERE sk.ObjectId = tr.object_id)
				  AND NOT EXISTS (SELECT 1 FROM #ExcludedObjects ex WHERE (ex.IsWildcard = 1 AND (pt.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (ps.name + N''.'' + pt.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern)) OR (ex.IsWildcard = 0 AND (pt.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (ps.name + N''.'' + pt.name) COLLATE DATABASE_DEFAULT = ex.Pattern)))
				  AND NOT EXISTS (SELECT 1 FROM #ExcludedObjects ex WHERE (ex.IsWildcard = 1 AND (tr.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + tr.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern)) OR (ex.IsWildcard = 0 AND (tr.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + tr.name) COLLATE DATABASE_DEFAULT = ex.Pattern)));';
				SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

				EXEC sys.sp_executesql @VALIDATION_SQL, N'@src INT OUTPUT', @src = @VALIDATION_SOURCE_COUNT OUTPUT;

				SET @VALIDATION_SQL = N'SELECT @tgt = COUNT(*) FROM {#TARGETDB_Q#}.sys.triggers tr JOIN {#TARGETDB_Q#}.sys.objects o ON o.object_id = tr.object_id JOIN {#TARGETDB_Q#}.sys.schemas s ON s.schema_id = o.schema_id WHERE s.name <> N''sys'' AND tr.parent_id IS NOT NULL;';
				SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

				EXEC sys.sp_executesql @VALIDATION_SQL, N'@tgt INT OUTPUT', @tgt = @VALIDATION_TARGET_COUNT OUTPUT;

				INSERT INTO #ValidationResults VALUES ('SCHEMA', 'Triggers', NULL, @VALIDATION_SOURCE_COUNT, @VALIDATION_TARGET_COUNT,
					CASE WHEN @VALIDATION_SOURCE_COUNT = @VALIDATION_TARGET_COUNT THEN 'OK' ELSE 'ERROR' END, NULL);
				IF @VALIDATION_SOURCE_COUNT <> @VALIDATION_TARGET_COUNT BEGIN SET @VALIDATION_PASSED = 0; SET @VALIDATION_ERRORS = @VALIDATION_ERRORS + 1; END;
				PRINT 'Triggers: Source=' + CONVERT(NVARCHAR(10), @VALIDATION_SOURCE_COUNT) + ', Target=' + CONVERT(NVARCHAR(10), @VALIDATION_TARGET_COUNT) + CASE WHEN @VALIDATION_SOURCE_COUNT = @VALIDATION_TARGET_COUNT THEN ' [OK]' ELSE ' [ERROR]' END;

				-- Validate Indexes
				SET @VALIDATION_SQL = N'
				SELECT @src = COUNT(*) FROM {#SOURCEDB_Q#}.sys.indexes i
				JOIN {#SOURCEDB_Q#}.sys.tables t ON t.object_id = i.object_id
				JOIN {#SOURCEDB_Q#}.sys.schemas s ON s.schema_id = t.schema_id
				WHERE s.name <> N''sys'' AND i.index_id > 0
				  AND NOT EXISTS (SELECT 1 FROM #ExcludedObjects ex WHERE (ex.IsWildcard = 1 AND (t.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern)) OR (ex.IsWildcard = 0 AND (t.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT = ex.Pattern)));';
				SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

				EXEC sys.sp_executesql @VALIDATION_SQL, N'@src INT OUTPUT', @src = @VALIDATION_SOURCE_COUNT OUTPUT;

				SET @VALIDATION_SQL = N'SELECT @tgt = COUNT(*) FROM {#TARGETDB_Q#}.sys.indexes i JOIN {#TARGETDB_Q#}.sys.tables t ON t.object_id = i.object_id JOIN {#TARGETDB_Q#}.sys.schemas s ON s.schema_id = t.schema_id WHERE s.name <> N''sys'' AND i.index_id > 0;';
				SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

				EXEC sys.sp_executesql @VALIDATION_SQL, N'@tgt INT OUTPUT', @tgt = @VALIDATION_TARGET_COUNT OUTPUT;

				INSERT INTO #ValidationResults VALUES ('SCHEMA', 'Indexes', NULL, @VALIDATION_SOURCE_COUNT, @VALIDATION_TARGET_COUNT,
					CASE WHEN @VALIDATION_SOURCE_COUNT = @VALIDATION_TARGET_COUNT THEN 'OK' ELSE 'WARN' END, NULL);
				IF @VALIDATION_SOURCE_COUNT <> @VALIDATION_TARGET_COUNT BEGIN SET @VALIDATION_WARNINGS = @VALIDATION_WARNINGS + 1; END;
				PRINT 'Indexes: Source=' + CONVERT(NVARCHAR(10), @VALIDATION_SOURCE_COUNT) + ', Target=' + CONVERT(NVARCHAR(10), @VALIDATION_TARGET_COUNT) + CASE WHEN @VALIDATION_SOURCE_COUNT = @VALIDATION_TARGET_COUNT THEN ' [OK]' ELSE ' [WARN]' END;

				-- Validate Foreign Keys
				SET @VALIDATION_SQL = N'
				SELECT @src = COUNT(*) FROM {#SOURCEDB_Q#}.sys.foreign_keys fk
				JOIN {#SOURCEDB_Q#}.sys.tables pt ON pt.object_id = fk.parent_object_id
				JOIN {#SOURCEDB_Q#}.sys.tables rt ON rt.object_id = fk.referenced_object_id
				JOIN {#SOURCEDB_Q#}.sys.schemas spt ON spt.schema_id = pt.schema_id
				JOIN {#SOURCEDB_Q#}.sys.schemas srt ON srt.schema_id = rt.schema_id
				WHERE spt.name <> N''sys''
				  AND NOT EXISTS (SELECT 1 FROM #ExcludedObjects ex WHERE (ex.IsWildcard = 1 AND (pt.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (spt.name + N''.'' + pt.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern)) OR (ex.IsWildcard = 0 AND (pt.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (spt.name + N''.'' + pt.name) COLLATE DATABASE_DEFAULT = ex.Pattern)))
				  AND NOT EXISTS (SELECT 1 FROM #ExcludedObjects ex WHERE (ex.IsWildcard = 1 AND (rt.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (srt.name + N''.'' + rt.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern)) OR (ex.IsWildcard = 0 AND (rt.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (srt.name + N''.'' + rt.name) COLLATE DATABASE_DEFAULT = ex.Pattern)));';
				SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

				EXEC sys.sp_executesql @VALIDATION_SQL, N'@src INT OUTPUT', @src = @VALIDATION_SOURCE_COUNT OUTPUT;

				SET @VALIDATION_SQL = N'SELECT @tgt = COUNT(*) FROM {#TARGETDB_Q#}.sys.foreign_keys fk JOIN {#TARGETDB_Q#}.sys.tables pt ON pt.object_id = fk.parent_object_id JOIN {#TARGETDB_Q#}.sys.schemas spt ON spt.schema_id = pt.schema_id WHERE spt.name <> N''sys'';';
				SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

				EXEC sys.sp_executesql @VALIDATION_SQL, N'@tgt INT OUTPUT', @tgt = @VALIDATION_TARGET_COUNT OUTPUT;

				INSERT INTO #ValidationResults VALUES ('SCHEMA', 'ForeignKeys', NULL, @VALIDATION_SOURCE_COUNT, @VALIDATION_TARGET_COUNT,
					CASE WHEN @VALIDATION_SOURCE_COUNT = @VALIDATION_TARGET_COUNT THEN 'OK' ELSE 'ERROR' END, NULL);
				IF @VALIDATION_SOURCE_COUNT <> @VALIDATION_TARGET_COUNT BEGIN SET @VALIDATION_PASSED = 0; SET @VALIDATION_ERRORS = @VALIDATION_ERRORS + 1; END;
				PRINT 'ForeignKeys: Source=' + CONVERT(NVARCHAR(10), @VALIDATION_SOURCE_COUNT) + ', Target=' + CONVERT(NVARCHAR(10), @VALIDATION_TARGET_COUNT) + CASE WHEN @VALIDATION_SOURCE_COUNT = @VALIDATION_TARGET_COUNT THEN ' [OK]' ELSE ' [ERROR]' END;
			END;

			--===================== DATA VALIDATION =====================--
			IF @VALIDATION_MODE IN ('ALL', 'DATA') AND @MODE IN ('FULL', 'DATA') BEGIN
				PRINT N'';
				PRINT N'--- Data Validation (Row Counts) ---';

				-- Create temp table to hold table list for validation
				CREATE TABLE #DataValidationTables (
					SchemaName NVARCHAR(128) NOT NULL,
					TableName NVARCHAR(128) NOT NULL
				);

				-- Populate table list dynamically
				SET @VALIDATION_CURSOR_SQL = N'
				INSERT INTO #DataValidationTables (SchemaName, TableName)
					SELECT s.name AS SchemaName, t.name AS TableName
					FROM {#SOURCEDB_Q#}.sys.tables t
					JOIN {#SOURCEDB_Q#}.sys.schemas s ON s.schema_id = t.schema_id
					WHERE s.name <> N''sys'' AND t.is_ms_shipped = 0
					  AND NOT EXISTS (SELECT 1 FROM #ExcludedObjects ex WHERE (ex.IsWildcard = 1 AND (t.name COLLATE DATABASE_DEFAULT LIKE ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT LIKE ex.Pattern)) OR (ex.IsWildcard = 0 AND (t.name COLLATE DATABASE_DEFAULT = ex.Pattern OR (s.name + N''.'' + t.name) COLLATE DATABASE_DEFAULT = ex.Pattern)))
					ORDER BY s.name, t.name;';
				SET @VALIDATION_CURSOR_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_CURSOR_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);
				EXEC sys.sp_executesql @VALIDATION_CURSOR_SQL;

				-- Declare cursor on temp table (cursor visible in current scope)
				DECLARE data_val_cur CURSOR FAST_FORWARD FOR
					SELECT SchemaName, TableName FROM #DataValidationTables ORDER BY SchemaName, TableName;

				OPEN data_val_cur;
				FETCH NEXT FROM data_val_cur INTO @VALIDATION_SCHEMA_NAME, @VALIDATION_TABLE_NAME;
				WHILE @@FETCH_STATUS = 0 BEGIN
					-- Get source row count
					BEGIN TRY
						SET @VALIDATION_SQL = N'SELECT @cnt = COUNT_BIG(*) FROM {#SOURCEDB_Q#}.' + QUOTENAME(@VALIDATION_SCHEMA_NAME) + N'.' + QUOTENAME(@VALIDATION_TABLE_NAME) + N';';
						SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

						EXEC sys.sp_executesql @VALIDATION_SQL, N'@cnt BIGINT OUTPUT', @cnt = @VALIDATION_SOURCE_ROWS OUTPUT;
					END TRY BEGIN CATCH
						SET @VALIDATION_SOURCE_ROWS = -1;  -- Error reading source table
						PRINT N'WARN: Could not read source table ' + @VALIDATION_SCHEMA_NAME + N'.' + @VALIDATION_TABLE_NAME + N': ' + ERROR_MESSAGE();
					END CATCH;

					-- Get target row count
					BEGIN TRY
						SET @VALIDATION_SQL = N'SELECT @cnt = COUNT_BIG(*) FROM {#TARGETDB_Q#}.' + QUOTENAME(@VALIDATION_SCHEMA_NAME) + N'.' + QUOTENAME(@VALIDATION_TABLE_NAME) + N';';
						SET @VALIDATION_SQL = REPLACE(REPLACE(REPLACE(REPLACE(@VALIDATION_SQL, @PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED), @PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED), @PH_SOURCEDB, @SOURCEDATABASE), @PH_TARGETDB, @TARGETDATABASE);

						EXEC sys.sp_executesql @VALIDATION_SQL, N'@cnt BIGINT OUTPUT', @cnt = @VALIDATION_TARGET_ROWS OUTPUT;
					END TRY BEGIN CATCH
						SET @VALIDATION_TARGET_ROWS = -1;  -- Table doesn't exist
					END CATCH;

					INSERT INTO #ValidationResults VALUES ('DATA', 'TableRows', @VALIDATION_SCHEMA_NAME + N'.' + @VALIDATION_TABLE_NAME, @VALIDATION_SOURCE_ROWS, @VALIDATION_TARGET_ROWS,
						CASE WHEN @VALIDATION_SOURCE_ROWS = @VALIDATION_TARGET_ROWS THEN 'OK' WHEN @VALIDATION_TARGET_ROWS = -1 THEN 'ERROR' ELSE 'ERROR' END,
						CASE WHEN @VALIDATION_SOURCE_ROWS = @VALIDATION_TARGET_ROWS THEN NULL WHEN @VALIDATION_TARGET_ROWS = -1 THEN 'Table missing in target' ELSE 'Row mismatch' END);

					IF @VALIDATION_SOURCE_ROWS <> @VALIDATION_TARGET_ROWS BEGIN 
						SET @VALIDATION_PASSED = 0; 
						SET @VALIDATION_ERRORS = @VALIDATION_ERRORS + 1;
						PRINT @VALIDATION_SCHEMA_NAME + N'.' + @VALIDATION_TABLE_NAME + N': Source=' + CONVERT(NVARCHAR(20), @VALIDATION_SOURCE_ROWS) + N', Target=' + CONVERT(NVARCHAR(20), @VALIDATION_TARGET_ROWS) + N' [ERROR]';
					END; ELSE BEGIN
						PRINT @VALIDATION_SCHEMA_NAME + N'.' + @VALIDATION_TABLE_NAME + N': ' + CONVERT(NVARCHAR(20), @VALIDATION_SOURCE_ROWS) + N' rows [OK]';
					END;

					FETCH NEXT FROM data_val_cur INTO @VALIDATION_SCHEMA_NAME, @VALIDATION_TABLE_NAME;
				END;
				CLOSE data_val_cur;
				DEALLOCATE data_val_cur;

				-- Cleanup temp table
				IF OBJECT_ID('tempdb..#DataValidationTables') IS NOT NULL DROP TABLE #DataValidationTables;
			END;

			-- Log validation results
			IF (@LOGLEVEL IN ('INFO')) BEGIN
				INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] 
					([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ADDED_WHO], [ADDED_WHEN])
				SELECT @RUN_ID, 
					CASE WHEN Status = 'ERROR' THEN 'ERROR' WHEN Status = 'WARN' THEN 'WARN' ELSE 'INFO' END,
					@SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION,
					N'Validation ' + ValidationType + N' - ' + ObjectType + ISNULL(N': ' + ObjectName, N'') + N' - Source=' + CONVERT(NVARCHAR(20), SourceCount) + N', Target=' + CONVERT(NVARCHAR(20), TargetCount) + N' [' + Status + N']',
					@MY_PROCEDURE_NAME, GETDATE()
				FROM #ValidationResults;
			END;

			-- Output validation summary
			PRINT N'';
			PRINT N'====================================================================================================';
			IF @VALIDATION_PASSED = 1 BEGIN
				PRINT N'VALIDATION PASSED! All objects and data migrated successfully.';
				IF @VALIDATION_WARNINGS > 0 BEGIN
					PRINT N'Warnings: ' + CONVERT(NVARCHAR(10), @VALIDATION_WARNINGS);
				END;

				-- Set target database to MULTI_USER mode
				PRINT N'';
				PRINT N'Setting target database to MULTI_USER mode...';
				BEGIN TRY
					SET @DYNAMICSQL = N'ALTER DATABASE {#TARGETDB_Q#} SET MULTI_USER;';
					-- Ersetze Platzhalter durch tatsächliche Werte
					SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
							@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
							@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
							@PH_SOURCEDB, @SOURCEDATABASE),
					@PH_TARGETDB, @TARGETDATABASE);
						IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
						IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
						BEGIN TRY
							EXEC sys.sp_executesql @DYNAMICSQL;
						END TRY BEGIN CATCH
							PRINT N'ERROR in dynamic SQL:';
							PRINT @DYNAMICSQL;
							THROW;
						END CATCH;
					PRINT N'SUCCESS: Target database ' + @TARGETDATABASE + N' is now in MULTI_USER mode.';

					IF (@LOGLEVEL IN ('INFO')) BEGIN
						INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] 
							([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ADDED_WHO], [ADDED_WHEN])
						VALUES (@RUN_ID, 'INFO', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 
							'Validation PASSED - Target database set to MULTI_USER', @MY_PROCEDURE_NAME, GETDATE());
					END;
				END TRY BEGIN CATCH
					PRINT N'WARNING: Could not set target database to MULTI_USER: ' + ERROR_MESSAGE();
				END CATCH;
			END; ELSE BEGIN
				SET @RETURN_STATUS = 2;  -- Validation failed
				PRINT N'VALIDATION FAILED! Errors: ' + CONVERT(NVARCHAR(10), @VALIDATION_ERRORS) + N', Warnings: ' + CONVERT(NVARCHAR(10), @VALIDATION_WARNINGS);
				PRINT N'Target database remains in RESTRICTED_USER mode. Please review and fix issues manually.';

				IF (@LOGLEVEL IN ('INFO', 'WARN', 'ERROR')) BEGIN
					INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] 
						([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
					VALUES (@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 
						'Validation FAILED - Target database remains in RESTRICTED_USER', 
						'Errors: ' + CONVERT(NVARCHAR(10), @VALIDATION_ERRORS) + ', Warnings: ' + CONVERT(NVARCHAR(10), @VALIDATION_WARNINGS),
						@MY_PROCEDURE_NAME, GETDATE());
				END;
			END;
			PRINT N'====================================================================================================';

			-- Output validation details table
			SELECT ValidationType, ObjectType, ObjectName, SourceCount, TargetCount, Status, Message 
			FROM #ValidationResults 
			ORDER BY CASE Status WHEN 'ERROR' THEN 1 WHEN 'WARN' THEN 2 ELSE 3 END, ValidationType, ObjectType;

			IF OBJECT_ID('tempdb..#ValidationResults') IS NOT NULL DROP TABLE #ValidationResults;
		END; -- End of validation
		----------------------------------------------------------------------------------------------------------------------------

	END; ELSE BEGIN
		-- If not executing, restore source database to original user access mode immediately
		PRINT 'INFO: Restoring source database to ' + ISNULL(@SOURCE_USER_ACCESS_ORIGINAL, 'MULTI_USER') + ' mode (script-only mode)...';
		BEGIN TRY
			SET @DYNAMICSQL = N'ALTER DATABASE {#SOURCEDB_Q#} SET ' + ISNULL(@SOURCE_USER_ACCESS_ORIGINAL, N'MULTI_USER') + N';';
			-- Ersetze Platzhalter durch tatsächliche Werte
			SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
					@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
					@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
					@PH_SOURCEDB, @SOURCEDATABASE),
					@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
			PRINT 'INFO: Source database restored to ' + ISNULL(@SOURCE_USER_ACCESS_ORIGINAL, 'MULTI_USER') + ' mode.';
			SET @SOURCE_LOCKED = 0;
		END TRY BEGIN CATCH
			PRINT 'WARN: Could not restore source database to ' + ISNULL(@SOURCE_USER_ACCESS_ORIGINAL, 'MULTI_USER') + ': ' + ERROR_MESSAGE();
		END CATCH;

		-- Log script-only mode
		IF (@LOGLEVEL IN ('INFO')) BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG] 
				([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [DURATION_MS], [ADDED_WHO], [ADDED_WHEN])
			VALUES 
				(@RUN_ID, 'INFO', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 
				 'Script generated (EXECUTE=0, no commands executed)', 
				 DATEDIFF(MILLISECOND, @STARTTIME, GETDATE()), @MY_PROCEDURE_NAME, GETDATE());
		END;
	END;
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Cleanup --=========================================================--
	IF OBJECT_ID('tempdb..#Commands') IS NOT NULL DROP TABLE #Commands;
	IF OBJECT_ID('tempdb..#ExcludedObjects') IS NOT NULL DROP TABLE #ExcludedObjects;
	IF OBJECT_ID('tempdb..#SkippedObjects') IS NOT NULL DROP TABLE #SkippedObjects;
	----------------------------------------------------------------------------------------------------------------------------

	--=================================================-- Important hint --==================================================--
	IF @VALIDATION_MODE = 'NONE' OR @EXECUTECOMMANDS = 0 BEGIN
		PRINT N'';
		PRINT N'NOTE: Target database may still be in RESTRICTED_USER mode.';
		PRINT N'      Use @pVALIDATE=ALL to auto-validate and set to MULTI_USER on success,';
		PRINT N'      or set it manually: ALTER DATABASE ' + ISNULL(@TARGETDATABASE_QUOTED, '[TargetDB]') + N' SET MULTI_USER;';
	END;
	----------------------------------------------------------------------------------------------------------------------------

	END TRY
	BEGIN CATCH
		SET @ERRORMESSAGE = ERROR_MESSAGE();
		IF ISNULL(@RETURN_STATUS,0) = 0 SET @RETURN_STATUS = 1;

		-- Best-effort restore of source database access if it was locked
		IF @SOURCE_LOCKED = 1 AND @SOURCEDATABASE_QUOTED IS NOT NULL BEGIN
			BEGIN TRY
				SET @DYNAMICSQL = N'ALTER DATABASE {#SOURCEDB_Q#} SET ' + ISNULL(@SOURCE_USER_ACCESS_ORIGINAL, N'MULTI_USER') + N';';
				-- Ersetze Platzhalter durch tatsächliche Werte
				SET @DYNAMICSQL = REPLACE(REPLACE(REPLACE(REPLACE(@DYNAMICSQL, 
						@PH_SOURCEDB_QUOTED, @SOURCEDATABASE_QUOTED),
						@PH_TARGETDB_QUOTED, @TARGETDATABASE_QUOTED),
						@PH_SOURCEDB, @SOURCEDATABASE),
						@PH_TARGETDB, @TARGETDATABASE);
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) <= 4000 PRINT @DYNAMICSQL;
	IF @DEBUG = 1 AND LEN(@DYNAMICSQL) > 4000 PRINT LEFT(@DYNAMICSQL, 4000) + N'...(truncated)';
	BEGIN TRY
		EXEC sys.sp_executesql @DYNAMICSQL;
	END TRY BEGIN CATCH
		PRINT N'ERROR in dynamic SQL:';
		PRINT @DYNAMICSQL;
		THROW;
	END CATCH;
				SET @SOURCE_LOCKED = 0;
			END TRY BEGIN CATCH
				PRINT 'WARN: CATCH could not restore source database access: ' + ERROR_MESSAGE();
			END CATCH;
		END;

		-- Drop temp tables if they exist
		IF OBJECT_ID('tempdb..#Commands') IS NOT NULL DROP TABLE #Commands;
		IF OBJECT_ID('tempdb..#ExcludedObjects') IS NOT NULL DROP TABLE #ExcludedObjects;
		IF OBJECT_ID('tempdb..#SkippedObjects') IS NOT NULL DROP TABLE #SkippedObjects;

		-- Log error if logging is enabled and the table exists
		IF (@LOGLEVEL IN ('INFO','WARN','ERROR')) AND OBJECT_ID(N'dbo.TBDD_MIGRATE_DATABASE_LOG','U') IS NOT NULL BEGIN
			INSERT INTO [dbo].[TBDD_MIGRATE_DATABASE_LOG]
				([RUN_ID], [LOG_LEVEL], [SOURCE_DATABASE], [TARGET_DATABASE], [NEW_COLLATION], [MESSAGE], [ERROR_MESSAGE], [ADDED_WHO], [ADDED_WHEN])
			VALUES
				(@RUN_ID, 'ERROR', @SOURCEDATABASE, @TARGETDATABASE, @NEWCOLLATION, 'Unhandled error in PRDD_MIGRATE_DATABASE', @ERRORMESSAGE, @MY_PROCEDURE_NAME, GETDATE());
		END;

		RAISERROR(@ERRORMESSAGE, 16, 1);
	END CATCH;

	RETURN @RETURN_STATUS;

END;
GO