Powershell Write-Host showing only dataTable name instead of data - sql-server

I'm trying to write a Powershell script that executes a SQL query contained in a .sql file
Function RunSQLScript ($connstring, $filePath)
{
$query = get-content $filePath;
$DTSet = New-Object System.Data.DataSet;
$Conn=New-Object System.Data.SQLClient.SQLConnection $connstring;
$Conn.Open();
try
{
$DataCmd = New-Object System.Data.SqlClient.SqlCommand;
$MyQuery = $query;
$DataCmd.CommandText = $MyQuery;
$DataCmd.Connection = $Conn;
$DAadapter = New-Object System.Data.SqlClient.SqlDataAdapter;
$DAadapter.SelectCommand = $DataCmd;
$DAadapter.Fill($DTSet) | Out-Null;
for ($i = 0; $i -lt $DTSet.Tables.Count; $i++) {
Write-Host $DTSet.Tables[$i];
}
}
finally
{
$Conn.Close();
$Conn.Dispose();
}
return $DTSet;
}
The internal Write-Host is showing the DataTable name instead of the DataRows.
If I manually create a DataSet with a DataTable in Powershell Console, Write-Host shows me the data in the DataTable rows, so I can't really figure out why it is not doing that in the previous script.
Can you give me some clues on how to show the data contained in the datatables instead of the table names?
Thank you

This piece of code was quite helpful for me, posting it here if anybody needs it.
for ($i = 0; $i -lt $DTSet.Tables.Count; $i++) {
$DTSet.Tables[$i] | format-table | out-host
}
That produces a nice table-like output on screen.

Related

Powershell populate word table from an array

I have a PS script that will import a csv into several arrays and I need it to populate a table in word. I am able to get the data into the arrays, and create a table with headers and the correct number of rows, but cannot get the data from the arrays into the table. Doing lots of google searches led me to the following code. Any help is greatly appreciated.
Sample of My_File.txt
Number of rows will vary, but the header row is always there.
component,id,iType,
VCT,AD-1234,Story,
VCT,Ad-4567,DR,
$component = #()
$id = #()
$iType =#()
$vFile = Import-CSV ("H:\My_file.txt")
$word = New-Object -ComObject "Word.Application"
$vFile | ForEach-Object {
$component += $_.components
$id += $_.id
$iType +=_.iType
}
$template = $word.Documents.Open ("H:\Test.docx")
$template = $word.Document.Add()
$word.Visible = $True
$Number_rows = ($vFile.count +1)
$Number_cols = 3
$range = $template.range()
$template.Tables.add($range, $Number_rows, $Number_cols) | out-null
$table = $template.Tables.Item(1)
$table.cell(1,1).Range.Text = "Component"
$table.cell(1,2).Range.Text = "ID"
$table.cell(1,3).Range.text = "Type"
for ($i=0; $i -lt; $vFile.count+2, $i++){
$table.cell(($i+2),1).Range.Text = $component[$i].components
$table.cell(($i+2),2).Range.Text = $id[$i].id
$table.cell(($i+2),3).Range.Text = $iType[$i].iType
}
$Table.Style = "Medium Shading 1 - Accent 1"
$template.SaveAs("H:\New_Doc.docx")
Don't separate the rows in the parsed CSV object array into three arrays, but leave the collection as-is and use the data to fill the table using the properties of that object array directly.
I took the liberty of renaming your variable $vFile into $data as to me at least this is more descriptive of what is in there.
Try
$data = Import-Csv -Path "H:\My_file.txt"
$word = New-Object -ComObject "Word.Application"
$word.Visible = $True
$template = $word.Documents.Open("H:\Test.docx")
$Number_rows = $data.Count +1 # +1 for the header
$Number_cols = 3
$range = $template.Range()
[void]$template.Tables.Add($range, $Number_rows, $Number_cols)
$table = $template.Tables.Item(1)
$table.Style = "Medium Shading 1 - Accent 1"
# write the headers
$table.cell(1,1).Range.Text = "Component"
$table.cell(1,2).Range.Text = "ID"
$table.cell(1,3).Range.text = "Type"
# next, add the data rows
for ($i=0; $i -lt $data.Count; $i++){
$table.cell(($i+2),1).Range.Text = $data[$i].component
$table.cell(($i+2),2).Range.Text = $data[$i].id
$table.cell(($i+2),3).Range.Text = $data[$i].iType
}
$template.SaveAs("H:\New_Doc.docx")
When done, do not forget to close the document, quit word and clean up the used COM objects:
$template.Close()
$word.Quit()
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($template)
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($word)
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()

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;

Powershell / SQL Server / Import-CSV

Working on another script for work and I'm attempting to read from a CSV containing only one column of data. And then for each item to find the corresponding ID when querying the SQL database. Then to put the result(ID1, CSVID1) in to an excel file(I have this part working fine).
Now I have run in to an issue as to how to populate the dataset within a foreach loop.
$excelAssets = Import-Csv .\test.csv -Header assetId | Foreach-Object {
$assetId = $_.assetId
# SQL Query Variables
$query = "SELECT AssetId AS AssetID, BrandId AS BrandID FROM [AssetLibrary_BrandAsset] WHERE AssetId = $assetId"
$connection = New-SqlConnection -Server $dataSource -Database $dataBase
#Execute the SQL commands and place the results in dataset
if ($connection.State -eq 'Open')
{
$swLap = Start-Elapsed $sw "Executing SQL Query"
Write-Verbose "$query";
$dataSet += Invoke-SQLQuery -connection $connection -query $query -ExecutionTimeout '0'
$i++
$connection.Close();
End-Elapsed $sw $swLap
} ELSE {
Write-Error "$($(Format-Elapsed $swLap)) SQL Connection Not Open - Exiting...";
exit;
}
}
Now $dataSet += doesn't work and I have googled numerous times to try and find the answer to this problem. Any help is appreciated.
Using the $dataSet
$dataTable = new-object "System.Data.DataTable" "Results"
$dataTable = $dataSet.Tables[0]
$rowDT = $dataTable.Rows.Count;
$colDT = $dataTable.Columns.Count;
Write-Host -NoNewLine "$(Format-Elapsed $sw.Elapsed) Rows: ";
Write-Host -NoNewLine "$($rowDT+1)" -ForegroundColor "Green";
Write-Host -NoNewLine " Columns: "
Write-Host -NoNewLine "$($colDT+1)" -ForegroundColor "Green";
Write-Host -NoNewLine " Cells: "
Write-Host "$( ($colDT+1)*($rowDT+1) )" -ForegroundColor "Green";
#Create a 2D Array of the DataTable
# http://stackoverflow.com/questions/13184191/fastest-way-to-drop-a-dataset-into-a-worksheet
$tableArray = New-Object 'object[,]' $rowDT, $colDT;
$swLap = Start-Elapsed $sw "DataTable transformation"
# i = row and j = column
for ($i=0;$i -lt $rowDT; $i++)
{
#Write-Progress -Activity "Transforming DataTable" -status "Row $i" -percentComplete ($i / $rowDT*100)
for ($j=0;$j -lt $colDT; $j++)
{
$tableArray[$i,$j] = $dataTable.Rows[$i].Item($j).ToString();
}
}
End-Elapsed $sw $swLap
$rowOffset = 1; $colOffset = 1;# 1,1 = "A1"
# Write out the header column names
for ($j=0;$j -lt $colDT; $j++)
{
$ActiveWorksheet.cells.item($rowOffset, $j+1) = $dataTable.Columns[$j].ColumnName;
}
$headerRange = $ActiveWorksheet.Range($ActiveWorksheet.cells.item($rowOffset, $colOffset), $ActiveWorksheet.cells.item($rowOffset, $colDT+$colOffset-1));
$headerRange.Font.Bold = $false
$headerRange.Interior.Color = $headingColour
$headerRange.Font.Name = $headingFont
$headerRange.Font.Color = $headingFontColour
$rowOffset++;
# Extract the data to Excel
$tableRange = $ActiveWorksheet.Range($ActiveWorksheet.cells.item($rowOffset, $colOffset), $ActiveWorksheet.cells.item($rowDT+$rowOffset-1, $colDT+$colOffset-1));
$tableRange.Cells.Value2 = $tableArray;
# Resize the columns in Excel
$swLap = Start-Elapsed $sw "Resize Excel Worksheet"
$wholeRange = $ActiveWorksheet.UsedRange
$wholeRange.EntireColumn.AutoFit() | Out-Null
End-Elapsed $sw $swLap
# Save Excel workbook
$ActiveWorkbook.SaveAs("$OutputFile")
$ActiveWorkbook.Close()
After assigning to $dataSet the first time, it's type is probably not array, meaning that the += operator doesn't behave exactly as you expect.
You can either initialize $dataSet as an empty array before you start assigning to it:
Import-Csv .\test.csv -Header assetId | Foreach-Object -Begin {$dataSet = #()} -Process {
# rest of script in here
} -End {return $dataSet}
or you can cast it during assigning:
[array]$dataSet += Invoke-SQLQuery -connection $connection -query $query -ExecutionTimeout '0'
finally, an alternative solution would be to ensure that the output from Invoke-SQLQuery is treated as an array before you assign it to $dataSet:
$dataSet += #(Invoke-SQLQuery -connection $connection -query $query -ExecutionTimeout '0')
Whatever suits your style of coding.

Using ForEach-Object on Array of Structs - Powershell

I'm evolving my Surveillance script, so i can choose a Service/Maintenance Window. Where all errors are ignored between two time intervals.
This is what i got:
Add-Type -TypeDefinition #"
public struct ServiceWindow
{
public int SWStart;
public int SWEnd;
}
"#
[array]$SWArray = New-Object ServiceWindow
$time = Get-Date -Format HHMM
$time
$ActiveBatchVar = "1000-1005;1306-1345;2300-2305"
$ActiveBatchVar = $ActiveBatchVar.Split(";")
For ($i = 0; $i -lt $ActiveBatchVar.Length; $i++)
{
$tempSW = New-Object ServiceWindow
$tempSW.SWStart = $ActiveBatchVar[$i].Split("-")[0]
$tempSW.SWEnd = $ActiveBatchVar[$i].Split("-")[1]
If ($i -eq 0) { $SWArray = $tempSW } else { $SWArray += $tempSW }
}
Write-Host Complete array...
$SWArray
ForEach-Object ($SWArray) {
Get-Date -Format HHMM
If ($time -ge $_.SWStart -and $time -lt $_.SWEnd) {Write-Host Wohoo we have hit a service window service window...}
}
I get an error in my last ForEach-Object loop. and can't figure out what is wrong.
The point is that I would like to check if the current time is between two given times, like "1000-1005".
Anyone got a clue what’s missing, or maybe a way to simplify the whole thing ;)
Ok, a few things here... You really seem to like the Split() method. You may want to look into some alternatives, like this:
$ActiveBatchVar = #(#("1000","1005"),#("1306","1345"),#("2300","2305"))
See what we did there? It's an array of arrays. #() is the array notation. So I have an array, with 3 arrays in it.
I'm not real familliar with structs, but I am familliar with custom objects, so I would use that if it were me. Then you could do something like:
$SWArray = #() #That's an empty array, we'll add things to it now that it exists
ForEach ($Batch in $ActiveBatchVar){
$SWArray += New-Object PSObject -Property #{
SWStart = $Batch[0]
SWEnd = $Batch[1]
}
}
So then we change the last bit so that you are assigning $time just before your next loop to keep it as accurate as possible, and correct the ForEach just a little and the whole thing would look like this:
$ActiveBatchVar = #(#("1000","1005"),#("1306","1345"),#("2300","2305"))
$SWArray = #()
ForEach ($Batch in $ActiveBatchVar){
$SWArray += New-Object PSObject -Property #{
SWStart = $Batch[0]
SWEnd = $Batch[1]
}
}
Write-Host Complete array...
$SWArray
$time = date -f HHmm
ForEach($SW in $SWArray) {
If ($time -ge $SW.SWStart -and $time -lt $SW.SWEnd) {
Write-Host "Wohoo we have hit a service window service window..."
}
}
Minimum changes:
ForEach-Object ($SWArray) {
to
$SWArray | % {
Also your last Write-Host should enclose the message in quoes ie
{Write-Host "Wohoo..."}
ForEach-Object ($SWArray) {}
This is the wrong syntax, you should use the keyword in
Foreach-Object ($array in $SWArray) {}
if you have a small array...
($SWArray).foreach({
Get-Date -Format HHMM
If ($time -ge $_.SWStart -and $time -lt $_.SWEnd)
{Write-Host Wohoo we have hit a service window service window...}
})

Fine-tuning Powershell SQL Script

My company has a program that tracks our Employee workouts. When we had this program made, we did not think about adding the ability to Add or Remove an employee to the program.
I wrote a script in PowerShell that allows us to do this easier than in SSMS. I would like to see if anyone can help me clean it up a bit and fine tune it.
My biggest headache is this 1 or -1 that gets returned anytime we execute a function. I would also like this to ask if they are finished, then loop back or exit. Right now it just exits as soon as they are done.
<#Writes the invoker to log#>
$trandate = Get-Date
$tranuser = $env:UserName
<# Variables to open the connection to the SQL server #>
$sqlcn = New-Object System.Data.SqlClient.SqlConnection
$sqlcn.ConnectionString = "server=10.10.1.19\VTSWORKOUT;Integrated
Security=true;Database=VTSWORKOUT;"
<# Read what the user wants to do #>
$input = Read-Host "Do you want to [A]dd a New Employee, [R]emove an Employee or [E]xit?"
switch($input){
<# Stuff for adding an employee to the database #>
A{
$eid = Read-Host "What is the Employees ID number?"
$fname = Read-Host "What is the Employees first name?"
$lname = Read-Host "What is the Employees last name?"
$dept = Read-Host "What department is the Employee in?"
$pay = Read-Host "Is the Employee Salaried? [0]Yes or [1]No"
$hire = Read-Host "When was the Employee hired? Input as MM-DD-YYYY"
Out-File -FilePath "L:\Personnel\WorkoutApp\workouts.log" -Append -InputObject "On $trandate, $tranuser added Employee# $eid, $fname $lname"
$sqlcn.Open()
$sqlcmd = $sqlcn.CreateCommand()
$query = "INSERT INTO employees values (#eid,#lname,#fname,#dept,#pay,#hire)"
$sqlcmd.CommandText = $query
$sqlcmd.Parameters.AddWithValue("#eid", $eid) | Out-Null
$sqlcmd.Parameters.AddWithValue("#fname", $fname) | Out-Null
$sqlcmd.Parameters.AddWithValue("#lname", $lname) | Out-Null
$sqlcmd.Parameters.AddWithValue("#dept", $dept) | Out-Null
$sqlcmd.Parameters.AddWithValue("#pay", $pay) | Out-Null
$sqlcmd.Parameters.AddWithValue("#hire", $hire) | Out-Null
$sqlcmd.ExecuteNonQuery()
$sqlcn.Close()
}
<# Stuff for removing an employee from the database#>
R{
<#Collect reason for removal#>
$reason = Read-Host -Prompt "Why are you deleting this employee?"
$eid = Read-Host "What is the ID number of the Employee you want to remove?"
$sqlcn.Open()
$sqlcmd = $sqlcn.CreateCommand()
$query = "SELECT EmployeeID,FirstName, LastName from Employees WHERE EmployeeID = #eid"
$sqlcmd.CommandText = $query
$sqlcmd.Parameters.AddWithValue("#eid", $eid) | Out-Null
$sqlcmd.ExecuteNonQuery()
$Reader = $sqlcmd.ExecuteReader()
$arry = #()
while ($Reader.Read()) {
$row = #{}
for ($i = 0; $i -lt $reader.FieldCount; $i++)
{
$row[$reader.GetName($i)] = $reader.GetValue($i)
}
#convert hashtable into an array of PSObjects
$arry+= new-object psobject -property $row
}
$sqlcn.Close()
write-host $arry
$empResult = Read-Host "Is that the correct employee? [Y]es or [N]o"
<#If the correct employee was found, continue below.
If the wrong employee was returned, Kill Program #>
switch($empResult) {
Y{
Out-File -FilePath "L:\Personnel\WorkoutApp\workouts.log" -Append -InputObject "On $trandate, $tranuser deleted Employee $eid for the following reason: $reason"
$sqlcn.Open()
$sqlcmd = $sqlcn.CreateCommand()
$query = "DELETE FROM Employees WHERE EmployeeID = #eid"
$sqlcmd.CommandText = $query
$sqlcmd.Parameters.AddWithValue("#eid", $eid)
$sqlcmd.ExecuteNonQuery()
$adp = New-Object System.Data.SqlClient.SqlDataAdapter $sqlcmd
$data = New-Object System.Data.DataSet
$adp.fill($data) | Out-Null
$sqlcn.Close()
}
N{
Out-File -FilePath "L:\Personnel\WorkoutApp\workouts.log" -Append -InputObject "On $trandate, $tranuser tried to deleted Employee $eid. But exited the program before doing so."
Write-Host "Please restart the program. If the issue persists, please contact the IT department."
Read-Host -Prompt "Press Enter to exit"
}
}
}
<# Line to exit the program #>
E{
exit
}
}
Any thoughts on cleaning this up would be greatly appreciated.
This is off-topic, but I'll give you an answer.
Generally, you don't want to use Parameters.AddWithValue() at all, because that sends every parameter as an NVARCHAR. It's not deprecated, but it's not a good idea to use it. If you've got datetimes or other non-string parameters, you can end up with problems. It's usually preferable to use Parameters.Add():
$sqlcmd.Parameters.Add("#eid", [System.Data.SqlDbType]::Int).Value = $eid
Obviously, the datatype you use from [System.Data.SqlDbType] should match the datatype of the actual column in the database. This also has the benefit that there won't be any return value that you need to send to Out-Null or cast as [void].
This is also a mess:
$sqlcmd.ExecuteNonQuery()
$Reader = $sqlcmd.ExecuteReader()
$arry = #()
while ($Reader.Read()) {
$row = #{}
for ($i = 0; $i -lt $reader.FieldCount; $i++)
{
$row[$reader.GetName($i)] = $reader.GetValue($i)
}
#convert hashtable into an array of PSObjects
$arry+= new-object psobject -property $row
}
First, you're executing the query twice. Both ExecuteNonQuery() and ExecuteReader() will execute the query! You do that multiple times in your script.
Second, you can just do this:
$DataTable = New-Object System.Data.DataTable
$DataTable.Load($sqlcmd.ExecuteReader())
Then, if you really don't want to work with a DataTable -- they're more complex than a custom object but really not that bad -- you can do this to convert it to a generic object pretty easily:
$Data = $DataTable | ConvertTo-Csv -NoTypeInformation | ConvertFrom-Csv
This will make everything a string, though, so be sure that's what you want. You might also try this:
$Data = $DataTable | Select-Object -Property <list>
You don't want to use Select-Object * because you'll get extra properties you probably don't want.
This is also executing the query twice:
$sqlcn.Open()
$sqlcmd = $sqlcn.CreateCommand()
$query = "DELETE FROM Employees WHERE EmployeeID = #eid"
$sqlcmd.CommandText = $query
$sqlcmd.Parameters.AddWithValue("#eid", $eid)
$sqlcmd.ExecuteNonQuery()
$adp = New-Object System.Data.SqlClient.SqlDataAdapter $sqlcmd
$data = New-Object System.Data.DataSet
$adp.fill($data) | Out-Null
$sqlcn.Close()
Both $sqlcmd.ExecuteNonQuery() and $adp.fill($data) execute the query! Additionally, ExecuteNonQuery() returns the number of records affected. You could do this:
$sqlcmd.ExecuteNonQuery() | Out-Null
Or this:
[void]$sqlcmd.ExecuteNonQuery()
But what you really should do is verify that the result is what you expect. You shouldn't be getting -1 for INSERT or DELETE statements.
Learn to look up the documentation for the methods you're calling and understand what the possible return values are and why. All the .Net methods are thoroughly documented on MSDN. You can almost always find them by Googling "C# ". You'll find C# examples that can easily be converted to PowerShell, too.

Resources