How can I copy huge amounts of data from Azure Storage Table Service to SQL Server using PowerShell?
AzTable which Microsoft recommends using does not support incremental load without underlying information about partition keys, and the documentation website is down...:
https://learn.microsoft.com/en-us/azure/storage/tables/table-storage-how-to-use-powershell
I am answering this question myself, because I have lots of trouble findind a solution online, and would like to help other people with this crap.
$StorageAccountResourceGroup = "MyResourceGroup"
$StorageAccountName = "MyStorageAccount"
$TableName = "MyStorageTableName"
$SqlConnectionString = "MySqlConnectionString"
$BulkCopy = [System.Data.SqlClient.SqlBulkCopy]::new()
$BulkCopy.DestinationTableName = "[myschema].[mytablename]"
$DataTable = <<Generate datatable from SQL-table>> # Code not included
Connect-AzAccount
$StorageAccountKey = (Get-AzStorageAccountKey -ResourceGroupName $StorageAccountResourceGroup -AccountName $StorageAccountName | Where-Object -FilterScript {$_.KeyName -eq "Key1"}).Value
$Context = New-AzStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $StorageAccountKey
$CloudTable = (Get-AzStorageTable -Context $Context -Name $TableName).CloudTable
$TableQuery = [Microsoft.Azure.Cosmos.Table.TableQuery]::new()
$Token = $null
BulkCounter = 0
do{
$ReturnObject = $CloudTable.ExecuteQuerySegmented($TableQuery, $Token)
$Token = $ReturnObject.ContinuationToken
foreach($Entry in $ReturnObject){
$Row = $DataTable.NewRow()
foreach($Column in $DataTable.Columns){
if($Column.ColumnName -eq "TimeStamp"){
$Value = $Entry.TimeStamp
} elseif($Column.ColumnName -eq "RowKey"){
$Value = $Entry.RowKey
} elseif($Column.ColumnName -eq "PartitionKey"){
$Value = $Entry.PartitionKey
} else {
if($Column.DataType -eq [System.Decimal]){
$Value = $Entry.Properties.$($Column.ColumnName).DoubleValue
} elseif($Column.DataType -eq [System.String]){
$Value = $Entry.Properties.$($Column.ColumnName).StringValue
} elseif($Column.DataType -eq [System.Guid]){
$Value = $Entry.Properties.$($Column.ColumnName).GuidValue
} elseif($Column.DataType -eq [System.datetimeoffset]){
$Value = $Entry.Properties.$($Column.ColumnName).DateTimeOffsetValue
} elseif($Column.DataType -eq [System.Int32]){
$Value = $Entry.Properties.$($Column.ColumnName).Int32Value
} elseif($Column.DataType -eq [System.Int64]){
$Value = $Entry.Properties.$($Column.ColumnName).Int64Value
} elseif($Column.DataType -eq [System.Boolean]){
$Value = $Entry.Properties.$($Column.ColumnName).BooleanValue
} elseif($Column.DataType -eq [System.Binary]){
$Value = $Entry.Properties.$($Column.ColumnName).BinaryValue
}
}
if([System.String]::IsNullOrWhiteSpace($Value)){
$Value = [System.DBNull]::value
}
$Row.($Column.ColumnName) = $Value
}
$DataTable.Rows.Add($Row)
$Counter++
}
$BulkCounter ++
if($BulkCounter % 25 -eq 0 -and $BulkCounter -ne 0){
$BulkCopy.WriteToServer($Datatable.CreateDataReader()) | Out-Null
$Datatable.Clear() | Out-Null
$BulkCounter = 0
}
} while ($Token)
if($Datatable.rows.count -gt 0){
$BulkCopy.WriteToServer($Datatable.CreateDataReader()) | Out-Null
$Datatable.Clear() | Out-Null
}
Here is also some functions for automatically generating tables from storage account tables:
function ConvertTo-SqlTypeFromEDM {
param (
$EDMType
)
if($EDMType -eq "String"){
return "varchar(100)"
} elseif($EDMType -eq "Guid"){
return "uniqueidentifier"
} elseif ($EDMType -eq "DateTime"){
return "datetimeoffset(0)"
} elseif ($EDMType -eq "Binary"){
return "varbinary(max)"
} elseif ($EDMType -eq "Int32"){
return "int"
} elseif ($EDMType -eq "Int64"){
return "long"
} elseif ($EDMType -eq "Double"){
return "decimal(25,5)"
} elseif ($EDMType -eq "Boolean"){
return "bit"
} else {
throw "Tybe $EDMType not implemented"
}
}
function Get-SqlQueryFromStorageTableSerivce {
param(
$CloudTable,
$SchemaName
)
$Query = [Microsoft.Azure.Cosmos.Table.TableQuery]::new()
$Query.TakeCount = 1
$Token = $null
$Result = $CloudTable.ExecuteQuerySegmented($Query, $Token)
$TableName = $CloudTable.Name
$TableString = "DROP TABLE IF EXISTS [$SchemaName].[$TableName]`n"
$TableString += "CREATE TABLE [$SchemaName].[$TableName] (`n`t"
$ColumnString = #()
$TableKeys = #()
if($Result.PartitionKey){
$ColumnString += "[PartitionKey] varchar(100) NOT NULL"
$TableKeys += "[PartitionKey]"
}
if($Result.RowKey){
$ColumnString += "[RowKey] varchar(100) NOT NULL"
$TableKeys += "[RowKey]"
}
if($Result.Timestamp){
$ColumnString += "[Timestamp] datetimeoffset(0) NULL"
}
foreach($Column in $Result.Properties.Keys){
$ColumnString += "[$Column] $(ConvertTo-SqlTypeFromEDM -EDMType $Result.Properties.$Column.PropertyType) NULL"
}
$TableString += $ColumnString -join ",`n`t"
$TableString += "`nCONSTRAINT [PK_1_$($TableName)_1] PRIMARY KEY CLUSTERED`n"
$TableString += "(`n`t" + ($TableKeys -join ",`n`t")
$TableString += "`n)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF, DATA_COMPRESSION = PAGE) ON [PRIMARY]"
$TableString += "`n) ON [PRIMARY]`n"
return $TableString
}
I always get an error when executing this script:
Incorrect syntax near '#values'
I have the following function to execute my SQL statement:
$Server = "Server"
$Database = "Database"
$i = 0
function ExecuteSQLQuery(){
$Connection = New-Object System.Data.SqlClient.SqlConnection
$Connection.ConnectionString = "server='$Server';database='$Database'; trusted_connection=true;"
$Connection.Open()
$query = #
"INSERT INTO [Database].[dbo].[Folder](FolderPath,UserGroup,Permission,AccessControllType) VALUES #values
"#
$command = $connection.CreateCommand()
$command.CommandText = $query
$command.Parameters.Add("#values", $values)
$command.ExecuteNonQuery()
}
And this is the rest of the code in which I am getting the Data for the INSERT
$RootPath = "\\server\folder1\folder2\folder3"
$Folders = dir $RootPath -recurse | where {$_.psiscontainer -eq $true}
#root folder permission
$ACLs = get-acl $RootPath | ForEach-Object { $_.Access }
Foreach ($ACL in $ACLs) {
#Add-Content -Value $OutInfo -Path $OutFile
$FolderPath = $RootPath
$User = $ACL.IdentityReference
$Permission = $ACL.FileSystemRights
$AccessControllType = $ACL.AccessControlType
if($i -gt 50) {
Invoke-sqlcmd #params -Query $InsertResult
$i=0
$values = ""
}
elseif($i -eq 0) {
$values += "('$FolderPath','$User','$Permission','$AccessControllType')"
$i ++
}
else {
$values += ",('$FolderPath','$User','$Permission','$AccessControllType')"
$i ++
}
}
#subfolders
foreach ($Folder in $Folders) {
$ACLs = get-acl $Folder.fullname | ForEach-Object { $_.Access }
Foreach ($ACL in $ACLs) {
$FolderPath = $Folder.Fullname
$User = $ACL.IdentityReference
$Permission = $ACL.FileSystemRights
$AccessControllType = $ACL.AccessControlType
$IsInherited = $ACL.IsInherited
if ($i -gt 50) {
ExecuteSQLQuery
$i=0
$values = ""
}
elseif ($i -eq 0) {
$values += "('$FolderPath','$User','$Permission','$AccessControllType')"
$i ++
}
else {
$values += ",('$FolderPath','$User','$Permission','$AccessControllType')"
$i ++
}
}
}
I am new to Powershell so I don't now if the error is because of my code or if the $values contains something Powershell or SQL can't handle.
Do you guys have any ideas?
I'm having a hard time converting results from Invoke-SqlCmd to a PSCustomobject.
So, I input my query and SQL server, then I run the Invoke-SqlCmd function, and then I try to add data from that (the database logical name, and the autogrowth status) to my PSCustomobject, so I can return it back to my modules public function.
$sqlinstance = "---"
$query = "---"
if ($sqlInstance -match "---") {
$dbAutogrowIGP = Invoke-Sqlcmd -Query $query -ServerInstance $sqlInstance
if ($dbAutogrowIGP.Growth -gt 100 -and $dbAutogrowIGP.Growth -lt 500) {
$autogrowStatus = [PSCustomObject]#{
'SQL_Instance' = $dbAutogrowIGP.LogicalName
'Check' = "Autogrow"
'Status' = "green"
'Status_reason' = ""
}
New-Object -Type Dictionary -Property $autogrowStatus
}
foreach ($db in $dbAutogrowIGP) {
if ($db.Growth -lt 100 -or $db.Growth -gt 500 -and $db.Growth -notlike "%") {
$autogrowStatus = [PSCustomObject]#{
'SQL_Instance' = $db.LogicalName
'Check' = "Autogrow"
'Status' = "red"
'Status_reason' = "$($db.LogicalName) has autogrowth set to $($db.Growth)."
}
New-Object -Type Dictionary -Property $autogrowStatus
}
if ($db.Growth -like "%") {
$autogrowStatus = [PSCustomObject]#{
'SQL_Instance' = $db.LogicalName
'Check' = "Autogrow"
'Status' = "yellow"
'Status_reason' = "$($db.LogicalName) has autogrowth set percentually, it should be an absolute number."
}
New-Object -Type Dictionary -Property $autogrowStatus
}
}
}
return $autogrowStatus
I've debugged it, and I've noticed it fails on the New-object call. I've tried both Dictionary and PSObject/PSCustomObject - however neither works
In my other functions, this works as expected, however in those, I'm using dbatools to make a call.
$getLogSizeIGP = Get-DbaDbLogSpace -sqlInstance $sqlInstance
if ($getLogSizeIGP.LogSize.Gigabyte -lt 10 -and $getLogSizeIGP.LogSpaceUsedPercent -lt 50) {
$logStatus = #{
'SQL_Instance' = $getLogSizeIGP.SqlInstance
'Check' = "Log_size"
'Status' = [gmEnvStatuses]::green
'Status_reason' = ""
}
New-Object -Type PSObject -Property $logStatus
}
How would I go about solving this issue?
This is the whole error message:
New-Object : Cannot bind parameter 'Property'. Cannot convert the "#{SQL_Instance=Maintenance_log; Check=Autogrow; Status=red; Status_reason=Maintenance_log has autogrowth set to 10%.}" value of type "System.Management.Automation.PSCustomObject" to type
"System.Collections.IDictionary".
At C:\Users\---\Desktop\autogrowth.ps1:50 char:55
+ New-Object -Type Dictionary -Property $autogrowStatus
+ ~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [New-Object], ParameterBindingException
+ FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.PowerShell.Commands.NewObjectCommand
Thanks!
The easiest way of collecting this data is by capturing it all at the beginning of the if ($sqlInstance -match "---") { statement and simply output the PsCustomObjects without trying to convert them.
Something like
$sqlinstance = "---"
$query = "---"
$autogrowStatus = if ($sqlInstance -match "---") {
$dbAutogrowIGP = Invoke-Sqlcmd -Query $query -ServerInstance $sqlInstance
if ($dbAutogrowIGP.Growth -gt 100 -and $dbAutogrowIGP.Growth -lt 500) {
# output the object to be captured in the $autogrowStatus variable
[PSCustomObject]#{
'SQL_Instance' = $dbAutogrowIGP.LogicalName
'Check' = "Autogrow"
'Status' = "green"
'Status_reason' = ""
}
}
foreach ($db in $dbAutogrowIGP) {
if ($db.Growth -lt 100 -or $db.Growth -gt 500 -and $db.Growth -notlike "%") {
[PSCustomObject]#{
'SQL_Instance' = $db.LogicalName
'Check' = "Autogrow"
'Status' = "red"
'Status_reason' = "$($db.LogicalName) has autogrowth set to $($db.Growth)."
}
}
if ($db.Growth -like "%") {
[PSCustomObject]#{
'SQL_Instance' = $db.LogicalName
'Check' = "Autogrow"
'Status' = "yellow"
'Status_reason' = "$($db.LogicalName) has autogrowth set percentually, it should be an absolute number."
}
}
}
}
return $autogrowStatus
The variable $autogrowStatus will become an array of PSCustomObjects to handle in the rest of your functions.
Hope this helps
I have written a script to Identify a timestamp in a logfile:
$start = Get-Content "C:\Webserverlogfiles\ise*.log" | select -first 1 -skip 6
$end -match '(([0-9][0-9]:){2}([0-9][0-9]))'
$end = $Matches[1]
$start -match '(([0-9][0-9]:){2}([0-9][0-9]))'
$start = $Matches[1]
$TimeDiff = New-TimeSpan $end $start
if ($TimeDiff.Seconds -lt 0) {
$Hrs = ($TimeDiff.Hours) + 23
$Mins = ($TimeDiff.Minutes) + 59
$Secs = ($TimeDiff.Seconds) + 59 }
else {
$Hrs = $TimeDiff.Hours
$Mins = $TimeDiff.Minutes
$Secs = $TimeDiff.Seconds }
$refreshrate = '{0:00}:{1:00}:{2:00}' -f $Hrs,$Mins,$Secs
echo $refreshrate
This returns the result I am after which is the timespan between each refresh.
Now I am trying to expand on this so it loops through the whole file. And so far my script just hangs.
$Workfrom = Get-Content "C:\Webserverlogfiles\ise*.log"
Foreach ($Line in (Get-Content $Workfrom)) {
$end = $line | Select-String 'ShowStatus = Reset Status' -SimpleMatch
$end -match '(([0-9][0-9]:){2}([0-9][0-9]))'
$end = $Matches[1]
$start = $line | Select-String 'ShowStatus = Waiting for server ()' -SimpleMatch
$start -match '(([0-9][0-9]:){2}([0-9][0-9]))'
$start = $matches[1]
$TimeDiff = New-TimeSpan $end $start
if ($TimeDiff.Seconds -lt 0) {
$Hrs = ($TimeDiff.Hours) + 23
$Mins = ($TimeDiff.Minutes) + 59
$Secs = ($TimeDiff.Seconds) + 59 }
else {
$Hrs = $TimeDiff.Hours
$Mins = $TimeDiff.Minutes
$Secs = $TimeDiff.Seconds }
$refreshrate = '{0:00}:{1:00}:{2:00}' -f $Hrs,$Mins,$Secs
echo $refreshrate
}
From what I can tell this is correct unless I have grouped too much into the ForEach loop. Can I ask what I am missing?
change
$Workfrom = Get-Content "C:\Webserverlogfiles\ise*.log"
Foreach ($Line in (Get-Content $Workfrom)
to
$Workfrom = Get-Content "C:\Webserverlogfiles\ise*.log"
Foreach ($Line in $Workfrom) {
$workfrom is already the lines of text. Otherwise, perhaps you meant Get-ChildItem in the first line?
I'm currently trying to make a function that gets all possible combinations of array values.
I have come up with a non function version but it's limited to 3 values so i'm trying to make a function out of it to become more Dynamic
I tried searching SO but could not find a powershell example of what i was trying to do, i could find a PHP version but i'm very limited in my PHP
PHP: How to get all possible combinations of 1D array?
Non-function Script
$name = 'First','Middle','Last'
$list = #()
foreach ($c1 in $name) {
foreach ($c2 in $name) {
foreach ($c3 in $name) {
if (($c1 -ne $c2) -and ($c2 -ne $c3) -and ($c3 -ne $c1))
{
$list += "$c1 $c2 $c3"
}
}
}
}
This gives me the result
First Middle Last
First Last Middle
Middle First Last
Middle Last First
Last First Middle
Last Middle First
I'm not sure how i would rearrange the values when i'm recursing the function, this is what i have so far:
<#
.Synopsis
Short description
.DESCRIPTION
Long description
.EXAMPLE
Example of how to use this cmdlet
.EXAMPLE
Another example of how to use this cmdlet
#>
function Get-Combinations
{
[CmdletBinding()]
[OutputType([int])]
Param
(
# Param1 help description
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
[string[]]$Array,
# Param1 help description
[Parameter(Mandatory=$false,
ValueFromPipelineByPropertyName=$false,
Position=1)]
[string]$Temp,
# Param1 help description
[Parameter(Mandatory=$false,
ValueFromPipelineByPropertyName=$true,
Position=2)]
[string[]]$Return
)
Begin
{
Write-Verbose "Starting Function Get-Combinations with parameters `n`n$($Array | Out-String)`n$temp`n`n$($Return | Out-String)"
If ($Temp)
{
$Return = $Temp
}
$newArray = new-object system.collections.arraylist
}
Process
{
Write-Verbose ($return | Out-String)
For($i=0; $i -lt $Array.Length; $i++)
{
#Write-Verbose $i
$Array | ForEach-Object {$newArray.Add($_)}
$newArray.RemoveAt($i)
Write-Verbose ($newArray | Out-String)
if ($newArray.Count -le 1)
{
Get-Combinations -Array $newArray -Temp $Temp -Return $Return
}
else
{
$Return = $Temp
}
}
$newArray
}
End
{
Write-Verbose "Exiting Function Get-Combinations"
}
}
$combinations = #("First","First2","Middle","Last")
$Combos = Get-Combinations -Array $combinations
$Combos
But the output i'm getting is all over the place
First2
Last
First2
Last
First
First2
Middle
Last
First
First2
Middle
Last
28/08 Update
Getting closer but still getting weird output
<#
.Synopsis
Short description
.DESCRIPTION
Long description
.EXAMPLE
Example of how to use this cmdlet
.EXAMPLE
Another example of how to use this cmdlet
#>
function Get-Combinations
{
[CmdletBinding()]
[OutputType([int])]
Param
(
# Param1 help description
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
[string[]]$Array,
# Param1 help description
[Parameter(Mandatory=$false,
ValueFromPipelineByPropertyName=$false,
Position=1)]
[string]$Temp,
# Param1 help description
[Parameter(Mandatory=$false,
ValueFromPipelineByPropertyName=$true,
Position=2)]
[string[]]$Return
)
Begin
{
Write-Verbose "Starting Function Get-Combinations with parameters `n`n$($Array | Out-String)`n$temp`n`n$($Return | Out-String)"
If ($Temp)
{
$Return += $Temp
}
#$newArray = new-object [System.Collections.ArrayList]
#$Array | ForEach-Object {$newArray.Add($_) | Out-Null}
[System.Collections.ArrayList]$newArray = $Array
}
Process
{
Write-Verbose "return -> $return"
For($i=0; $i -lt $Array.Length; $i++)
{
Write-Verbose "`$i -> $i"
$element = $newArray[0]
$newArray.RemoveAt(0)
Write-Verbose "`$newArray -> $newArray"
Write-Verbose "Element -> $element"
if ($newArray.Count -gt 0)
{
Get-Combinations -Array $newArray -Temp (($temp + " " +$element).Trim()) -Return $Return
}
else
{
$Return = $Temp + " " + $element
}
}
$return
}
End
{
Write-Verbose "Exiting Function Get-Combinations"
}
}
$combinations = #("First","First2","Middle","Last")
$return = #()
$Combos = Get-Combinations -Array $combinations -Return $return
$Combos
New output (Yes there is a space before the 'Last' value, no i have no idea why)
First First2 Middle Last
First First2 Last
First Middle Last
First Last
First2 Middle Last
First2 Last
Middle Last
Last
Here is my solution:
function Remove ($element, $list)
{
$newList = #()
$list | % { if ($_ -ne $element) { $newList += $_} }
return $newList
}
function Append ($head, $tail)
{
if ($tail.Count -eq 0)
{ return ,$head }
$result = #()
$tail | %{
$newList = ,$head
$_ | %{ $newList += $_ }
$result += ,$newList
}
return $result
}
function Permute ($list)
{
if ($list.Count -eq 0)
{ return #() }
$list | %{
$permutations = Permute (Remove $_ $list)
return Append $_ $permutations
}
}
cls
$list = "x", "y", "z", "t", "v"
$permutations = Permute $list
$permutations | %{
Write-Host ([string]::Join(", ", $_))
}
EDIT: the same in one function (Permute). This is cheating a bit, however since I replaced plain functions whith lambdas. You could replace recursive calls with a stack you handle yourself, but that would make the code unecessarily complex ...
function Permute ($list)
{
$global:remove = {
param ($element, $list)
$newList = #()
$list | % { if ($_ -ne $element) { $newList += $_} }
return $newList
}
$global:append = {
param ($head, $tail)
if ($tail.Count -eq 0)
{ return ,$head }
$result = #()
$tail | %{
$newList = ,$head
$_ | %{ $newList += $_ }
$result += ,$newList
}
return $result
}
if ($list.Count -eq 0)
{ return #() }
$list | %{
$permutations = Permute ($remove.Invoke($_, $list))
return $append.Invoke($_, $permutations)
}
}
cls
$list = "x", "y", "z", "t"
$permutations = Permute $list
$permutations | %{
Write-Host ([string]::Join(", ", $_))
}
I tried to learn something new and help you out but Im stuck. maybe this will help you get in the right direction but I dont know enough about Powershell recursion to figure this out. I converted the php to powershell and in theory it should work but it doesnt.
$array = #('Alpha', 'Beta', 'Gamma', 'Sigma')
function depth_picker([system.collections.arraylist]$arr,$temp_string, $collect)
{
if($temp_string -ne ""){$collect += $temp_string}
for($i = 0; $i -lt $arr.count;$i++)
{
[system.collections.arraylist]$arrCopy = $arr
$elem = $arrCopy[$i]
$arrCopy.removeRange($i,1)
if($arrCopy.count -gt 0){
depth_picker -arr $arrCopy -temp_string "$temp_string $elem" -collect $collect}
else{$collect += "$temp_string $elem"}
}
}
$collect = #()
depth_picker -arr $array -temp_string "" -collect $collect
$collect
It seems to work and will get you the first set of possibles:
Alpha
Alpha Beta
Alpha Beta Gamma
Alpha Beta Gamma Sigma
But for some reason that I cant figure out when it gets back to the previous functions and does $i++ then checks ($i -lt $arr.count) $arr.count it always 0 so it never goes to the next iteration to continue finding the possibilities.
Hopefully someone else can fix what I cant seem to figure out as I dont know enough about recursion. But it seems that with each level of depth called the previous depth level $arr variable and values is lost.
Here is my solution with a recursive function. It generates space separated strings but it's quite simple to split each element with $list[$i].split(" "):
function Get-Permutations
{
param ($array, $cur, $depth, $list)
$depth ++
for ($i = 0; $i -lt $array.Count; $i++)
{
$list += $cur+" "+$array[$i]
if ($depth -lt $array.Count)
{
$list = Get-Permutations $array ($cur+" "+$array[$i]) $depth $list
}
}
$list
}
$array = #("first","second","third","fourth")
$list = #()
$list = Get-Permutations $array "" 0 $list
$list
The solution posted by Micky Balladelli almost worked for me. Here is a version that does not duplicate values:
Function Get-Permutations
{
param ($array_in, $current, $depth, $array_out)
$depth++
$array_in = $array_in | select -Unique
for ($i = 0; $i -lt $array_in.Count; $i++)
{
$array_out += ($current+" "+$array_in[$i]).Trim()
if ($depth -lt $array_in.Count)
{
$array_out = Get-Permutations $array_in ($current+" "+$array_in[$i]) $depth $array_out
}
else {}
}
if(!($array_out -contains ($array_in -Join " "))) {}
for ($i = 0; $i -lt $array_out.Count; $i++)
{
$array_out[$i] = (($array_out[$i].Split(" ")) | select -Unique) -Join " "
}
$array_out | select -Unique
}