How to pass array as variable in SQLCMD - sql-server

I am trying to pass server names in sql script , but its not working.
Please help
SQL Script patch_report.sql, I am running via powershell giving error
SELECT * from table where server in ('$(trimsqlstr)')
Error
Msg 102,level Level 15, State 1, Server DBserver, Line 1
Incorrect syntax near 'server1'.
$DB_server = 'DBserver'
$serverName = "server1
server2
server3
"
$serverName = $serverName -split "\n" | foreach {$_.ToString().TrimEnd()}
$trimsqlstr = foreach($server in $serverName){
if ($serverName.Indexof($server) -eq $($serverName.Length-1)){
"'$Server'"
} else {
"'$Server',"
}
SQLCMD.exe -v trimsqlstr = "$($trimsqlstr)" -E -S $DB_server -W -i patch_report.sql
I am expecting it to result like this
SELECT * from table where server in ('server1','server2','server3')

The accepted answer is an excellent solution, I just want to point you towards the -join operator.
The -join operator is very practical for joining members of an array to a string.
You can do something like this:
$serverNames = "server1
server2
server3
"
$serverNameArray = $serverNames -split "\n" | foreach {$_.ToString().TrimEnd()} | Where-Object {$_} | foreach {"'$_'"}
$whereClause = $serverNameArray -join ','
$selectQuery = "SELECT * from table where server in ($whereClause)"
Where-Object {$_} is removing the empty elements.

You may use below code:
$DB_server = 'DBserver'
$serverName = "server1
server2
server3
"
$serverName = $serverName -split "\n" | foreach {$_.ToString().TrimEnd()}
ForEach($server in $serverName)
{
$serverstring = $serverstring+"'"+$server+"'"+","
}
$trimsqlstr = $serverstring.Substring(0, $serverstring.Length-4)
And then use $trimsqlstr in query like below
SELECT * from table where server in ($trimsqlstr)

Related

Split values using Powershell

I have a file with multiple expressions like "$REGX('CareMedic.2_0','CustomerInformation','Customer Information')". The file can be a xml file, text file or any other type. If the file contains 9 of those expressions, I'm trying to pull all nine and send the values to a database.
I've tried my code as below:
$input_path = ‘C:\Users\Administrator\Desktop\test2.xml’
$SQLServer = "WIN-17V7QT0IJVK"
$SQLDBName = "Test"
$uid ="WIN-17V7QT0IJVK\Administrator"
$pwd = "letmebackinplease"
$SqlQuery = "SELECT * from product_schema;"
$ConnectionString = "Server = $SQLServer; Database = $SQLDBName; Integrated Security = True;"
$SqlConnection = New-Object System.Data.SqlClient.SqlConnection $ConnectionString
$SqlConnection.open()
if($SqlConnection.state -eq "Open"){
Write-Host "Test connection successful"
}
$regex = '()\(.*?\)'
$output = select-string -Path $input_path -Pattern $regex -AllMatches | % { $.Matches } | % { $.Value } |
ForEach-Object {
($_ -split "\(|\)")[1]
}
foreach ($line in $output){
$line = $line -replace "\(",""
$line = $line -replace "\)",""
$line = $line -replace "\'",""
$col1,$col2,$col3 = $line -split ","
[PSCustomObject]#{
col1 = $col1
col2 = $col2
col3 = $col3
} | select col1,col2,col3
$insert_query = "INSERT INTO [$SQLDBName].[dbo].[product_schema]
([version]
,[field]
,[value])
VALUES
($col1, $col2, $col3);"
$execute_query = New-Object System.Data.SqlClient.SqlCommand
$execute_query.connection = $SQLConnection
$execute_query.commandtext = $insert_query
$execute_query.ExecuteNonQuery()
}
$SqlConnection.close()
If the file has two of the below:
('Medic.2_0','AgeInformation','Age Information')
('Medic.2_0','TransactionID','Transaction ID')
My actual output should be:
'Medic.2_0' stored in Version Column
'AgeInformation' stored in the Field Column
'Age Information' stored in the value column
'Medic.2_0' stored in Version Column
'TransactionID' stored in the Field Column
'Transaction ID' stored in the value column
I have to take each of the values and store it in a column in a temp table setup on MySQL server like below:
**Version** **Field** **Value**
Medic.2_0 AgeInformation Age Information
Medic.2_0 TransactionID Transaction ID
Error Encountered:
Exception calling "ExecuteNonQuery" with "0" argument(s): "Incorrect syntax near '.2'."
At C:\Users\Administrator\Desktop\test.ps1:47 char:10
+ $execute_query.ExecuteNonQuery()
+ ~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : SqlException
Can someone please recommend how shall I change my code to solve this?
In answer to your original question before editing: Assuming your output looks like this and is saved in a variable named $output
('Medic.2_0','AgeInformation','Age Information')
('Medic.2_0','TransactionID','Transaction ID')
Try this:
foreach ($line in $output){
$line = $line -replace "\(",""
$line = $line -replace "\)",""
$line = $line -replace "\'",""
$col1,$col2,$col3 = $line -split ","
[PSCustomObject]#{
col1 = $col1
col2 = $col2
col3 = $col3
} | select col1,col2,col3 | export-csv d:\test.csv -append -NoTypeInformation
}
We are looping through the $output line by line removing the brackets and the single quotes, splitting the remaining text on the comma, then assigning each of the three entries into the relevant variables. Once they are in variables we can then easily create a PSObject and use it to select our requirements for our export-csv
Try to add this code:
$info=#() #for store your values
foreach($item in $output){
$z=$item.split(',') #for split to 3 strings
$info+=[PSCustomObject]#{ #create custom object which have named columns and store our values
Version = $z[0]
Field = $z[1]
Value = $z[2]
}
}
Write-Output $info #variable that store all columns
Then you must run foreach loop to each object in $info .
you can run it like this:
foreach($data in $info){
$data.Version #to access Version field
$data.Field #to access Field field
$data.Value #to access Value field
.......your SQL query......
}

Invoke-SqlCmd generates an exception when used in a loop that contains another Invoke-SqlCmd

I'm having an issue with calling Invoke-SqlCmd when it contains a second Invoke-SqlCmd call:
function Get-Foo
{
$query=`
#"
WITH data AS
(
SELECT 1 ID, 'A' NAME
UNION ALL
SELECT 2 ID, 'B' NAME
UNION ALL
SELECT 3 ID, 'C' NAME
)
SELECT *
FROM data
"#
Invoke-SqlCmd -ServerInstance "SERVER" -Database "DATABASE" -Query $query
}
function Get-Bar
{
param
(
[int]$ParentId
)
$query=`
#"
WITH data AS
(
SELECT 1 ID, 'A' NAME, 1 PARENT_ID
UNION ALL
SELECT 2 ID, 'B' NAME, 1 PARENT_ID
UNION ALL
SELECT 3 ID, 'C' NAME, 2 PARENT_ID
)
SELECT *
FROM data
WHERE parent_id = $ParentId
"#
Invoke-SqlCmd -ServerInstance "SERVER" -Database "DATABASE" -Query $query
}
Get-Foo | ForEach-Object {
Get-Bar -ParentId $_.ID
}
The first iteration of the outer loop works fine, but when it attempts the the second iteration, and exception is generated:
Invoke-SqlCmd : The WriteObject and WriteError methods cannot be
called from outside the overrides of the BeginProcessing,
ProcessRecord, and EndProcessing methods, and they can only be called
from within the same thread. Validate that the cmdlet makes these
calls correctly, or contact Microsoft Customer Support Services. At
Untitled-1.ps1:18 char:3
+ Invoke-SqlCmd -ServerInstance "SERVER" -Database "DATABASE ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidResult: (SERVER:PSObject) [Invoke-Sqlcmd], PSInvalidOperationExceptio n
+ FullyQualifiedErrorId : ExecutionFailed,Microsoft.SqlServer.Management.PowerShell.GetScriptCommand
This syntax works, however:
$foo = Get-Foo
$foo | ForEach-Object {
Get-Bar
}
I'm guessing that I need to close the first Invoke-SqlCmd, but perhaps there is another solution.
This is related to the way the PowerShell pipeline works (see Understanding pipelines), and a limitation in Invoke-SqlCmd where it doesn't like running more than one query in parallel.
To illustrate, let's rewrite your example as follows:
function Invoke-SqlCmd
{
write-output "aaa";
write-output "bbb";
write-output "ccc";
}
function Get-Foo
{
write-host "Get-Foo started";
Invoke-SqlCmd;
write-host "Get-Foo finished";
}
function Get-Bar
{
param( $value )
write-host "Get-Bar '$value'";
}
and now run this script:
write-host "using pipelines"
Get-Foo | foreach-object { Get-Bar $_ }
which gives this output:
using pipelines
Get-Foo started
Get-Bar 'aaa'
Get-Bar 'bbb'
Get-Bar 'ccc'
Get-Foo finished
If you look at the output, you can see that the "Get-Bar" commands are being called while Get-Foo is still "active" in the pipeline.
Compare to this:
write-host "using immediate evaluation"
(Get-Foo) | foreach { Get-Bar $_ }
which gives:
using immediate evaluation
Get-Foo started
Get-Foo finished
Get-Bar 'aaa'
Get-Bar 'bbb'
Get-Bar 'ccc'
where Get-Foo is executed to completion before the values are piped into Get-Bar.
Your specific error is due to your script using multiple concurrent pipeline steps but Invoke-SqlCmd not supporting multiple concurrent pipeline steps. Fortunately you can tell PowerShell to use "immediate evaluation" instead in a few different ways, including the approaches mentioned in other answers:
assigning the expression to a temporary variable first:
$foo = Get-Foo;
$foo | ForEach-Object { Get-Bar $_ }
using the SubExpression operator in your script (note the $ is optional in some cases as per Is "Dollar-sign" optional in powershell "$()"?)
(Get-Foo) | Foreach-Object { Get-Bar $_ }
or wrap Invoke-SqlCmd in a subexpression inside your function. (This one is a bit misleading because Get-Foo is still active in the pipeline but Invoke-SqlCmd is evaluated immediately).
function Get-Foo
{
write-host "Get-Foo started";
(Invoke-SqlCmd);
write-host "Get-Foo finished";
}
I cannot explain why this isn't working (although it looks like the threads are being mixed and the .NET provider cannot handle it properly).
What does work (but this is basically the same as your proposal of assigning it to a variable first) is to surround Get-Foo by parentheses:
(Get-Foo) | Foreach-Object { Get-Bar }
You can also experiment with the -Begin, -Process and -End parameters of Foreach-Object:
Get-Foo | ForEach-Object -Process {$_} -End { Get-Bar }
Based on Defending Invoke-SqlCmd, I decided to implement my own Invoke-SqlCmd cmdlet:
function Invoke-SqlCmd
{
[cmdletbinding()]
param
(
[string]$ServerInstance,
[string]$Database,
[string]$Query
)
Write-Debug $MyInvocation.MyCommand.Name
Write-Debug "$ServerInstance.$Database"
try {
$Connection = New-Object System.Data.SQLClient.SQLConnection
$Connection.ConnectionString = "server=$ServerInstance;database=$Database;trusted_connection=true;"
$Connection.Open()
$Command = New-Object System.Data.SQLClient.SQLCommand
$Command.Connection = $Connection
$Command.CommandText = $Query
$DataAdapter = New-Object System.Data.SqlClient.SqlDataAdapter $Command
$Dataset = New-Object System.Data.Dataset
$DataAdapter.Fill($Dataset) | Out-Null # suppress record count
$Dataset.Tables[0]
}
catch
{
Write-Error $_
}
finally
{
$Connection.Close()
}
}
This implementation will be used instead of the 'regular' Invoke-SqlCmd cmdlet in my functions.
The loop works as expected.

Powershell - proper way to execute SQL query with multiple select statements and result tables

I'm trying to execute an SQL query with few select statements, that returns multiple tables as a result. The problem is that I can't find a way to read and use the tables separately.
Expected results:
Actual results: (it is printed row by row)
Purpose: I've made a script that creates an empty excel file with multiple sheets and each of the sheets will be used to contain each resultset of the query.
The only thing left is to put the needed text into the sheets. Here is my code for that part only:
$ConnectionString = "Data Source=...;Initial Catalog=...;User Id=...;Password=..."
$DBServerName = $ConnectionString.split('=')[1].split(';')[0]
$DBName = $ConnectionString.split('=')[2].split(';')[0]
$DBUser = $ConnectionString.split('=')[3].split(';')[0]
$DBPassword = $ConnectionString.split('=')[4].split(';')[0]
$CurrentFilePath = "C:\SQLqueryWithManyResultsets.sql"
$query = Get-Content -literalPath $CurrentFilePath | Out-String #getting the query string from file
$resultTables = Invoke-Sqlcmd -Query $query -ServerInstance $DBServerName -Database $DBName -DisableVariables -Password $DBPassword -Username $DBUser -ErrorAction Stop
foreach ($result in $resultTables) {
$result | Format-Table #where the magic happens
}
I've made a lot of research, but I cannot find a proper way to store and read the tables the way i need.
Try this:
Clear-Host;
$objConnection = New-Object System.Data.SqlClient.SqlConnection;
$objConnection.ConnectionString = "...";
$ObjCmd = New-Object System.Data.SqlClient.SqlCommand;
$ObjCmd.CommandText = "...";
$ObjCmd.Connection = $objConnection;
$ObjCmd.CommandTimeout = 0;
$objAdapter = New-Object System.Data.SqlClient.SqlDataAdapter;
$objAdapter.SelectCommand = $ObjCmd;
$objDataSet = New-Object System.Data.DataSet;
$objAdapter.Fill($objDataSet) | Out-Null;
for ($i=0; $i -lt $objDataSet.Tables.Count; $i++) {
Write-Host ($objDataSet.Tables[$i] | Format-Table | Out-String);
}
$query = $null;
$objDataSet = $null;
$objConnection.Close();
$objConnection = $null;

how to write powershell script to list the available databases from multiple servers?

Requirement : My requirement is I have to list the available databases from 150 servers. Each server has minimum 1 and maximum 15 instances.
Below script is working only for instances listed in sqlserver.txt but I need to fetch multiple instances across multiple servers.
Help is highly appriciated.
ForEach ($instance in Get-Content "C:\PowerSQL\SQL_Servers.txt")
{
Import-Module SQLPS -DisableNameChecking
Invoke-SQLcmd -Server $instance -Database master 'select ##servername as InstanceName,name as DatabaseName,state_desc as DBStatus from sys.databases' | Format-Table
}
You can use this script to find all reachable instances on your network and running your query there:
Import-Module SQLPS -DisableNameChecking
$servers = [System.Data.Sql.SqlDataSourceEnumerator]::Instance.GetDataSources()
ForEach ($i in $servers) {
$instance = $i.ServerName+"\"+$i.InstanceName
Invoke-SQLcmd -Server $instance -Database master 'select ##servername as InstanceName,name as DatabaseName,state_desc as DBStatus from sys.databases' | Format-Table
}
If you need only server name to pass then use $instance = $i.ServerName. Part of code was taken from here long time ago.
EDIT
With writing in CSV file and error catching:
Import-Module SQLPS -DisableNameChecking
$servers = [System.Data.Sql.SqlDataSourceEnumerator]::Instance.GetDataSources()
$results = #()
ForEach ($i in $servers) {
$instance = $i.ServerName+"\"+$i.InstanceName
try {
$sqlres = Invoke-SQLcmd -Server $instance -Database master 'select ##servername as InstanceName,name as DatabaseName,state_desc as DBStatus from sys.databases'
ForEach($st in $sqlres) {
$instanceinfo = #{
InstanceName = $st.InstanceName
DatabaseName = $st.DatabaseName
DBStatus = $st.DBStatus
}
$results += New-Object PSObject -Property $instanceinfo
}
} catch {
"error when running Invoke-SQLcmd "+$instance
Write-Host($error)
}
}
$results | export-csv -Path D:\sql_instances_info.csv -NoTypeInformation
Im not sure what is the problem here. You can put all servers/instances in txt file and iterate:
#array of addresses, this can be fetched from file
$list = "localhost\SQL2014",".\SQL2014","(local)\SQL2014" #MyServer\MyInstance
$list | `
% { Invoke-Sqlcmd -Server $_ -Database master 'select ##servername as InstanceName,name as DatabaseName,state_desc as DBStatus from sys.databases' } | `
Format-Table -AutoSize
If those are remote servers without integrated security you would need to pass -UserName and -Password arguments.

Adding column to SQL query on multiple instances from powershell

I have the following powershell script which reads in a list of servers, and runs SQL command on these servers. This data is then exported to csv and to excel format
I would like to be able to add the targeted server name from my server list as the first column so columns would look like this (server name added to front)
Server Name | Name | CollectionSet ID | Collection Mode | Retention Period | Schedule
This is the current script I have:
Param
(
[string]$fServers = 'W:\Theo\Scripts\mdw_servers.csv'
)
$query = "SELECT a.name AS 'DC Name',
collection_set_id AS 'Collection_set ID',
CASE collection_mode
WHEN 1 THEN 'non-cached'
WHEN 0 THEN 'cached'
END AS 'Collection Type' ,
days_until_expiration AS 'Retention Period' ,
b.name AS 'Schedule Name'
FROM msdb.dbo.syscollector_collection_sets a ,
msdb.dbo.sysschedules b
WHERE a.schedule_uid = b.schedule_uid
AND is_running = 1;"
$csvFilePath = "W:\Theo\Scripts\queryresults.csv"
$excelFilePath = "W:\Theo\Scripts\queryresults.xls"
# Run Query against multiple servers, combine results
$allServers = Get-Content -Path $fServers
foreach ($Server in $allServers) {
write-host "Executing query against server: " $Server
$results += Invoke-Sqlcmd -Query $query -ServerInstance $Server;
}
# Output to CSV
write-host "Saving Query Results in CSV format..."
$results | export-csv $csvFilePath -NoTypeInformation
# Convert CSV file to Excel
write-host "Converting CSV output to Excel..."
$excel = New-Object -ComObject excel.application
$excel.visible = $False
$excel.displayalerts=$False
$workbook = $excel.Workbooks.Open($csvFilePath)
$workSheet = $workbook.worksheets.Item(1)
$resize = $workSheet.UsedRange
$resize.EntireColumn.AutoFit() | Out-Null
$xlExcel8 = 56
$workbook.SaveAs($excelFilePath,$xlExcel8)
$workbook.Close()
$excel.quit()
$excel = $null
write-host "Results are saved in Excel file: " $excelFilePath
Any input is appreciated!
have you tried
SELECT ##SERVERNAME AS 'Server Name'
https://msdn.microsoft.com/en-us/library/ms187944.aspx

Resources