T-SQL: Create Table DDL Script


A T-SQL script to export the definition of a table may be used in an automation. For example when I automate an import of a data into the database, I don’t create a staging table with NVARCHAR(MAX) columns, but export the structure of the destination table as a staging table.

Create database.

-- Create database
USE [master];
GO

IF (DB_ID('PL') IS NOT NULL)
	BEGIN
		ALTER DATABASE PL SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
		DROP DATABASE PL;
	END
GO

CREATE DATABASE PL;
GO

USE PL;
GO

-- Create schema
CREATE SCHEMA pl;
GO

-- Create test table
IF (OBJECT_ID('PL.pl.Table 1', 'U') IS NOT NULL)
	BEGIN DROP TABLE PL.pl.[Table 1] END;
GO

CREATE TABLE PL.pl.[Table 1]
	(
		-- Exact Numerics
		[BIGINT] BIGINT
		, [BIT] BIT
		, [DECIMAL] DECIMAL
		, [INT] INT
		, [MONEY] MONEY
		, [NUMERIC] NUMERIC
		, [SMALLINT] SMALLINT
		, [SMALLMONEY] SMALLMONEY
		, [TINYINT] TINYINT
		 
		-- Approximate Numerics 
		, [FLOAT] FLOAT
		, [REAL] REAL
		 
		-- Date and Time 
		, [DATE] DATE
		, [DATETIME] DATETIME
		, [DATETIME2] DATETIME2
		, [DATETIMEOFFSET] DATETIMEOFFSET
		, [SMALLDATETIME] SMALLDATETIME
		, [TIME] TIME

		-- Character Strings 
		, [CHAR] CHAR
		, [VARCHAR] VARCHAR
		, [TEXT] TEXT
		 
		-- Unicode Character Strings 
		, [NCHAR] NCHAR
		, [NVARCHAR] NVARCHAR
		, [NTEXT] NTEXT
		 
		-- Binary Strings 
		, [BINARY] BINARY
		, [VARBINARY] VARBINARY
		, [IMAGE] IMAGE
		 
		-- Other Data Types 
		--, [CURSOR ] CURSOR 
		, [GEOGRAPHY] GEOGRAPHY
		, [GEOMETRY] GEOMETRY
		, [HIERARCHYID] HIERARCHYID
		, [SQL_VARIANT] SQL_VARIANT
		--, [TABLE] TABLE
		, [SYSNAME] SYSNAME
		, [TIMESTAMP] TIMESTAMP
		, [UNIQUEIDENTIFIER] UNIQUEIDENTIFIER
		, [XML] XML
		
		-- PL test columns
		, [IDENTITY] BIGINT IDENTITY(30000, 1200)
		, [VARCHAR 25] VARCHAR(25)
		, [Computed] AS (DATEADD(DAY, -(DAY([DATETIME NOT NULL]) - 1), [DATETIME NOT NULL]))
		, [Computed PERSISTED] AS ([TINYINT] + [SMALLINT]) PERSISTED
		, [DATETIME NOT NULL] DATETIME NOT NULL
		, [INT_NOT_NULL] INT NOT NULL
		, [BIT_NOT_NULL] BIT NOT NULL
		, [CHAR_2] CHAR(2)
		, [VARCHAR_MAX] VARCHAR(MAX)
		, [NVARCHAR_MAX] NVARCHAR(MAX)
		, [VARCHAR_25_COLLATION] VARCHAR(25) COLLATE Japanese_XJIS_100_CI_AS_KS
		, [NVARCHAR_11_COLLATION] NVARCHAR(11) COLLATE Chinese_Traditional_Pinyin_100_CI_AS_KS_WS
		, [VARBINARY_MAX] VARBINARY(MAX)
		, [FLOAT_2] FLOAT(2)
		, [NUMERIC_12_8] NUMERIC(12, 8)
		, [DECIMAL_18_6] DECIMAL(18, 6)
		, [UNIQUEIDENTIFIER_NOT_NULL] UNIQUEIDENTIFIER NOT NULL
		, [MONEY_NOT_NULL] MONEY NOT NULL
		, [SMALLMONEY_NOT_NULL] SMALLMONEY NOT NULL
	)
	ON [PRIMARY];
GO

-- Insert fake data into the test table
INSERT PL.pl.[Table 1]
	(
		[BIGINT]
		, [BIT]
		, [DECIMAL]
		, [SMALLMONEY_NOT_NULL]
		, [DATETIME NOT NULL]
		, [INT_NOT_NULL]
		, [BIT_NOT_NULL]
		, [UNIQUEIDENTIFIER_NOT_NULL]
		, [MONEY_NOT_NULL]
		, [SYSNAME]
	)
SELECT
	123
	, NULL
	, 12.3
	, 123.45
	, CONVERT(CHAR(10), GETDATE(), 120)
	, 10
	, 0
	, CAST('B14BC077-F1DF-457C-9F7E-7CB9E0BC1CF3' AS UNIQUEIDENTIFIER)
	, 321.66
	, 'SYSNAME fake data 1';
GO

-- Check the test table
SELECT *
FROM PL.pl.[Table 1];
GO

Script to export table.
SET NOCOUNT ON;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- Input Variables - Start
DECLARE
	@Source NVARCHAR(392) -- Database.Schema.Table
	, @Destination NVARCHAR(392)

SELECT
	@Source = N'PL.pl.Table 1'
	, @Destination = N'myNewTable'
-- Input Variables - Start

-- Split @Source - Start
DECLARE
	@SourceDatabaseName SYSNAME
	, @SourceSchemaName SYSNAME
	, @SourceTableName SYSNAME;

SELECT
	@SourceDatabaseName = LTRIM(RTRIM(PARSENAME(@Source, 3)))
	, @SourceSchemaName = LTRIM(RTRIM(PARSENAME(@Source, 2)))
	, @SourceTableName = LTRIM(RTRIM(PARSENAME(@Source, 1)));

IF (ISNULL(@SourceDatabaseName, '') = '') BEGIN SELECT @SourceDatabaseName = DB_NAME(); END
IF (ISNULL(@SourceSchemaName, '') = '') BEGIN SELECT @SourceSchemaName = N'dbo'; END
-- Split @Source - End

-- Missing @SourceTableName - Start
IF (ISNULL(@SourceTableName, '') = '')
	BEGIN
		PRINT N'Parameter @Source is not valid --> '
			+ 'Database: ' + @SourceDatabaseName
			+ '; Schema: ' + @SourceSchemaName
			+ '; Table: ' + CASE WHEN (ISNULL(@SourceTableName, '') = '') THEN '(blank)' END;
		RETURN;
	END
-- Missing @SourceTableName - End

-- Populate @ObjectID - Start
DECLARE
	@ObjectID INT
	, @SQL_@ObjectID NVARCHAR(MAX);

SELECT @SQL_@ObjectID = N'
SELECT @ObjectIDOut = O.[object_id]
FROM
	' + QUOTENAME(@SourceDatabaseName) + N'.sys.objects AS O
	JOIN ' + QUOTENAME(@SourceDatabaseName) + N'.sys.schemas AS S
		ON O.[schema_id] = S.[Schema_id]
WHERE
	O.[type] = ''U''
	AND S.[name] = N''' + @SourceSchemaName + N'''
	AND O.[name] = N''' + @SourceTableName + N''';
';

EXEC sp_executesql
	@SQL_@ObjectID
	, N'@ObjectIDOut INT OUTPUT'
	, @ObjectIDOut = @ObjectID OUTPUT;
-- Populate @ObjectID - End

-- @ObjectID does not exist - Start
IF (ISNULL(@ObjectID, '') = '')
	BEGIN
		PRINT
			QUOTENAME(@SourceDatabaseName)
				+ CHAR(46) + QUOTENAME(@SourceSchemaName)
				+ CHAR(46) + QUOTENAME(@SourceTableName)
				+ N' doesn''t exist';
		RETURN;
	END
-- @ObjectID does not exist - End

-- Split @Destination - Start
DECLARE
	@DestinationDatabaseName SYSNAME
	, @DestinationSchemaName SYSNAME
	, @DestinationTableName SYSNAME;

SELECT
	@DestinationDatabaseName = LTRIM(RTRIM(PARSENAME(@Destination, 3)))
	, @DestinationSchemaName = LTRIM(RTRIM(PARSENAME(@Destination, 2)))
	, @DestinationTableName = LTRIM(RTRIM(PARSENAME(@Destination, 1)));

IF (ISNULL(@DestinationDatabaseName, '') = '') BEGIN SELECT @DestinationDatabaseName = DB_NAME(); END
IF (ISNULL(@DestinationSchemaName, '') = '') BEGIN SELECT @DestinationSchemaName = N'dbo'; END
-- Split @Destination - End

-- Missing @DestinationTableName - Start
IF (ISNULL(@DestinationTableName, '') = '')
	BEGIN
		PRINT N'Missing table name in parameter @Destination';
		RETURN;
	END
-- Missing @DestinationTableName - Start

-- Script Table - Start
DECLARE @SQL NVARCHAR(MAX);
DECLARE
	@SQL_Columns NVARCHAR(MAX)
	, @SQL_ColumnsOut NVARCHAR(MAX);
		
-- Delete old destination table 		
SELECT @SQL = N'-- Delete old destination table'
	+ CHAR(13) + CHAR(10)
	+ N'IF (OBJECT_ID('''
	+ QUOTENAME(@DestinationDatabaseName)
	+ CHAR(46) + QUOTENAME(@DestinationSchemaName)
	+ CHAR(46) + QUOTENAME(@DestinationTableName)
	+ N''') IS NOT NULL)'
	+ CHAR(13) + CHAR(10)
	+ N'	BEGIN'
	+ N' DROP TABLE '
	+ QUOTENAME(@DestinationDatabaseName)
	+ CHAR(46) + QUOTENAME(@DestinationSchemaName)
	+ CHAR(46) + QUOTENAME(@DestinationTableName)
	+ N';'
	+ N' END
	'
	+ CHAR(13) + CHAR(10);
			
-- CREATE TABLE
SELECT @SQL = @SQL
	+ N'-- CREATE TABLE'
	+ CHAR(13) + CHAR(10)
	+ N'CREATE TABLE '
	+ QUOTENAME(@DestinationDatabaseName)
	+ CHAR(46) + QUOTENAME(@DestinationSchemaName)
	+ CHAR(46) + QUOTENAME(@DestinationTableName)
	+ N'
(' + CHAR(13) + CHAR(10);

SELECT @SQL_Columns = N'
SELECT @ColumnsOut = 
	STUFF((
			SELECT R.R
			FROM
				(
					SELECT
						CASE
							WHEN C.column_id = 1 THEN CHAR(9) + ''''
							ELSE CHAR(9) + '', ''
						END -- [New Line Start]
						+ QUOTENAME(C.[name]) -- [Column Name]
						+ CASE
							WHEN C.[is_computed] = 1
								THEN '' AS '' + CC.[definition] + CASE WHEN CC.[is_persisted] = 1 THEN '' PERSISTED'' ELSE '''' END
							ELSE
								+ '' '' + CASE WHEN T.is_user_defined = 1 THEN QUOTENAME(SCHEMA_NAME(T.schema_id))
									+ ''.'' + QUOTENAME(T.[name]) ELSE QUOTENAME(UPPER(T.[name])) END -- [Type] TODO Chesk if there are any custom defined types and export create scripts for them.
								+ CASE
									WHEN (T.[name] IN (''decimal'', ''numeric'') AND NOT (C.[precision] = 18 AND C.[scale] = 0))
										THEN '' ('' + CAST(C.[precision] AS VARCHAR) + '', '' + CAST(C.[scale] AS VARCHAR) + '')''
									WHEN (T.[name] IN (''float'') AND C.[precision] != 53)
										THEN '' ('' + CAST(C.[precision] AS VARCHAR) + '')''
									WHEN (T.[name] IN (''char'', ''varchar'') AND C.[max_length] != 1)
										THEN '' ('' + REPLACE(CAST(C.[max_length] AS VARCHAR), ''-1'', ''MAX'') + '')''
									WHEN (T.[name] IN (''nchar'', ''nvarchar'') AND C.[max_length] != 2)
										THEN '' ('' + REPLACE(CAST(CAST(C.[max_length] AS FLOAT) / CAST(2 AS FLOAT) AS VARCHAR), ''-0.5'', ''MAX'') + '')''
									WHEN (T.[name] IN (''varbinary'') AND C.[max_length] != 1)
										THEN '' ('' + REPLACE(CAST(C.[max_length] AS VARCHAR), ''-1'', ''MAX'') + '')''
									ELSE ''''
								END -- [Precision, Scale]
								+ CASE
									WHEN (C.[collation_name] IS NOT NULL AND T.is_user_defined != 1
										/*AND C.collation_name != (SELECT CONVERT(VARCHAR, SERVERPROPERTY(''collation'')))*/
										/*Always export collation, because we do not know where (on which server with what settings) the table will be created*/
									)
										THEN '' COLLATE '' + C.[collation_name]
									ELSE ''''
								END -- [Collation]
								+ CASE WHEN C.[is_identity] = 1 THEN '' IDENTITY ('' + CAST(I.[seed_value] AS VARCHAR) + '', ''
									+ CAST(I.[increment_value] AS VARCHAR) + '')'' ELSE '''' END -- [Identity]
								+ CASE WHEN C.[is_rowguidcol] = 1 THEN '' ROWGUIDCOL'' ELSE '''' END -- [RowGuIDCol]
								+ CASE WHEN C.[is_nullable] = 1 THEN '''' ELSE '' NOT NULL'' END -- [Nullable]
						END -- [Computed]
						+ CHAR(13) + CHAR(10) -- [New Line End]
						AS R
						, C.[column_id] AS ID
					FROM
						' + QUOTENAME(@SourceDatabaseName) + '.sys.columns AS C
						LEFT JOIN ' + QUOTENAME(@SourceDatabaseName) + '.sys.types AS T
							ON C.[user_type_id] = T.[user_type_id]
						LEFT JOIN ' + QUOTENAME(@SourceDatabaseName) + '.sys.computed_columns AS CC
							ON C.[object_id] = CC.[object_id]
							AND C.[column_id] = CC.[column_id]
						LEFT JOIN ' + QUOTENAME(@SourceDatabaseName) + '.sys.identity_columns AS I
							ON C.[object_id] = I.[object_id]
							AND C.[column_id] = I.[column_id]
						--LEFT JOIN ' + QUOTENAME(@SourceDatabaseName) + '.sys.default_constraints AS DC
						--	ON C.[object_id] = DC.[parent_object_id]
						--	AND C.[column_id] = DC.[parent_column_id]
					WHERE C.[object_id] = ' + CAST(@ObjectID AS VARCHAR) + '
				) AS R
			ORDER BY ID
			FOR XML PATH(''''), ROOT(''Root''), TYPE
		).value(''/Root[1]'', ''NVARCHAR(MAX)''), 1, 0, ''''
	)
'

EXEC sp_executesql
	@SQL_Columns
	, N'@ColumnsOut NVARCHAR(MAX) OUTPUT'
	, @ColumnsOut = @SQL_ColumnsOut OUTPUT;

SELECT @SQL = @SQL + ISNULL(@SQL_ColumnsOut, '') + ');
'
-- Script Table - End

-- Show the script - Start
PRINT @SQL;
-- Show the script - Start
GO

The preliminry steps check the variables @Source and @Destination:

  • @Source – blank source table and not existing source table
  • @Destination – blank destination table

Based on @SourceSchemaName and @SourceTableName I pick @ObjectID and use it as an identifier of the scripted table.

Next I delete a destination table if it exists. A hint: manipulate the backend processes on a separate server/database/schema.

CREATE TABLE script simply joins the system tables where the information is and wraps the joined result with CASE statements.
I use a Dynamic SQL to point to the exact object (Database, Schema, Table).

Delete Database.

USE [master];
GO

IF (DB_ID('PL') IS NOT NULL)
	BEGIN
		ALTER DATABASE PL SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
		DROP DATABASE PL;
	END
GO

This script will be modified constantly in order to be as accurate as possible. Please be adviced that the responsability for your code is yours.

Any suggestions are welcomed.

Keep it simple :-)