Build and fill a SQL Server Database using SQL Compare CLI and a Batch Script
Phil Factor provides a powerful DOS batch script which, when coupled with SQL Compare CLI, allows you to build databases from source, during development, and fill them with the specific datasets required for testing.
Imagine you need to build the latest version of your database, stock it with the data you need for testing, and distribute several copies of it. By far the fastest and easiest was to do it, especially if the database is large, is to use SQL Clone. Alternatively, if you use SQL Toolbelt, you could build each of the five copies from a source directory, fill them with standard test data sets, via BCP, then use SQL Change Automation and some PowerShell (or the SQL Compare CLI and PowerShell) to deploy them.
However, there are clearly still Ops people who prefer to use DOS scripts over PowerShell, as the surprising popularity of one of my recent blogs on the topic will attest. They are familiar with it and often still maintain a vast archive of existing DOS scripts. Can one, I mused, create a DOS batch script that does the same job? No PowerShell, no SMO, just SQL Compare, SQLCMD and BCP. If you yearn for a bit of excitement and suspense, please skip over the next paragraph.
For the rest of us, I can reveal the ‘spoiler’ that the answer is ‘yes’. An article that recounts a painful failure and head-scratching might be an interesting novelty on a product learning website, but it won’t happen here. I’ll explain how and why I wrote this demonstration code.
DOS Batch Scripts
It is hard to imagine, at first glance, a less promising scripting language than DOS Batch, though it is uniquely suited to file-system tasks. Its roots go back to UNIX via CP/M and MSDOS. It is still there in MSDOS. I’ve had to use it many times over the years to perform tasks for which there were no realistic alternatives. Surprisingly, it still attracts a small but dedicated following. For an up-to-date and interesting collections of examples, see ‘Rosetta Code: Category:Batch file’ and DosTips – The DOS Batch Guide. The Rosetta Code site allows you to compare the code of different languages as they perform standard algorithms and tasks.
Aside from the warm, nostalgic feeling it gives some admins, there are occasionally times, such as shared database tenancy, or in creating databases with special datasets for testing, where a DOS-based method like this can be more convenient.
More to the point, almost every release management tool, build automation tool, deployment workflow system, configuration management or Continuous integration tool allows you to execute DOS batches. It is as close to the universal scripting language as we have, despite the pre-eminence of PowerShell on Windows.
The task of building and filling a database
During development and test, you’ll often need to create one or more copies of a database build, stocked with the specific data you need for testing. You are likely to need to maintain several datasets. For example:
- You may not be allowed access to the real data. There are plenty of good reasons for this. It may be private or confidential. It might be illegal. Therefore, you’ll need a dataset that is generated to be as close as possible in its nature and distribution to the real data, or is a masked, anonymized version of the real data.
- You may be supporting a series of tests that require a standardized data set that is used every time to enable the testers to do black-box testing.
- You could be doing scalability tests to make sure that the design of database performs well with different amounts of data.
We need an automated way to build each of the working database copies. As discussed, while there are easier and faster ways to do it, we’re going to use a DOS batch file. It will build a database from a source directory, using SQL Compare, and then fill it with data from an existing directory of BCP Files. To do this, you provide the name of a root directory and it stores the data under subdirectories representing the server and database. It creates these directories as necessary. The DOS script contacts the newly built database, gets a list of tables, and BCPs the data from the source directory to the target.
Having created the initial working copy, we can then create subsequent copies by using it as a source database. The DOS batch script will check the destination and delete any existing database of the same name, before creating an empty target database. It then uses SQL Compare to synchronize the schema of the empty target with the source, and then finally copies over all the data from the source database.
There are, along with many seemingly simple tasks, certain complications. Target servers need to be appropriately set up with the right extensions, such as full-text search, to meet the requirements of the database. A database may have requirements that require special filters or switches. With SQL Compare, we can use ArgFiles to refine the script to allow this. I show a way of slipping in configurable parameters in BCP because I need to have a device like this to allow for ID/Password authentication.
If tables or databases have illegal names with spaces in them, this must be dealt with. SQL Server instance names too require modification, if you use them as directory names when storing files.
One of the more awkward tasks is that of reading any required IDs and passwords from disk. It is horrible to keep these in a script. You must, at least, keep them in files in your user area where NTFS security should keep them away from prying eyes. In an ideal world, every application would accept and read a standard connection string, but sadly every CLI application seems to have a different way of doing this, so the credentials must be parsed and correctly laid out for the command line.
Running the Code
For the purposes of the article, the script has been laid out stream-of-consciousness-style to make it easier to follow, though it is easy to make it into a function if you have a lot of jobs to do.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
Echo off VERIFY errors 2>nul SETLOCAL ENABLEDELAYEDEXPANSION SETLOCAL ENABLEEXTENSIONS Set outcome=did our best if ERRORLEVEL 1 ( echo could not set local execution environment goto bombsite ) REM set output to yes or no depending on whether you want the source to have its data copied out Set output=yes REM set input to yes or no depending on whether you want the target to have its data copied in Set input=yes Rem set Source to the database you wish to copy Set Source=MySourceDatabase Rem Set SourceServer to the name of the server or instance to copy from Set Sourceserver=MySourceServer Rem set Target to the database you wish to copy to Set Target=TestCopy Rem BEWARE!! It deletes the existing copy of the database Rem Set TargetServer to the name of the server or instance to copy to Set TargetServer=MyDestinationServer REM Specify your work directory. I chose 'BatchBCP' in my user area Set workpath=%userProfile%\BatchBCP Rem Specify a scripts directory for the source if you want one. otherwise put 'none' Set SourceScriptsDirectory=c:\MySQLSourceDirectories\MyDatabase REM before you start, if you need to write out your SQLCMD credentials to a file in your user rem area using code like this, ONLY if you use SQL Server Credentials. Rem echo MyUserID/MyPassword>%userProfile%\%TargetServer:\=_%SQLCompare.txt rem echo MyOtherUserID/MyOtherPassword>%userProfile%\%SourceServer:\=_%SQLCompare.txt Rem if ERRORLEVEL 1 ( Rem echo Could not write Source Credentials REM goto bombsite REM ) rem REM read in your Source SQLCMD command and credentials if you have any if exist %userProfile%\%SourceServer:\=_%SQLCompare.txt ( Set /p SourceCredentials=<%userProfile%\%SourceServer:\=_%SQLCompare.txt ) if ERRORLEVEL 1 ( echo Could not read in Source Credentials goto bombsite ) Rem Parse the source credentials into two variables set "Sourceuid=%SourceCredentials:/=" & set "SourcePw=%" Rem credentials are presented in two different ways by the CLI apps Set SourcesqlcmdCredentials= /U %Sourceuid% /P %SourcePw% Set SourceSQLCompareCredentials= /username1:%Sourceuid% /Password1:%SourcePw% rem set source credentials correctly for windows security IF NOT DEFINED SourceCredentials ( Set SourcesqlcmdCredentials = set SourceSQLCompareCredentials = ) REM read in your Target SQLCMD command and credentials if you have any if exist %userProfile%\%TargetServer:\=_%SQLCompare.txt ( Set /p TargetCredentials=<%userProfile%\%TargetServer:\=_%SQLCompare.txt ) if ERRORLEVEL 1 ( echo Could not read in Target Credentials goto bombsite ) Rem Parse the target credentials into two variables set "Targetuid=%TargetCredentials:/=" & set "TargetPw=%" Rem credentials are presented in two different ways by the CLI apps Set TargetsqlcmdCredentials= /U %Targetuid% /P %TargetPw% Set TargetSQLCompareCredentials= /username2:%Targetuid% /Password2:%TargetPw% rem set target credentials orrectly for windows security IF NOT DEFINED TargetCredentials ( Set TargetsqlcmdCredentials = set TargetSQLCompareCredentials = ) Rem Now we check the Target database on the target server to see if it Rem already exists, If so, we delete it and create an empty database Set QUERY= IF EXISTS (SELECT name FROM sys.databases where NAME LIKE 'testcopy' ) DROP DATABASE TestCopy; CREATE DATABASE TestCopy sqlcmd -S %targetServer% %TargetsqlcmdCredentials% -d master -h -1 -f 65001 -Q "%QUERY%" if ERRORLEVEL 1 ( echo Failed to use target %targetServer% to create %target%. goto bombsite ) Rem now we synchronize the source with the target to provide a fresh Rem new database at the right level if %SourceScriptsDirectory% == none ( echo synchronizing database %Source% on %sourceServer% with %Target% on %TargetServer% "%ProgramFiles(x86)%\Red Gate\SQL Compare 13\sqlcompare.exe" /server1:%sourceServer% /database1:%Source% %SourceSQLCompareCredentials% /server2:%targetServer% /database2:%Target% %TargetSQLCompareCredentials% /Synchronize SET outcome=created database %Target% on %TargetServer% from %Source% on %sourceServer% ) else ( echo synchronizing scripts directory %SourceScriptsDirectory% with %Target% on %TargetServer% "%ProgramFiles(x86)%\Red Gate\SQL Compare 13\sqlcompare.exe" /scripts1:%SourceScriptsDirectory% /server2:%targetServer% /database2:%Target% %TargetSQLCompareCredentials% /Synchronize SET outcome=created database %Target% on %TargetServer% from scripts ) if ERRORLEVEL 1 ( echo An error with SQL Compare occurred. goto bombsite ) REM see the output onscreen while debugging. REM check whether the database directory within the workpath/server directory exists if not exist "%workpath%\%SourceServer:\=_%\%Source%" (md %workpath%\%SourceServer:\=_%\%Source%) if ERRORLEVEL 1 ( echo An error creating "%workpath%\%SourceServer:\=_%\%Source%" for database occurred goto bombsite ) Echo Copying table data from %SourceServer%.%Source% to "%workpath%\%SourceServer:\=_%\%Source% " Rem Create the query that brings you the list of tables. This is used for both rem the input and output operations. We can cope with tables that use illegal characters Set QUERY="SET NOCOUNT ON; SELECT replace(Quotename(Object_Schema_Name(object_id))+'.'+Quotename(name),' ','--') AS The_Tables FROM sys.tables WHERE is_ms_shipped=0;" Rem only do the next block if the user wants data copied out from the source if %output% == yes ( REM Execute the query and work through the returned list of tables for /F usebackq %%i in (`sqlcmd -S %sourceServer% %SourcesqlcmdCredentials% -d %Source% -h -1 -f 65001 -Q %QUERY%`) do ( rem for every tablespec in the list append the following text .... REM catch the first error caused by the sqlcmd if ERRORLEVEL 1 ( echo An error ovccured while accessing %SourceServer% to get the list of tables goto bombsite ) Set Tablename= %%i for /f "tokens=* delims= " %%a in ("!Tablename!") do set Tablename=%%a Rem correct the name of the table Set Tablename=!Tablename:--= ! REM change a dot for a dash as it isn't legal set filename= !Tablename:.=-! REM trim the filename- well, remove all spaces Set "filename=!filename: =!" echo Copying out data from !Tablename! in database %Source% on server %SourceServer% to !filename!.bcp BCP "!Tablename!" out %workpath%\%SourceServer:\=_%\%Source%\!filename!.bcp -n -d %Source% -S %sourceServer% %SourcesqlcmdCredentials% if ERRORLEVEL 1 ( echo BCP error when copying out data from !Tablename! in database %Source% on server %SourceServer% goto bombsite ) ) SET outcome=%outcome%, copied out data from %Source% on %SourceServer% ) Rem only do the next block if the user wants data copied out to the target if %Input% == yes ( Echo Copying table data to %TargetServer%.%Target% from "%workpath%\%SourceServer:\=_%\%Source%" REM Execute the query and create the entire SQL Command file that will be executed for /F usebackq %%i in (`sqlcmd -S %TargetServer% %TargetsqlcmdCredentials% -d %Target% -h -1 -f 65001 -Q %QUERY%`) do ( rem for every tablespec in the list append the following text .... REM catch the first error caused by the sqlcmd if ERRORLEVEL 1 ( echo An error occured while accessing %TargetServer% to get the list of tables goto bombsite ) Set Tablename= %%i for /f "tokens=* delims= " %%a in ("!Tablename!") do set Tablename=%%a Rem correct the name of the table Set Tablename=!Tablename:--= ! REM change a dot for a dash as it isn't legal set filename= !Tablename:.=-! REM trim the filename- well, remove all spaces Set "filename=!filename: =!" echo Copying in data to !Tablename! in database %Target% on server %TargetServer% from %workpath%\%SourceServer:\=_%\%Source%\!filename!.bcp BCP "!Tablename!" in %workpath%\%SourceServer:\=_%\%Source%\!filename!.bcp -n -d %Target% -E -S %TargetServer% %TargetsqlcmdCredentials% if ERRORLEVEL 1 ( echo BCP error when copying in data from !Tablename! in database %target% on server %TargetServer% goto bombsite ) ) SET outcome=%outcome%, copied out data from %Source% on %SourceServer% ) goto end :bombsite Rem This is where the program goes if a problem is detected color 04 Echo Sadly, we failed, though we %outcome%! echo on ENDLOCAL color Exit /b 1 :end REM This is where we go if all ended well. Echo we %outcome% and finished successfully ENDLOCAL echo on Exit /b 0 |
Testing it all out
Running this is pretty simple. What you do depends on the task. You can, of course, set up different versions of the batch file for each task, or go for the lower-maintenance option of remembering the configuration options that are commented in the source.
1. Create target database from source directory without data
At the start of the script, Fill in the path to the source directory as the value of the variable SourceScriptsDirectory, add the name of the target database to Target and the Target server to TargetServer, and the specify in the workpath file location the path to the directory containing the native BCP data files. Set output to no and set input to no (meaning don’t copy data from the source or into the target).
2. Create target database from source directory, fill with data from BCP directory
Fill in, at the start of the script, the path to the source directory, in the variable SourceScriptsDirectory, add the name of the target database to Target and the target server to TargetServer. Specify in the workpath file location, the path to the directory containing the native BCP data files. Set output to no and set input to yes (meaning don’t copy data from the source but copy it into the target)
3. Create target database from source directory, fill with data from source database
Fill in, at the start of the script, the path to the source directory in the variable SourceScriptsDirectory, add the name of the target database to Target and the target server to TargetServer. Specify in the workpath file location, the path to the directory containing the native BCP data files. Set output to yes and set input to yes (meaning copy data from the source and copy it into the target).
4. Synch a target database with a source database, BCP in data
You merely fill in, to the start of the script, the name of the source database and server, the target database and server, and the workpath file location to put the native BCP data files. Set output to yes and set input to yes.
5. Synch a target database with a source database, without data
As before, you fill in, to the start of the script, the name of the source database and server, the target database and server, and the workpath location. Set output to no and set input to no.
Handling login credentials
Unless you want to use your Windows login for the operation, you also need to write your SQL Server credentials into a file, one for each servers that you use to the root of your user area as I’ve indicated in the source code. This only must be executed only once, and then you should delete the code! I’ve provided the source code to do this:
1 2 3 4 5 6 7 8 9 |
REM before you start, if you need to write out your SQLCMD credentials to a file in your user rem area using code like this, ONLY if you use SQL Server Credentials. Rem echo MyUserID/MyPassword>%userProfile%\%TargetServer:\=_%SQLCompare.txt rem echo MyOtherUserID/MyOtherPassword>%userProfile%\%SourceServer:\=_%SQLCompare.txt Rem if ERRORLEVEL 1 ( Rem echo Could not write Source Credentials REM goto bombsite REM ) rem |
Just delete the REM
keywords, add your UserIDs and credentials and all should be well. Don’t forget to delete the code once you’ve executed it.
Running the DOS batch script
Open a command prompt and type in the name of the batch file, including the path, and off it goes. Such is the nature of DOS files that things can easily go awry but there shouldn’t be too many problems with this code. To debug, first delete the @echo
off the first line so you can see how the batch ran. There are more hints here.
Here is the typical output, execution just completed:
When the script has run, and you have included the BCP OUT operation (output=true
) you should see the files in their directory here:
If you fire up SQL Data Compare, it will tell you that the data in both copies of the database are the same. And SQL Server will say the same about the metadata.
Conclusions
The joy of applications that can run in DOS as CLI applications is that you can choose from a wide range of scripting languages and methods. Although I like PowerShell, there are plenty of people around, especially in Ops, who actually prefer the DOS batch language because it is so near to the operating system, it is used on a large library of existing scripts, and it runs without needing a special scripting environment.
SQLCMD is such a powerful system that you can do a lot of things with it easily that take effort in any other way. Even the database developer can find DOS to be a convenient way of slinging scripts together with the help of a library of commonly used snippets. If you combine this with database tools with a CLI interface, such as SQL Compare, SQL Data Compare, or SQL Data Generator, and you have a pretty powerful toolkit for creating your latest development builds and filling them with the specific datasets you need.