ป้ายกำกับ: description

SQL Server: batch migrate sync tableSQL Server: batch migrate sync table

เป็นเพราะว่า sa ใน team มีคนเข้ามาแก้ table หลายคน แถมบาง database ก็ไม่ได้แก้ จะมาไล่แก้ทุก ๆ ที่มันก็ไม่ไหว เขียน sync script ออกมาเพื่อที่จะให้มันเปลี่ยน name, type, order, description, Foreign Keys, Indexes ได้จากจุดเดียวไปเลย
batch_migrate_sync_table.sql

-- USE DB_DEV;
-- USE DB_QA;
-- USE DB_PROD;
 
SET NOCOUNT ON;

BEGIN TRY
BEGIN TRANSACTION

 -- =============================================
 -- 1. CONFIGURATION
 -- =============================================
 DECLARE @SchemaName NVARCHAR(128) = ''
 DECLARE @TableName NVARCHAR(128) = 'wp_posts'

 -- Table Description
 DECLARE @TableDesc NVARCHAR(4000) = N'สำหรับเก็บ post';

 -- Check for NULL or Empty Schema -> Default to 'dbo'
 IF @SchemaName IS NULL OR LTRIM(RTRIM(@SchemaName)) = ''
  SET @SchemaName = 'dbo';

 -- Standard Variables
 DECLARE @TempTableName NVARCHAR(128) = @TableName + '_dup'
 DECLARE @OldTableName NVARCHAR(128) = @TableName + '_old'
 DECLARE @TargetObjID INT = OBJECT_ID(QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName))

 -- Dynamic SQL Variables
 DECLARE @SqlDropFK NVARCHAR(MAX) = '', @SqlCreateFK NVARCHAR(MAX) = '', @SqlCreatePK_Indexes NVARCHAR(MAX) = ''
 DECLARE @CreateTableSQL NVARCHAR(MAX), @CopySQL NVARCHAR(MAX), @DropOldSQL NVARCHAR(MAX)
 DECLARE @ColList NVARCHAR(MAX) = '', @SharedColList NVARCHAR(MAX) = '', @Msg NVARCHAR(MAX)
 DECLARE @IdentityColName NVARCHAR(128) = NULL
 DECLARE @PreserveIdentity BIT = 0
 DECLARE @CurrentFullName NVARCHAR(256), @TempFullName NVARCHAR(256)

 -- Cursor Helpers
 DECLARE @Cur_ColName NVARCHAR(128), @Cur_DataType NVARCHAR(100), @Cur_IsNullable CHAR(1), @Cur_Desc NVARCHAR(4000)
 DECLARE @Cur_FKID INT, @Cur_FKName NVARCHAR(128), @Cur_ParentSchema NVARCHAR(128), @Cur_ParentTable NVARCHAR(128)
 DECLARE @Helper_FKCols NVARCHAR(MAX), @Helper_RefCols NVARCHAR(MAX)
 DECLARE @Cur_IdxID INT, @Cur_IdxName NVARCHAR(128), @Cur_IsPK BIT, @Cur_IsUnique BIT, @Cur_IdxType TINYINT
 DECLARE @Helper_IdxCols NVARCHAR(MAX), @Helper_IncCols NVARCHAR(MAX)

 -- =============================================
 -- 2. DEFINE NEW STRUCTURE
 -- =============================================
 DECLARE @ColumnDefs TABLE (
  ID INT IDENTITY(1,1),
  ColumnName NVARCHAR(128),
  DataType NVARCHAR(100),
  IsNullable CHAR(1), -- 'Y' = NULL, 'N' = NOT NULL
  Description NVARCHAR(4000)
 );

 -- Insert Columns
 INSERT INTO @ColumnDefs (ColumnName, DataType, IsNullable, Description)
 VALUES
  ('ID', 'BIGINT IDENTITY(1,1)', 'N', 'Primary Key'),
  ('post_author', 'BIGINT', 'N', 'Author ID'),
  ('post_date', 'DATETIME2(0)', 'N', 'Post Date'),
  ('post_date_gmt', 'DATETIME2(0)', 'N', 'Post Date GMT'),
  ('post_content', 'NVARCHAR(MAX)', 'N', 'Post Content'),
  ('post_title', 'NVARCHAR(MAX)', 'N', 'Post Title'),
  ('post_excerpt', 'NVARCHAR(MAX)', 'N', 'Post Excerpt'),
  ('post_status', 'NVARCHAR(20)', 'N', 'Post Status (publish, draft, etc.)'),
  ('comment_status', 'NVARCHAR(20)', 'N', 'Comment Status (open/closed)'),
  ('ping_status', 'NVARCHAR(20)', 'N', 'Ping Status'),
  ('post_password', 'NVARCHAR(255)', 'N', 'Post Password'),
  ('post_name', 'NVARCHAR(200)', 'N', 'Post Name (Slug)'),
  ('to_ping', 'NVARCHAR(MAX)', 'N', 'URLs to ping'),
  ('pinged', 'NVARCHAR(MAX)', 'N', 'Pinged URLs'),
  ('post_modified', 'DATETIME2(0)', 'N', 'Modification Date'),
  ('post_modified_gmt', 'DATETIME2(0)', 'N', 'Modification Date GMT'),
  ('post_content_filtered', 'NVARCHAR(MAX)', 'N', 'Filtered Content'),
  ('post_parent', 'BIGINT', 'N', 'Parent Post ID'),
  ('guid', 'NVARCHAR(255)', 'N', 'Global Unique ID'),
  ('menu_order', 'INT', 'N', 'Menu Order'),
  ('post_type', 'NVARCHAR(20)', 'N', 'Post Type (post, page, etc.)'),
  ('post_mime_type', 'NVARCHAR(100)', 'N', 'Mime Type'),
  ('comment_count', 'BIGINT', 'N', 'Comment Count');
 
 -- =============================================
 -- 2.1 AUTO-DISCOVERY & IDENTITY CHECK
 -- =============================================

 -- Detect Identity Column Name in New Structure
 SELECT TOP 1 @IdentityColName = ColumnName FROM @ColumnDefs WHERE DataType LIKE '%IDENTITY%';

 IF @TargetObjID IS NOT NULL
 BEGIN
  PRINT '... Auto-detecting missing columns from existing table ...'
  INSERT INTO @ColumnDefs (ColumnName, DataType, IsNullable, Description)
  SELECT
   c.name,
   CASE
    WHEN t.name IN ('varchar', 'char', 'varbinary', 'binary') THEN
     t.name + '(' + CASE WHEN c.max_length = -1 THEN 'MAX' ELSE CAST(c.max_length AS VARCHAR) END + ')'
    WHEN t.name IN ('nvarchar', 'nchar') THEN
     t.name + '(' + CASE WHEN c.max_length = -1 THEN 'MAX' ELSE CAST(c.max_length / 2 AS VARCHAR) END + ')'
    WHEN t.name IN ('decimal', 'numeric') THEN
     t.name + '(' + CAST(c.precision AS VARCHAR) + ',' + CAST(c.scale AS VARCHAR) + ')'
    ELSE
     t.name
   END,
   CASE WHEN c.is_nullable = 1 THEN 'Y' ELSE 'N' END,
   ISNULL(CAST(ep.value AS NVARCHAR(4000)), '')
  FROM sys.columns c
  JOIN sys.types t ON c.user_type_id = t.user_type_id
  LEFT JOIN sys.extended_properties ep ON ep.major_id = c.object_id AND ep.minor_id = c.column_id AND ep.name = 'MS_Description'
  WHERE c.object_id = @TargetObjID
   AND c.is_computed = 0
   AND c.name NOT IN (SELECT ColumnName FROM @ColumnDefs)
  ORDER BY c.column_id;
  PRINT ' -> Auto-discovery complete.'
 END

 -- =============================================
 -- 3. BACKUP RELATIONS
 -- =============================================
 IF @TargetObjID IS NOT NULL
 BEGIN
  PRINT '... Backing up Relations/Indexes ...'
  -- FKs
  DECLARE FKCursor CURSOR LOCAL FAST_FORWARD FOR SELECT fk.object_id, fk.name, SCHEMA_NAME(fk.schema_id), OBJECT_NAME(fk.parent_object_id) FROM sys.foreign_keys fk WHERE fk.referenced_object_id = @TargetObjID;
  OPEN FKCursor; FETCH NEXT FROM FKCursor INTO @Cur_FKID, @Cur_FKName, @Cur_ParentSchema, @Cur_ParentTable;
  WHILE @@FETCH_STATUS = 0 BEGIN
   SELECT @Helper_FKCols = STUFF((SELECT ',' + QUOTENAME(c.name) FROM sys.foreign_key_columns fkc JOIN sys.columns c ON fkc.parent_object_id = c.object_id AND fkc.parent_column_id = c.column_id WHERE fkc.constraint_object_id = @Cur_FKID ORDER BY fkc.constraint_column_id FOR XML PATH('')), 1, 1, '');
   SELECT @Helper_RefCols = STUFF((SELECT ',' + QUOTENAME(c.name) FROM sys.foreign_key_columns fkc JOIN sys.columns c ON fkc.referenced_object_id = c.object_id AND fkc.referenced_column_id = c.column_id WHERE fkc.constraint_object_id = @Cur_FKID ORDER BY fkc.constraint_column_id FOR XML PATH('')), 1, 1, '');
   SET @SqlDropFK += 'ALTER TABLE ' + QUOTENAME(@Cur_ParentSchema) + '.' + QUOTENAME(@Cur_ParentTable) + ' DROP CONSTRAINT ' + QUOTENAME(@Cur_FKName) + ';' + CHAR(13);
   SET @SqlCreateFK += 'ALTER TABLE ' + QUOTENAME(@Cur_ParentSchema) + '.' + QUOTENAME(@Cur_ParentTable) + ' WITH NOCHECK ADD CONSTRAINT ' + QUOTENAME(@Cur_FKName) + ' FOREIGN KEY (' + @Helper_FKCols + ') REFERENCES ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName) + ' (' + @Helper_RefCols + ');' + CHAR(13);
   FETCH NEXT FROM FKCursor INTO @Cur_FKID, @Cur_FKName, @Cur_ParentSchema, @Cur_ParentTable;
  END
  CLOSE FKCursor; DEALLOCATE FKCursor;

  -- Indexes
  DECLARE IdxCursor CURSOR LOCAL FAST_FORWARD FOR SELECT index_id, name, is_primary_key, is_unique, type FROM sys.indexes WHERE object_id = @TargetObjID AND type IN (1, 2) ORDER BY is_primary_key DESC;
  OPEN IdxCursor; FETCH NEXT FROM IdxCursor INTO @Cur_IdxID, @Cur_IdxName, @Cur_IsPK, @Cur_IsUnique, @Cur_IdxType;
  WHILE @@FETCH_STATUS = 0 BEGIN
   SELECT @Helper_IdxCols = STUFF((SELECT ',' + QUOTENAME(c.name) + CASE WHEN ic.is_descending_key = 1 THEN ' DESC' ELSE ' ASC' 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 = @TargetObjID AND ic.index_id = @Cur_IdxID AND ic.is_included_column = 0 ORDER BY ic.key_ordinal FOR XML PATH('')), 1, 1, '');
   SELECT @Helper_IncCols = STUFF((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 = @TargetObjID AND ic.index_id = @Cur_IdxID AND ic.is_included_column = 1 ORDER BY ic.column_id FOR XML PATH('')), 1, 1, '');
   IF @Cur_IsPK = 1 SET @SqlCreatePK_Indexes += 'ALTER TABLE ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName) + ' ADD CONSTRAINT ' + QUOTENAME(@Cur_IdxName) + ' PRIMARY KEY ' + CASE WHEN @Cur_IdxType = 1 THEN 'CLUSTERED' ELSE 'NONCLUSTERED' END + ' (' + @Helper_IdxCols + ');' + CHAR(13);
   ELSE SET @SqlCreatePK_Indexes += 'CREATE ' + CASE WHEN @Cur_IsUnique = 1 THEN 'UNIQUE ' ELSE '' END + 'NONCLUSTERED INDEX ' + QUOTENAME(@Cur_IdxName) + ' ON ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName) + ' (' + @Helper_IdxCols + ')' + CASE WHEN LEN(@Helper_IncCols) > 0 THEN ' INCLUDE (' + @Helper_IncCols + ')' ELSE '' END + ';' + CHAR(13);
   FETCH NEXT FROM IdxCursor INTO @Cur_IdxID, @Cur_IdxName, @Cur_IsPK, @Cur_IsUnique, @Cur_IdxType;
  END
  CLOSE IdxCursor; DEALLOCATE IdxCursor;
 END

 -- =============================================
 -- 4. BUILD SCRIPTS & CHECK SHARED IDENTITY
 -- =============================================
 DECLARE ScriptCursor CURSOR LOCAL FAST_FORWARD FOR SELECT ColumnName, DataType, IsNullable FROM @ColumnDefs ORDER BY ID;
 OPEN ScriptCursor;
 FETCH NEXT FROM ScriptCursor INTO @Cur_ColName, @Cur_DataType, @Cur_IsNullable;

 WHILE @@FETCH_STATUS = 0
 BEGIN
  -- Build Create List
  IF LEN(@ColList) > 0 SET @ColList += ', ' + CHAR(13) + CHAR(10) + ' ';
  SET @ColList += QUOTENAME(@Cur_ColName) + ' ' + @Cur_DataType + CASE WHEN @Cur_IsNullable = 'Y' THEN ' NULL' ELSE ' NOT NULL' END;

  -- Build Shared Column List
  IF @TargetObjID IS NOT NULL AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = @TargetObjID AND name = @Cur_ColName)
  BEGIN
   IF LEN(@SharedColList) > 0 SET @SharedColList += ', ';
   SET @SharedColList += QUOTENAME(@Cur_ColName);

   -- [KEY FIX] If this shared column is the Identity Column, mark flag to Preserve Identity
   IF @IdentityColName IS NOT NULL AND @Cur_ColName = @IdentityColName
   BEGIN
    SET @PreserveIdentity = 1;
   END
  END

  FETCH NEXT FROM ScriptCursor INTO @Cur_ColName, @Cur_DataType, @Cur_IsNullable;
 END
 CLOSE ScriptCursor; DEALLOCATE ScriptCursor;

 -- =============================================
 -- 5. EXECUTION
 -- =============================================

 -- 5.1 Create Temp Table
 SET @CreateTableSQL = 'CREATE TABLE ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TempTableName) + ' (' + @ColList + ');';
 PRINT 'Creating Temp Table...'
 EXEC sp_executesql @CreateTableSQL;

 -- 5.2 Copy Data
 IF @TargetObjID IS NOT NULL AND LEN(@SharedColList) > 0
 BEGIN
  IF LEN(@SqlDropFK) > 0 EXEC sp_executesql @SqlDropFK;

  PRINT 'Copying data...'
  SET @CopySQL = '';

  -- Only set IDENTITY_INSERT ON if we are actually copying the Identity Column
  IF @PreserveIdentity = 1
  BEGIN
    SET @CopySQL += 'SET IDENTITY_INSERT ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TempTableName) + ' ON; ';
  END

  SET @CopySQL += 'INSERT INTO ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TempTableName) + ' (' + @SharedColList + ') ' +
      'SELECT ' + @SharedColList + ' FROM ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName) + '; ';

  IF @PreserveIdentity = 1
  BEGIN
    SET @CopySQL += 'SET IDENTITY_INSERT ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TempTableName) + ' OFF; ';
  END

  EXEC sp_executesql @CopySQL;
 END

 -- 5.3 Column Descriptions
 DECLARE DescCursor CURSOR LOCAL FAST_FORWARD FOR SELECT ColumnName, Description FROM @ColumnDefs WHERE Description IS NOT NULL AND Description <> '';
 OPEN DescCursor;
 FETCH NEXT FROM DescCursor INTO @Cur_ColName, @Cur_Desc;
 WHILE @@FETCH_STATUS = 0 BEGIN
  EXEC sp_addextendedproperty @name = N'MS_Description', @value = @Cur_Desc, @level0type = N'SCHEMA', @level0name = @SchemaName, @level1type = N'TABLE', @level1name = @TempTableName, @level2type = N'COLUMN', @level2name = @Cur_ColName;
  FETCH NEXT FROM DescCursor INTO @Cur_ColName, @Cur_Desc;
 END
 CLOSE DescCursor; DEALLOCATE DescCursor;

 -- 5.4 Table Description
 PRINT 'Adding Table Description...'
 EXEC sp_addextendedproperty @name = N'MS_Description', @value = @TableDesc, @level0type = N'SCHEMA', @level0name = @SchemaName, @level1type = N'TABLE', @level1name = @TempTableName;

 -- 5.5 Swap Tables
 IF @TargetObjID IS NOT NULL
 BEGIN
  PRINT 'Swapping tables...'
  SET @CurrentFullName = QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName);
  SET @TempFullName = QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TempTableName);

  EXEC sp_rename @CurrentFullName, @OldTableName;
  EXEC sp_rename @TempFullName, @TableName;

  SET @DropOldSQL = 'DROP TABLE ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@OldTableName) + ';';
  EXEC sp_executesql @DropOldSQL;

  IF LEN(@SqlCreatePK_Indexes) > 0 EXEC sp_executesql @SqlCreatePK_Indexes;
  IF LEN(@SqlCreateFK) > 0 EXEC sp_executesql @SqlCreateFK;
 END
 ELSE
 BEGIN
  SET @TempFullName = QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TempTableName);
  EXEC sp_rename @TempFullName, @TableName;
 END

COMMIT TRANSACTION
PRINT 'Migration Completed Successfully.'

END TRY
BEGIN CATCH
 IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
 SET @Msg = 'ERROR: ' + ERROR_MESSAGE() + ' (Line: ' + CAST(ERROR_LINE() AS NVARCHAR(10)) + ')';
 PRINT @Msg;
END CATCH;

พอเปลี่ยน table ก็เปลี่ยนชื่อ table และ column ที่มี script จะไปไล่แก้ column ตามลำดับเอง ส่วน column ที่ไม่อยู่ในลิสต์ก็จะโดนไว้ล่างสุด ถึงจะไม่ได้แก้ให้ตรง 100 % แต่ก็ดีกว่ามาเขียน SQL หรือใช้ UI ไล่แก้ที่ละจุดเยอะ