I posted a procedure last night, but I deleted it, because that procedure only moves foreign keys. The procedure below also moves indexes and triggers.
The idea is this: you move the table to the guest schema:
ALTER SCHEMA guest TRANSFER dbo.tbl
Then you create the table new in the dbo schema and copy data over. Then you run:
EXEC MoveFkeyIxTri 'tbl'
And it will copy all constraints, triggers and indexes, and re-target all referencing constraints. Once you done this, you can drop the old table.
Since the procedure is a couple of hundred lines, I wanted to attach it, but the Q&A platform was not really co-operative in that regard today.
/*--------------------------------------------------------------------------------------------------
This procedures copies foreign key, triggers and non-constraint indexes from guest.@tblname to
@tblname and re-targets referencing foreign keys from pointing to guest.@tblname to @tbl.
guest.@tblname is assumed to be the old version of @tblname, and @tblname is the new one. Date
should have been copied to @tblname prior to running this procedure.
@tblname must be in the dbo schema.
If some object (fkey, index of trigger) is not to be copied, for instance because the definition
is no longer valid, pass it in @exceptions.
The copying part of the procedure is not transactional, and any erros are trapped and displayed,
but execution continues. (Since in case of accidents you can drop @tbl and start over). The part
that moves the referencing foreign keys is transactional and must pass in whole. (To save you from
having a mess to clean up.)
----------------------------------------------------------------------------------------------------*/
go
-- Preliminaries. Since we have a table type, we cannot use CREATE OR ALTER, but we need to
-- drop, so type can be recreated.
DROP PROCEDURE IF EXISTS MoveFkeyIxTri
DROP TYPE IF EXISTS ExceptionList
CREATE TYPE ExceptionList AS TABLE (exception sysname NOT NULL PRIMARY KEY)
go
CREATE PROCEDURE MoveFkeyIxTri @tblname sysname,
@exceptions ExceptionList READONLY AS
SET XACT_ABORT, NOCOUNT ON
BEGIN TRY
DECLARE @old_tbl nvarchar(150),
@new_tbl sysname,
@old_tbl_id int,
@new_tbl_id int,
@index_id int,
@stats_id int,
@ref_table nvarchar(1024),
@ref_schema nvarchar(129),
@cnst_name nvarchar(1024),
@fkcols nvarchar(MAX),
@parentcols nvarchar(MAX),
@upd_action nvarchar(60),
@del_action nvarchar(60),
@repl_action nvarchar(60),
@sql nvarchar(MAX),
@cur CURSOR
SELECT @old_tbl = 'guest.' + quotename(@tblname),
@new_tbl = 'dbo.' + quotename(@tblname)
SELECT @old_tbl_id = object_id(@old_tbl), @new_tbl_id = object_id(@new_tbl)
IF @old_tbl_id IS NULL
RAISERROR('The table %s does not exist.', 16, 1, @old_tbl)
IF @new_tbl_id IS NULL
RAISERROR('The table %s does not exist.', 16, 1, @new_tbl)
-- Basic check that data bas been copied.
IF (SELECT MAX(rows) FROM sys.partitions WHERE object_id = @old_tbl_id) > 0 AND
(SELECT MAX(rows) FROM sys.partitions WHERE object_id = @new_tbl_id) = 0
RAISERROR('The data in %s has not been copied to %s', 16, 1, @old_tbl, @new_tbl)
SET @cur = CURSOR STATIC LOCAL FOR
SELECT CASE WHEN f.referenced_object_id <> f.parent_object_id
THEN quotename(s.name) + '.' + quotename(r.name)
ELSE @new_tbl
END,
quotename(f.name),
fc1.collist.value('.', 'nvarchar(MAX)'),
fc2.collist.value('.', 'nvarchar(MAX)'),
replace(f.update_referential_action_desc, '_', ' '),
replace(f.delete_referential_action_desc, '_', ' '),
CASE WHEN f.is_not_for_replication = 1
THEN 'NOT FOR REPLICATION'
ELSE ''
END
FROM sys.foreign_keys f
JOIN sys.objects r ON f.referenced_object_id = r.object_id
JOIN sys.schemas s ON r.schema_id = s.schema_id
CROSS APPLY (SELECT quotename(c.name) + ','
FROM sys.foreign_key_columns fc
JOIN sys.columns c ON fc.parent_object_id = c.object_id
AND fc.parent_column_id = c.column_id
WHERE fc.constraint_object_id = f.object_id
ORDER BY fc.constraint_column_id
FOR XML PATH(''), TYPE) AS fc1(collist)
CROSS APPLY (SELECT quotename(c.name) + ','
FROM sys.foreign_key_columns fc
JOIN sys.columns c ON fc.referenced_object_id = c.object_id
AND fc.referenced_column_id = c.column_id
WHERE fc.constraint_object_id = f.object_id
ORDER BY fc.constraint_column_id
FOR XML PATH(''), TYPE) AS fc2(collist)
WHERE f.parent_object_id = @old_tbl_id
AND f.name NOT IN (SELECT e.exception FROM @exceptions e)
OPEN @cur
WHILE 1 = 1
BEGIN
FETCH @cur INTO @ref_table, @cnst_name, @fkcols,
@parentcols, @upd_action, @del_action, @repl_action
IF @@FETCH_STATUS <> 0
BREAK
SELECT @sql = 'ALTER TABLE ' + @new_tbl + ' ADD CONSTRAINT ' + @cnst_name + '
FOREIGN KEY (' + substring(@fkcols, 1, len(@fkcols) - 1) + ')
REFERENCES ' + @ref_table + '(' +
substring(@parentcols, 1, len(@parentcols) - 1) + ')
ON UPDATE ' + @upd_action + '
ON DELETE ' + @del_action + ' ' + @repl_action
PRINT @sql
EXEC(@sql)
END
-- Copy non-key indexes.
SET @cur = CURSOR STATIC FOR
SELECT index_id
FROM sys.indexes
WHERE is_primary_key = 0
AND is_unique_constraint = 0
AND is_hypothetical = 0
AND object_id = @old_tbl_id
AND name NOT IN (SELECT exception FROM @exceptions)
OPEN @cur
WHILE 1 = 1
BEGIN
FETCH @cur INTO @index_id
IF @@FETCH_STATUS <> 0
BREAK
SELECT @sql = 'CREATE ' +
CASE i.is_unique WHEN 1 THEN 'UNIQUE ' ELSE '' END +
CASE i.index_id WHEN 1 THEN 'CLUSTERED ' ELSE '' END +
CASE WHEN i.type = 3 THEN 'XML '
WHEN i.type = 4 THEN 'SPATIAL '
WHEN i.type IN (5, 6) THEN 'COLUMNSTORE '
ELSE ''
END + 'INDEX ' + quotename(i.name) +
' ON ' + @new_tbl +
'(' + substring(ic.collist.value('.', 'nvarchar(MAX)'), 1,
len(ic.collist.value('.', 'nvarchar(MAX)')) - 1) + ')'
FROM sys.indexes i
CROSS APPLY (SELECT quotename(c.name) +
CASE ic.is_descending_key
WHEN 1 THEN ' DESC'
ELSE ''
END + ','
FROM sys.index_columns ic
JOIN sys.columns c ON ic.object_id = c.object_id
AND ic.column_id = c.column_id
WHERE ic.object_id = @old_tbl_id
AND ic.index_id = @index_id
AND (i.type > 2 OR
ic.key_ordinal > 0)
ORDER BY ic.key_ordinal
FOR XML PATH(''), TYPE) AS ic(collist)
WHERE object_id = @old_tbl_id
AND index_id = @index_id
-- Add any included columns. (We need to do this query by query, since a big
-- query fails with a QP error, or least it did once upon a time.)
IF EXISTS (SELECT *
FROM sys.index_columns
WHERE object_id = @old_tbl_id
AND index_id = @index_id
AND is_included_column = 1)
BEGIN
SELECT @sql += ' INCLUDE(' +
substring(ic.incllist.value('.', 'nvarchar(MAX)'), 1,
len(ic.incllist.value('.', 'nvarchar(MAX)')) - 1) + ')'
FROM (SELECT quotename(c.name) + ','
FROM sys.index_columns ic
JOIN sys.columns c ON ic.object_id = c.object_id
AND ic.column_id = c.column_id
WHERE ic.object_id = @old_tbl_id
AND ic.index_id = @index_id
AND ic.is_included_column = 1
ORDER BY ic.index_column_id
FOR XML PATH(''), TYPE) AS ic(incllist)
END
-- Tack on index filter and IGNORE_DUP_KEY setting. Thet latter is always there,
-- so that we know that with have WITH section for the rest.
SELECT @sql += CASE WHEN i.filter_definition IS NOT NULL
THEN ' WHERE ' + i.filter_definition + ' '
ELSE ''
END +
' WITH (IGNORE_DUP_KEY = ' + IIF(i.ignore_dup_key = 0, 'OFF', 'ON') + ',
FILLFACTOR = ' + IIF(i.fill_factor = 0, '100',
CONVERT(varchar(10), i.fill_factor)) + ',
PAD_INDEX = ' + IIF(i.is_padded = 0, 'OFF', 'ON') + ',
STATISTICS_NORECOMPUTE = ' + IIF(s.no_recompute = 0, 'OFF', 'ON') + ',
ALLOW_ROW_LOCKS = ' + IIF(i.allow_row_locks = 0, 'OFF', 'ON') + ',
ALLOW_PAGE_LOCKS = ' + IIF(i.allow_page_locks = 0, 'OFF', 'ON') + ',
DATA_COMPRESSION = ' + p.data_compression_desc + ')'
FROM sys.indexes i
JOIN sys.stats s ON i.object_id = s.object_id
AND i.index_id = s.stats_id
JOIN sys.partitions p ON p.object_id = i.object_id
AND p.index_id = i.index_id
AND p.partition_number = 1
WHERE i.object_id = @old_tbl_id
AND i.index_id = @index_id
PRINT @sql
EXEC(@sql)
END
--- Copy triggers.
SET @cur = CURSOR STATIC FOR
SELECT sm.definition
FROM sys.triggers t
JOIN sys.sql_modules sm ON t.object_id = sm.object_id
WHERE t.parent_id = @old_tbl_id
AND t.name NOT IN (SELECT e.exception FROM @exceptions e)
OPEN @cur
WHILE 1 = 1
BEGIN
FETCH @cur INTO @sql
IF @@FETCH_STATUS <> 0
BREAK
PRINT @sql
EXEC(@sql)
END
-- Time to move referencing foreign keys.
SET @cur = CURSOR STATIC FOR
SELECT quotename(s.name),
quotename(s.name) + '.' + quotename(r.name),
quotename(f.name),
fc1.collist.value('.', 'nvarchar(MAX)'),
fc2.collist.value('.', 'nvarchar(MAX)'),
replace(f.update_referential_action_desc, '_', ' '),
replace(f.delete_referential_action_desc, '_', ' '),
CASE WHEN f.is_not_for_replication = 1
THEN 'NOT FOR REPLICATION'
ELSE ''
END
FROM sys.foreign_keys f
JOIN sys.objects r ON f.parent_object_id = r.object_id
JOIN sys.schemas s ON r.schema_id = s.schema_id
CROSS APPLY (SELECT quotename(c.name) + ','
FROM sys.foreign_key_columns fc
JOIN sys.columns c ON fc.parent_object_id = c.object_id
AND fc.parent_column_id = c.column_id
WHERE fc.constraint_object_id = f.object_id
ORDER BY fc.constraint_column_id
FOR XML PATH(''), TYPE) AS fc1(collist)
CROSS APPLY (SELECT quotename(c.name) + ','
FROM sys.foreign_key_columns fc
JOIN sys.columns c ON fc.referenced_object_id = c.object_id
AND fc.referenced_column_id = c.column_id
WHERE fc.constraint_object_id = f.object_id
ORDER BY fc.constraint_column_id
FOR XML PATH(''), TYPE) AS fc2(collist)
WHERE f.referenced_object_id = @old_tbl_id
AND f.referenced_object_id <> f.parent_object_id
AND f.name NOT IN (SELECT e.exception FROM @exceptions e)
OPEN @cur
BEGIN TRANSACTION
WHILE 1 = 1
BEGIN
FETCH @cur INTO @ref_schema, @ref_table, @cnst_name, @fkcols,
@parentcols, @upd_action, @del_action, @repl_action
IF @@fetch_status <> 0
BREAK
-- Drop old consraint, so that the new can be added.
SELECT @sql = 'ALTER TABLE ' + @ref_table + ' DROP CONSTRAINT ' + @cnst_name
PRINT @sql
EXEC (@sql)
-- Then add the new one.
SELECT @sql = 'ALTER TABLE ' + @ref_table + ' ADD CONSTRAINT ' + @cnst_name + '
FOREIGN KEY (' + substring(@fkcols, 1, len(@fkcols) - 1) + ')
REFERENCES ' + @new_tbl + '(' +
substring(@parentcols, 1, len(@parentcols) - 1) + ')
ON UPDATE ' + @upd_action + '
ON DELETE ' + @del_action + ' ' + @repl_action
PRINT @sql
EXEC(@sql)
END
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF @@trancount > 0 ROLLBACK TRANSACTION
; THROW
END CATCH