Run SQL script file from powershell - powershell

I am trying to run queries stored in a text file from PowerShell. I use following to do that;
Invoke-Expression 'sqlcmd -d TestDB -U $user -P $pw -i "E:\SQLQuery1.sql"'
If an error or exception occurs when executing the queries from the .sql file, how can I capture that in my Powershell script? How can I get the script output?
NOTE: I cannot use invoke-sqlcmd

To answer the question
If some error or exception occurred when executing .sql file how can I get that into my PowerShell script? How can I get the script output?"
Invoke-Expression returns the output of the expression executed. However, it may only capture STDOUT, not STDERR (I haven't tested, as I don't use this method), so you might not get the actual error message.
From the Help:
The Invoke-Expression cmdlet evaluates or runs a specified string as a command and returns the results of the expression or command
The better route is to use the PowerShell method you already have available - Invoke-SQLCmd is installed if you have installed any of the SQL Server 2008 (or newer) components/tools (like SSMS). If you've got SQL Server 2012, it's very easy: import-module sqlps. For 2008, you need to add a Snap-In, add-pssnapin SqlServerCmdletSnapin. And since you have sqlcmd.exe, the PowerShell components should be there already.
If all else fails, go the System.Data.SQLClient route:
$Conn=New-Object System.Data.SQLClient.SQLConnection "Server=YOURSERVER;Database=TestDB;User Id=$user;password=$pw";
$Conn.Open();
$DataCmd = New-Object System.Data.SqlClient.SqlCommand;
$MyQuery = get-content "e:\SQLQuery1.sql";
$DataCmd.CommandText = $MyQuery;
$DataCmd.Connection = $Conn;
$DAadapter = New-Object System.Data.SqlClient.SqlDataAdapter;
$DAadapter.SelectCommand = $DataCmd;
$DTable = New-Object System.Data.DataTable;
$DAadapter.Fill($DTable)|Out-Null;
$Conn.Close();
$Conn.Dispose();
$DTable;
With both this and Invoke-SQLCmd, you'll be able to use try/catch to pick up and handle any error that occurs.

As seen in this question's answers, there is a method built into Powershell to invoke SQLCMD called, unsurprisingly, Invoke-Sqlcmd.
It's very easy to use for individual files:
Invoke-sqlcmd -ServerInstance $server -Database $db -InputFile $filename
Or groups:
$listOfFiles | % { Invoke-sqlcmd -ServerInstance $server -Database $db -InputFile $_ }

Use invoke-sqlquery module, available at this website.

Related

Azure Pipelines - logging commands from SQL script

I am trying to log some messages from a TSQL script running via Azure Pipelines, for instance, before creating a table we check if table already exists and if so we simply print a message and skip table creation...
there are good articles explaining how to access Azure Pipelines Logging Commands from BASH or PowerShell, for instance this article: https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash
but how to output messages to the pipeline logs from within TSQL statement itself?
I will try with RAISERROR ( e.g. RAISERROR('Table [dbo].[ReportHistory] already exists!', 0, 1) WITH NOWAIT; ) hopefully works better than PRINT command, has anyone had similar issue and how did he resolve it?
You can run your scripts through PowerShell Invoke-Sqlcmd with -Verbose key. Here is small example for PowerShell task:
$server = "$(servername)"
$dbname = "$(dbname)"
$u = "$(username)"
$p = "$(password)"
$filename = "testfile.sql"
$filecontent = "RAISERROR('Table [dbo].[ReportHistory] already exists!', 0, 1) WITH NOWAIT;`r`nGO`r`n"
Set-Content -Path $filename -Value $filecontent
Write-Host '##[command] Executing file... ', $filename
#Execution of SQL packet
try
{
Invoke-Sqlcmd -InputFile "$filename" -ServerInstance $server -Database $dbname -Username "$u" -Password "$p" -QueryTimeout 36000 -Verbose
}
catch
{
Write-Host "##[error]" $Error[0]
Write-Host "##[error]----------\n"
}
Result:
You can use Azure Pipelines Logging Commands together with PRINT and RAISERROR commands.
The logging commands syntax ##vso[task..] is the reserved keywords in Azure devops piplines. When ##vso[task..] is found in the tasks' output stream, Azure devops pipeline will execute the logging commands.
So that you can output messages to the pipeline logs from within TSQL statement using logging commands with PRINT or RAISERROR. See below example:
PRINT N'##vso[task.logissue type=warning]Table already exists.';
RAISERROR('##vso[task.logissue type=warning]Table [dbo].[ReportHistory] already exists!',0,1);
See below output messages in pipeline log:

SSAS Deployment Via Powershell Corrupting Format String

I am having a problem when deploying cubes via PowerShell that I don’t know how to fix. In my solution I have some measures with FormatString: \£ #,##0;-\£ #,##0. If I deploy them normally via Visual Studio, the XMLA I pull from the SSAS server after deployment will have <FormatString>\£ #,##0;-\£ #,##0</FormatString> which is all good and expected. However, if I deploy via PowerShell, the resulting XMLA from the server is <FormatString>\£ #,##0;-\£ #,##0</FormatString>.
I realise the issue is with £ being parsed, but I am not sure how to resolve it.
The bit of background here is that I am using Octopus Deploy, and I've extracted the PowerShell below where the issue occurs:
$modelpath = "C:\...\Project_Name.xmla"
$Path_To_Microsoft_AnalysisServices_Deployment = "C:\Program Files (x86)\Microsoft SQL Server\140\Tools\Binn\ManagementStudio\Microsoft.AnalysisServices.Deployment.exe"
$SSAS_Project_Name = "Project_Name"
$Path_To_SSAS_Bin_Folder = "F:\Octopus Deploy\CustomInstallation\Project_Name\Content"
$FilePath_asdatabase = "$Path_To_SSAS_Bin_Folder\$SSAS_Project_Name.asdatabase"
$FilePath_deploymenttargets = "$Path_To_SSAS_Bin_Folder\$SSAS_Project_Name.deploymenttargets"
$FilePath_configsettings = "$Path_To_SSAS_Bin_Folder\$SSAS_Project_Name.configsettings"
$FilePath_deploymentoptions = "$Path_To_SSAS_Bin_Folder\$SSAS_Project_Name.deploymentoptions"
Start-Process -FilePath $Path_To_Microsoft_AnalysisServices_Deployment -ArgumentList "`"$FilePath_asdatabase`"","/s:`"$FilePath_DeploymentLog`"","/o:`"$SSAS_Project_Name.xmla`"","/d" -Wait -NoNewWindow
# Load the Adomd Client
[System.Reflection.Assembly]::LoadWithPartialName(“Microsoft.AnalysisServices.AdomdClient”) | Out-Null;
# Connect to the server
Write-Host "Executing Deployment Script"
$serverName = “Data Source=.”
$conn = New-Object Microsoft.AnalysisServices.AdomdClient.AdomdConnection $serverName
$conn.Open()
# Read XMLA
$xmla = Get-Content $modelpath
# Execute XMLA
$cmd = New-Object Microsoft.AnalysisServices.AdomdClient.AdomdCommand $xmla, $conn
return $cmd.ExecuteNonQuery();
I have done some investigation and I’ve found the following:
The xmla file produced by PowerShell (not in the snippet) has the correct Format String \£ #,##0;-\£ #,##0
The .asdatabase file also has the correct format string
I suspect that something is happening either at:
Start-Process -FilePath $Path_To_Microsoft_AnalysisServices_Deployment -ArgumentList “`”$FilePath_asdatabase`””,”/s:`”$FilePath_DeploymentLog`””,”/o:`”$SSAS_Project_Name.xmla`””,”/d” -Wait -NoNewWindow
or further down at:
# Execute XMLA
$cmd = New-Object Microsoft.AnalysisServices.AdomdClient.AdomdCommand $xmla, $conn
return $cmd.ExecuteNonQuery();
Any help would be appreciated.
PS: I’ve tried playing with the Format String when setting it in the solution but haven’t been able to find a solution. Note that I can’t simple use “Currency” b/c the server is set to English (US).
The answer to this is to specify the encoding. From Jeroen in the comment in response to my question:
It looks like PowerShell is auto-detecting the encoding of the XMLA incorrectly as Windows-1252, when it's actually UTF-8 (without a BOM). Try $xmla = Get-Content $modelpath -Encoding UTF8. It may also be the case that the XMLA itself is actually incorrect -- make sure to inspect it in an editor capable of reporting the encoding, like Notepad++, not just have it echo back by PowerShell itself. – Jeroen Mostert 15 mins ago

Powershell - Copying File to Remote Host and Executing Install exe using WMI

EDITED: Here is my code now. The install file does copy to the remote host. However, the WMI portion does not install the .exe file, and no errors are returned. Perhaps this is a syntax error with WMI? Is there a way to just run the installer silently with PsExec? Thanks again for all the help sorry for the confusion:
#declare params
param (
[string]$finalCountdownPath = "",
[string]$slashes = "\\",
[string]$pathOnRemoteHost = "c:\temp\",
[string]$targetJavaComputer = "",
[string]$compname = "",
[string]$tempPathTarget = "\C$\temp\"
)
# user enters target host/computer
$targetJavaComputer = Read-Host "Enter the name of the computer on which you wish to install Java:"
[string]$compname = $slashes + $targetJavaComputer
[string]$finalCountdownPath = $compname + $tempPathTarget
#[string]$tempPathTarget2 =
#[string]$finalCountdownPath2 = $compname + $
# say copy install media to remote host
echo "Copying install file and running installer silently please wait..."
# create temp dir if does not exist, if exist copy install media
# if does not exist create dir, copy dummy file, copy install media
# either case will execute install of .exe via WMII
#[string]$finalCountdownPath = $compname + $tempPathTarget;
if ((Test-Path -Path $finalCountdownPath) )
{
copy c:\hdatools\java\jre-7u60-windows-i586.exe $finalCountdownPath
([WMICLASS]"\\$targetJavaComputer\ROOT\CIMV2:win32_process").Create("cmd.exe /c c:\temp\java\jre-7u60-windows-i586.exe /s /v`" /qn")
}
else {
New-Item -Path $finalCountdownPath -type directory -Force
copy c:\hdatools\dummy.txt $finalCountdownPath
copy "c:\hdatools\java\jre-7u60-windows-i586.exe" $finalCountdownPath
([WMICLASS]"\\$targetJavaComputer\ROOT\CIMV2:win32_process").Create("cmd.exe /c c:\temp\java\jre-7u60-windows-i586.exe /s /v`" /qn")
}
I was trying to get $Job = Invoke-Command -Session $Session -Scriptblock $Script to allow me to copy files on a different server, because I needed to off load it from the server it was running from. I was using the PowerShell Copy-Item to do it. But the running PowerShell script waits until the file is done copying to return.
I want it to take as little resources as possible on the server that the powershell is running to spawn off the process on another server to copy the file. I tried to user various other schemes out there, but they didn't work or the way I needed them to work. (Seemed kind of kludgey or too complex to me.) Maybe some of them could have worked? But I found a solution that I like that works best for me, which is pretty easy. (Except for some of the back end configuration that may be needed if it is is not already setup.)
Background:
I am running a SQLServer Job which invokes Powershell to run a script which backups databases, copies backup files, and deletes older backup files, with parameters passed into it. Our server is configured to allow PowerShell to run and under the pre-setup User account with SQL Server Admin and dbo privileges in an Active Directory account to allow it to see various places on our Network as well.
But we don't want it to take the resources away from the main server. The PowerShell script that was to be run would backup the database Log file and then use the another server to asynchronously copy the file itself and not make the SQL Server Job/PowerShell wait for it. We wanted it to happen right after the backup.
Here is my new way, using WMI, using Windows Integrate Security:
$ComputerName = "kithhelpdesk"
([Wmiclass]'Win32_Process').GetMethodParameters('Create')
Invoke-WmiMethod -ComputerName RemoteServerToRunOn -Path win32_process -Name create -ArgumentList 'powershell.exe -Command "Copy-Item -Path \\YourShareSource\SQLBackup\YourDatabase_2018-08-07_11-45.log.bak -Destination \\YourShareDestination\YourDatabase_2018-08-07_11-45.log.bak"'
Here is my new way using passed in Credentials, and building arg list variable:
$Username = "YouDomain\YourDomainUser"
$Password = "P#ssw0rd27"
$ComputerName = "RemoteServerToRunOn"
$FromFile = "\\YourShareSource\SQLBackup\YourDatabase_2018-08-07_11-45.log.bak"
$ToFile = "\\YourShareDestination\SQLBackup\YourDatabase_2018-08-07_11-45.log.bak"
$ArgumentList = 'powershell.exe -Command "Copy-Item -Path ' + $FromFile + ' -Destination ' + $ToFile + '"'
$SecurePassWord = ConvertTo-SecureString -AsPlainText $Password -Force
$Cred = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $Username, $SecurePassWord
([Wmiclass]'Win32_Process').GetMethodParameters('Create')
Invoke-WmiMethod -ComputerName $ComputerName -Path win32_process -Name create -ArgumentList $ArgumentList -Credential $Cred
We think that this above one is the preferred one to use.
You can also run a specific powershell that will do what you want it to do (even passing in parameters to it):
Invoke-WmiMethod -ComputerName RemoteServerToRunOn -Path win32_process -Name create -ArgumentList 'powershell.exe -file "C:\PS\Test1.ps1"'
This example could be changed to pass in parameters to the Test1.ps1 PowerShell script to make it more flexible and reusable. And you may also want to pass in a Credential like we used in a previous example above.
Help configuring WMI:
I got the main gist of this working from: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/invoke-wmimethod?view=powershell-5.1
But it may have also needed WMI configuration using:
https://helpcenter.gsx.com/hc/en-us/articles/202447926-How-to-Configure-Windows-Remote-PowerShell-Access-for-Non-Privileged-User-Accounts?flash_digest=bec1f6a29327161f08e1f2db77e64856b433cb5a
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/enable-psremoting?view=powershell-5.1
Powershell New-PSSession Access Denied - Administrator Account
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/invoke-wmimethod?view=powershell-5.1 (I used to get how to call Invoke-WmiMethod).
https://learn.microsoft.com/en-us/powershell/scripting/core-powershell/console/powershell.exe-command-line-help?view=powershell-6 (I used to get syntax of command line)
I didn't use this one, but could have: How to execute a command in a remote computer?
I don't know for sure if all of the steps in the web articles above are needed, I suspect not. But I thought I was going to be using the Invoke-Command PowerShell statement to copy the files on a remote server, but left my changes from the articles above that I did intact mostly I believe.
You will need a dedicated User setup in Active Directory, and to configure the user accounts that SQL Server and SQL Server Agent are running under to give the main calling PowerShell the privileges needed to access the network and other things to, and can be used to run the PowerShell on the remote server as well. And you may need to configure SQLServer to allow SQL Server Jobs or Stored Procedures to be able to call PowerShell scripts like I did. But this is outside the scope of this post. You Google other places on the internet to show you how to do that.

How to connect to SQL Server LocalDB using Invoke-Sqlcmd?

I have sqlcmd.exe from both SQLServer 2008 and SQLServer 2012:
PS C:\> Get-Command sqlcmd.exe
Definition
----------
C:\Program Files\Microsoft SQL Server\100\Tools\Binn\SQLCMD.EXE
C:\Program Files\Microsoft SQL Server\110\Tools\Binn\SQLCMD.EXE
By modifying $env:PATH i force the use of sqlcmd.exe from SQL Server 2012:
PS C:\> $env:PATH = ($env:PATH -split ";" | Where-Object { $_ -notlike "*\Microsoft SQL Server\100\*" }) -join ";"
PS C:\> Get-Command sqlcmd.exe
Definition
----------
C:\Program Files\Microsoft SQL Server\110\Tools\Binn\SQLCMD.EXE
The default instance of LocalDB is up and running, and owned by the current user:
PS C:\> sqllocaldb i v11.0
Name: v11.0
Version: 11.0.2318.0
Shared name:
Owner: DOMAIN\me
Auto-create: Yes
State: Running
Last start time: 12/06/13 18:17:57
Instance pipe name: np:\\.\pipe\LOCALDB#08EDBEF0\tsql\query
Now, i can execute command on (localdb)\v11.0 using sqlcmd.exe
PS C:\> sqlcmd.exe -S "(localdb)\v11.0" -Q "select 1"
-----------
1
But when trying the same with Invoke-Sqlcmd i get a connection error:
PS C:\> Import-Module sqlps
PS C:\> Invoke-Sqlcmd -ServerInstance "(localdb)\v11.0" -Query "select 1"
Invoke-Sqlcmd : A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: SQL Network Interfaces, error: 26 - Error Locating Server/Instance Specified)
What can i do to lmake Invoke-Sqlcmd connect to (localdb)\v11.0 ?
UPDATE
Invoke-Sqlcmd from the SqlServer module supports LocalDB:
Invoke-Sqlcmd -ServerInstance "(localdb)\v11.0" -Query "select 1"
Invoke-Sqlcmd -ConnectionString "Server=(localdb)\v11.0; Integrated Security=True" -Query "select 1"
# both will work
From what I know sqlcmd.exe uses a connection string that has all the information, whereas Invoke-Sqlcmd breaks that information down into different parameters. So you likely need to split (localdb) and v11.0 across ServerInstance, Database, and ComputerName. Or you can use the ConnectionString parameter instead.
Now, I am not familiar with your use of localdb so there might be something funky with the way it handles that...or something.
Got this from a couple other sources, seems to work so far.
JBs Powershell
and
How can I run PowerShell with the .NET 4 runtime?
Another way of making PowerShell and LocalDB play nice is to make PowerShell aware of DOTNET 4.0.3. This can be done by creating a file called "powershell.exe.config" in the C:\Windows\System32\WindowsPowerShell\v1.0 . The file should contain the following:
<?xml version="1.0"?>
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0.30319"/>
<supportedRuntime version="v2.0.50727"/>
</startup>
</configuration>
Be aware that this not an officially supported way of using PowerShell, so it might break other stuff ...
I have been doing this at work recently and had some initial troubles connecting to a local Database. To get it to work, I ran the following code;
C:\> Import-Module sqlps -DisableNameChecking
SQLSERVER\:> cd ".\SQL\$(hostname)"
SQLSERVER\:> Invoke-Sqlcmd -Username "user" -Password "pass" -Database "databasename" -Query "foobar"
This worked for me and I was able to query the database. Obviously, change the Username, Password and Database parameter details to whatever the name of your database on the SQL Instance is called.
This is code that works for me under adverse conditions (see my comments just after the code). I suspect that simpler code may work in a more common environment, but I haven't dug into it.
The instance pipe times out after a few minutes. You're better off if you can connect using (localdb)\instanceName, because those connections don't seem to time out.
function Get-InstancePipeName ([string] $localDbName)
{
while (!($pipeName = ((sqllocaldb info $localDbName) -match 'instance pipe name').Split(':', 2)[1].Trim()))
{
sqllocaldb start $localDbName | Out-Null
}
return $pipeName
}
$scsb = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
$scsb.psbase.DataSource = Get-InstancePipeName localDbName # <== put your db name here
$sc = New-Object System.Data.SqlClient.SqlConnection $scsb.ConnectionString
$smoSc = New-Object Microsoft.SqlServer.Management.Common.ServerConnection $sc
$smoSvr = New-Object Microsoft.SqlServer.Management.Smo.Server $smoSc
Invoke-Sqlcmd -ServerInstance $smoSvr -Query 'select 1'
For reasons currently outside my control, the execution environment where this runs is unusual. It's a remote execution environment with an incomplete session context. Also, I had to redefine USERPROFILE to work around some other issues.
[later edit: I recently found a way to extend the timeout - I had to add a RECONFIGURE after the 2nd sp_configure and (as recommended) stop and start the localdb to get it to take effect)]
I'm guessing that invoke-sqlcmd doesn't know what "(localdb)" is. Try using localhost instead.

Powershell Invoke-sqlcmd keeping connection open and trying to reuse it

I have a Powershell script that uses invoke-sqlcmd to apply scripts to a series of development databases. I loop through a list of scripts and compare it to the current release level of the database and then apply the required scripts to get the DB to the release level it needs to be at. Certain databases are reference databases and are in a READ_ONLY state. I connect to those database run an alter DB script setting them to READ_WRITE apply the script then change the back to READ_ONLY. Overall the script works well, the issue is it looks like when PowerShell first opens a connection to the database and applies the first script and then goes to alter the DB back to READ_ONLY the database has objects locked. I've traced it back to the previous connection and a Shared_Transaction_Workspace lock (sys.dm_tran_locks) for what looks to be the previous powershell connection. Why is this connection still open after the invoke-sqlcmd has completed and is there anything I can do about it? Can I force invoke-sqlcmd to use a new connection for each invocation of the cmdlet?
I have tried a messy fix killing the offending connection and then retrying the connection but I think there is something better.
I've always done this and it seems to work:
[System.Data.SqlClient.SqlConnection]::ClearAllPools()
Well, I know that this is a very old post and the people from Microsoft told that fixed this issue (as told the article mentioned by David Brabant) but maybe I'm not the luckiest guy and have to make an workaround to make it happens.
Even running Microsoft SQL Server 2012 (SP1) - 11.0.3128.0 (X64) I had the same issue and after make some researches I got a way to get some parameter from Invoke-Sqlcmd as output so I can get the Session ID of the current user process with the built-in ##SPID global variable from the SQL Server and make a connection with ADO.NET to execute a KILL clause to close the opened connection.
So let's to the workaround applied in my case
#Invoke the Invoke-Sqlcmd to execute an script from a file
Invoke-Sqlcmd -Server "[SERVER_NAME]" -Database [DATABASE_NAME] -Username [USER] -Password [PASSWORD] -InputFile [DOT_SQL_FILE_PATH]
#Invoke the Invoke-Sqlcmd to execute a inline SQL statement to get the SessionID as a Powershell variable
$SQLSession = Invoke-Sqlcmd -Server "[SERVER_NAME]" -Database [DATABASE_NAME] -Username [USER] -Password [PASSWORD] -query "select ##spid as SessionID"
# Build query to execute KILL clause
$DbQuery = "KILL " + $SQLSession.SessionID;
# Create SQL connection with ADO.NET
$DbConnection = New-Object System.Data.SqlClient.SqlConnection
$DbConnectionString = "Server = [SERVER_NAME]; Database = [DATABASE_NAME]; User ID=[USER]; Password=[PASSWORD];"
$DbConnection.ConnectionString = $DbConnectionString
$DbConnection.Open()
# Create SQL command for KILL clause
$DbCommand = New-Object System.Data.SQLClient.SQLCommand
$DbCommand.Connection = $DbConnection
$DbCommand.CommandText = $DbQuery
# Execute KILL clause
$DbCommand.ExecuteNonQuery()
# Close connection
$DbConnection.Close()
I hope that it helps
Even though I am using the newest version of SSMS (Version 16.5.3 - Build 13.0.16106.4), I still get this issue. I haven't figured out what the "right" way of forcing the connection closed is, but I have a work-around that is simple and resolves the issue for me. If you just need to get the connection off the database, you can do the following:
Run normal command(s)
Invoke-Sqlcmd -ServerInstance "SOME_SERVER" -Database "SOME_DB" ...
When you are ready to eliminate the connection from the database:
Invoke-Sqlcmd -ServerInstance "SOME_SERVER" -Database "SOME_DB" -Query "use [master];"
This will switch the connection to master, thus removing it from the database of interest. If you absolutely need the connection closed, I think you need to resort to SqlClient or such.