6

I am trying to execute some sqlcmd through T-SQL on SQL Server 2008. There is a part of my code where I am checking a data file size and if that data file size does not equal to 0, then start deleting the specific table so I can BCP in new data.

Below is my code that is not being executed:

SET @myVariable = '
SETLOCAL 
FOR %%R IN (X:\Main Folder\Data\'+@databaseName+'_'+@tableName+'.dat) DO SET size=%%~zR 
IF %size% NEQ 0 (
        SQLCMD -E -S my-server-name -Q "DELETE FROM '+@databaseName+'.'+@schemaName+'.'+@tableName+';" >> X:\Main Folder\Log\Log.txt 
)'

EXEC master..xp_cmdshell @myVariable

For some reason when I execute my stored procedure, the code above seems to be skipped because it does not shoot back any error messages.

EDIT: After re-adjusting the spacing and my code, @myVariable, gets executed now. However, it still does not work in regards that it still deletes the table even though the data file size = 0. However, when I hard code it within a batch file, it works perfectly fine. Any ideas?

AznDevil92
  • 554
  • 2
  • 11
  • 39

8 Answers8

3

You need to use single % in your for loop as you are not executing the code in a batch file (that requires %%), see this post for some further clarification. So your for loop should be:

FOR %R IN (X:\Main Folder\Data\'+@databaseName+'_'+@tableName+'.dat) DO SET size=%~zR 
Community
  • 1
  • 1
Alex
  • 21,273
  • 10
  • 61
  • 73
2

I can't speak to your DOS command. I can however suggest using Ole Automation Procedures to get the file size. That way you would not have to rely on running batch commands.

First you need to enable Ole Automation Procedures on your SQL Server instance, as follows:

sp_configure 'show advanced options', 1;
GO
RECONFIGURE;
GO
sp_configure 'Ole Automation Procedures', 1;
GO
RECONFIGURE;
GO

You only need to do this once.


Next is a script that gets the file size. The example assumes that there's a file called C:\Temp\testfile.txt. The script selects the size if the file exists, or selects 0 if it doesn't. You can take this script as an example to do what you want based on the size.

Here goes:

DECLARE @hr INT;
DECLARE @size INT;
DECLARE @obj_file INT;
DECLARE @obj_file_system INT;
DECLARE @file_name VARCHAR(100)='C:\Temp\testfile.txt';

-- Create a FileSystemObject. Create this once for all subsequent file manipulation. Don't forget to destroy this object once you're done with file manipulation (cf cleanup)
EXEC @hr = sp_OACreate 'Scripting.FileSystemObject', @obj_file_system OUT;
IF @hr<>0 GOTO __cleanup;

-- Get a handle for the file. Don't forget to release the handle for each file you get a handle for (see cleanup). The return will be different from 0 if the file doesn't exist
EXEC @hr = sp_OAMethod @obj_file_system, 'GetFile', @obj_file out, @file_name;
IF @hr<>0 GOTO __print_file_size;

-- Retrieve the file size.
EXEC sp_OAGetProperty @obj_file, 'size', @size OUT;

__print_file_size:
SELECT ISNULL(@size,0) AS file_size;

__cleanup:
EXEC sp_OADestroy @obj_file_system;
EXEC sp_OADestroy @obj_file;
TT.
  • 15,774
  • 6
  • 47
  • 88
2

I think the problem is that you're not using quotes around your filenames. That top level directory has a space in it.

Jaco's answer looks correct and I'm sure it was part of the problem. You should probably initialize size just to be safe too:

SET @myVariable = '
SETLOCAL
SET size=0
FOR %R IN ("X:\Main Folder\Data\'+@databaseName+'_'+@tableName+'.dat") DO SET size=%~zR 
IF %size% NEQ 0 (
        SQLCMD -E -S my-server-name -Q "DELETE FROM '+@databaseName+'.'+@schemaName+'.'+@tableName+';" >> "X:\Main Folder\Log\Log.txt" 
)'

EXEC master..xp_cmdshell @myVariable

Without the quotes the for loop is going to treat that its "set" (the terminology used in for /?) as two items separated by the space. If your current directory is X:\Main Folder\Data\ it would still work though since it sees the last one as a relative path to the .dat file and then still sets the right value on the last pass.

shawnt00
  • 16,443
  • 3
  • 17
  • 22
  • I have added quotes and double single quotes, and removed the second percent. Still no luck – AznDevil92 Jan 19 '16 at 18:40
  • Do you already have a `size` variable in your environment? Have your confirmed it's actually doing the loop and grabbing the value? – shawnt00 Jan 19 '16 at 19:14
  • Another thought: Is is possible that you don't have command extensions enabled? I believe that could cause it to fail on the `if ... neq` and then unconditionally run the delete operation. – shawnt00 Jan 20 '16 at 15:48
  • So I figured out another method that worked without having to check a data file. What I did was use SQLCMD -E -S ServerA - Q "IF(SELECT COUNT(*) FROM tableName) > 0 BEGIN DELETE FROM ServerB.TableName END" – AznDevil92 Jan 20 '16 at 16:01
2

Not sure if this is an option for you but since you are on SQL 2008 you should be able to use powershell command instead:

DECLARE @File varchar(100), @outputFile varchar(100)
DECLARE @cmd varchar(1000)
SELECT @File = 'path_to_file'
SELECT @outputFile = 'path_to_output_file'

SELECT @cmd = 'powershell.exe -command "if((Get-Item '''+@File+''').length -gt 0) {&sqlcmd.exe -E -S SERVERNAME -Q ''SELECT name FROM master.sys.databases ;'' -o '+@outputFile+'}"'


SELECT @cmd

exec master..xp_cmdshell @cmd

I've checked and it seems to be working depending on the file size.

Maaykel
  • 31
  • 4
2

Why would you go 'down' to the command line at all? (there are reasons why xp_cmdshell is disabled by default)

Could you not simply loop over your tables (sys.tables WHERE name LIKE ...) and then

  • create a shadow-copy of the table (SELECT INTO)
  • BULK INSERT from the (expected) file into shadow-table (in a TRY..CATCH to handle situations where the file does not exist, or is empty, or is corrupt, ..
  • if there is data in the shadow-table, then DELETE the actual table records and move the data over
  • if there is no data in the shadow table then DELETE the actual table records (or leave them in if you assume a missing or empty bcp file means it will arrive later on and you're stuck with the current version for now)
  • DROP the shadow table again
deroby
  • 5,902
  • 2
  • 19
  • 33
1

You are using X:\ in your code. But the code is running under the service account for SQL Server. That account may not have x: available.

I would suggest using a UNC instead of a mapped drive. Also, make sure that your service is running under a domain account, and that the domain account has all required permissions to the UNC.

Raj More
  • 47,048
  • 33
  • 131
  • 198
  • I have just tried using UNC like \\servername\X$\.... but it did not work. And I do not believe it is a permission issue since all my other xp_cmdshell commands are being executed. – AznDevil92 Jan 13 '16 at 19:04
1

I realized that I can check the table count instead of a data file size by using this method:

SET @sqlCheck = 'SQLCMD -E -S ServerA -Q "IF (SELECT COUNT(*) FROM '+@databaseName+'.'+@schemaName+'.'+@tableName+') > 0 BEGIN DELETE FROM ServerB.'+@databaseName+'.'+@schemaName+'.'+@tableName+' END;"'
                EXEC MASTER..xp_cmdshell @sqlcheck
AznDevil92
  • 554
  • 2
  • 11
  • 39
  • 1
    It's better to put this code in a stored procedure as mentioned in my comment to your question. If your code takes any sort of user input then you may open yourself upto SQL injection attacks. – Steve Ford Jan 21 '16 at 09:26
  • Why would you check if the table is empty or not before deleting? Doing the count will take a while and has no added benefit at all. Simply run the DELETE command; regardless the number of records you started with (zero or thousands), the end-result is identical: an empty table. – deroby Jan 23 '16 at 13:23
  • 1
    Also, you're creating a command-line version of some dynamic SQL and then execute that using xp_cmshell ?!? Why not simply use `EXEC ('DELETE FROM ServerB.'+@databaseName+'.'+@schemaName+'.'+@tableName)` ? – deroby Jan 23 '16 at 13:34
  • I took it that there were two tables involved on different servers and that's why the check. – shawnt00 Jan 24 '16 at 22:04
  • @deroby The reason why I am checking the tables is because there are views that I created with a WHERE clause on ServerA. If that view has 0 records selected, then it would be stupid to delete all of the data on the table located on ServerB. Therefore, I have to do the check first before deleting any records. I also did not know I can use the EXEC command and will give it a try. – AznDevil92 Jan 25 '16 at 15:17
  • 1
    @AznDevil92 still not sure why you need to use cmdshell to execute this. Having cmdshell enabled is a security risk and your dynamic sql opens you up to SQL injection attacks! If you need to access another SQL Server then why not use OPENROWSET or Linked Server! – Steve Ford Jan 26 '16 at 09:58
1

You seems to know the names of the databases and tables already, so you can use the following, which basically does a DIR for the file you're looking for and checks if it's '0 bytes', if so it then does whatever you want. Things to note:

  • STRING TEMPLATES -- When building strings, I like to build a 'template' and then replace within the string. This is a good way to make sure you have the right number of quotes, parenthesis, etc. I did it twice here, once to build the DIR command and then again to build the TRUNCATE command.

  • TRUNCATE -- although not part if your question, you may want to use a TRUNCATE instead of DELETE FROM. If you had a million rows in your table, DELETE FROM may take 2 min to run, where as TRUNCATE will always take 0-seconds to run.

Your answer:

SET NOCOUNT ON;

DECLARE @DatabaseName VARCHAR(50) = 'database1'
DECLARE @TableName VARCHAR(50) = 'table1'

DECLARE @PathTemplate VARCHAR(50) = 'dir c:\temp\{@DatabaseName}_{@TableName}.txt'

SET @PathTemplate = REPLACE(@PathTemplate, '{@DatabaseName}', @DatabaseName);
SET @PathTemplate = REPLACE(@PathTemplate, '{@TableName}', @TableName);

DECLARE @FileNames AS TABLE (FileNames VARCHAR(100))

INSERT @FileNames (FileNames)
exec xp_cmdshell @PathTemplate

IF EXISTS ( SELECT 1 FROM @FileNames WHERE FileNames LIKE '%0 bytes')
BEGIN
    PRINT 'No Content/Missing File'
END 
ELSE BEGIN

    DECLARE @SqlExc VARCHAR(500) = 'TRUNCATE TABLE [{@DatabaseName}].[dbo].[{@TableName}]'
    SET @SqlExc = REPLACE(@SqlExc, '{@DatabaseName}', @DatabaseName);
    SET @SqlExc = REPLACE(@SqlExc, '{@TableName}', @TableName);

    PRINT @SqlExc
    -- sp_executesql @SqlExc  <-- Do this in production

END
Robert Paulsen
  • 4,935
  • 3
  • 21
  • 27