├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── CI ├── Assert.ps1 ├── CI.ps1 ├── InstallPowerShell.ps1 └── Publish.ps1 ├── Examples └── Join-Object.Examples.ps1 ├── Install.ps1 ├── Join-Object.ps1 ├── Join-Object.psd1 ├── Join-Object.psm1 ├── LICENSE ├── README.md ├── Tests ├── CompareData │ ├── DataSetSmall, DataTable - PSCustomObject. AllInBoth. DataTable.xml │ ├── DataSetSmall, DataTable - PSCustomObject. AllInBoth.xml │ ├── DataSetSmall, DataTable - PSCustomObject. DataTable. (DataTableTypes).xml │ ├── DataSetSmall, DataTable - PSCustomObject. DataTable. (Error, Mismatch).xml │ ├── DataSetSmall, DataTable - PSCustomObject. DataTable.xml │ ├── DataSetSmall, DataTable - PSCustomObject. OnlyIfInBoth. DataTable.xml │ ├── DataSetSmall, DataTable - PSCustomObject. OnlyIfInBoth.xml │ ├── DataSetSmall, DataTable - PSCustomObject. PassThru. (DataTableTypes).xml │ ├── DataSetSmall, DataTable - PSCustomObject. PassThru.xml │ ├── DataSetSmall, DataTable - PSCustomObject.xml │ ├── DataSetSmall, DataTable - PSCustomObjectJunk. DataTable. (AllowColumnsMerging).xml │ ├── DataSetSmall, DataTable - PSCustomObjectJunk. PassThru. (AllowColumnsMerging).xml │ ├── DataSetSmall, DataTable - PSCustomObjectKeyArray. (Comparer).xml │ ├── DataSetSmall, DataTable - PSCustomObjectMulti. (SubArray).xml │ ├── DataSetSmall, DataTable - [DataTable]PSCustomObjectJunk. PassThru. (AllowColumnsMerging).xml │ ├── DataSetSmall, DataTableMulti - PSCustomObjectMulti. AllInBoth. (SubGroups).xml │ ├── DataSetSmall, DataTableMulti - PSCustomObjectMulti. AllInBoth. DataTable. (DuplicateLines).xml │ ├── DataSetSmall, DataTableMulti - PSCustomObjectMulti. AllInBoth. DataTable. (SubGroups).xml │ ├── DataSetSmall, DataTableMulti - PSCustomObjectMulti. AllInLeft. PassThru. (SubGroups).xml │ ├── DataSetSmall, DataTableMulti - [DataTable]PSCustomObjectMulti. AllInLeft. PassThru. (SubGroups).xml │ ├── DataSetSmall, PSCustomObject - DataTable. (DBNull to $null).xml │ ├── DataSetSmall, PSCustomObject - DataTable. (Error, Mismatch).xml │ ├── DataSetSmall, PSCustomObject - DataTable. (JoinScript String).xml │ ├── DataSetSmall, PSCustomObject - DataTable. (JoinScript).xml │ ├── DataSetSmall, PSCustomObject - DataTable. (KeepRightJoinProperty).xml │ ├── DataSetSmall, PSCustomObject - DataTable. (Multi Join).xml │ ├── DataSetSmall, PSCustomObject - DataTable. (Ordered).xml │ ├── DataSetSmall, PSCustomObject - DataTable. AllInBoth. DataTable.xml │ ├── DataSetSmall, PSCustomObject - DataTable. AllInBoth.xml │ ├── DataSetSmall, PSCustomObject - DataTable. DataTable.xml │ ├── DataSetSmall, PSCustomObject - DataTable. OnlyIfInBoth. DataTable.xml │ ├── DataSetSmall, PSCustomObject - DataTable. OnlyIfInBoth.xml │ ├── DataSetSmall, PSCustomObject - DataTable. PassThru.xml │ ├── DataSetSmall, PSCustomObject - DataTable.xml │ ├── DataSetSmall, PSCustomObjectMulti - DataTable. (SubArray).xml │ ├── DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInBoth. (SubGroups Key).xml │ ├── DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInBoth. (SubGroups).xml │ ├── DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInBoth. DataTable. (DuplicateLines).xml │ ├── DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInBoth. DataTable. (SubGroups Key).xml │ ├── DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInBoth. DataTable. (SubGroups).xml │ ├── DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInLeft. PassThru. (SubGroups).xml │ ├── DataSetSmall, PSCustomObjectMulti - [PSCustomObject]DataTableMulti. AllInLeft. PassThru. (SubGroups).xml │ ├── DataSetSmall, PSCustomObject[0] - DataTable. (Single).xml │ └── DataSetSmall, [Collections.ArrayList]PSCustomObject - DataTable.xml ├── Join-Object.Tests.Big.ps1 ├── Join-Object.Tests.ps1 ├── TestDataSetBig100K.db ├── TestDataSetBig10k.db └── TestHelpers.ps1 ├── appveyor.yml ├── azure-pipelines.yml └── morelinq.3.2.0 ├── .signature.p7s ├── COPYING.txt ├── lib ├── net451 │ ├── MoreLinq.dll │ ├── MoreLinq.pdb │ └── MoreLinq.xml ├── netstandard1.0 │ ├── MoreLinq.dll │ ├── MoreLinq.pdb │ └── MoreLinq.xml └── netstandard2.0 │ ├── MoreLinq.dll │ ├── MoreLinq.pdb │ └── MoreLinq.xml └── morelinq.3.2.0.nupkg /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/.gitignore -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "PowerShell", 9 | "request": "launch", 10 | "name": "PowerShell Pester Tests", 11 | "script": "Import-Module -Name '.\\' -Force ; Invoke-Pester", // Change to '.\\ModuleName.psd1' if Git name different 12 | "args": [""], 13 | "cwd": "${workspaceFolder}" 14 | }, 15 | { 16 | "type": "PowerShell", 17 | "request": "launch", 18 | "name": "PowerShell Pester Tests Big", 19 | "script": "Import-Module -Name '.\\' -Force ; Invoke-Pester -Script .\\Tests\\Join-Object.Tests.Big.ps1", 20 | "args": [ 21 | "" 22 | ], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "PowerShell", 27 | "request": "launch", 28 | "name": "PowerShell Launch Current File", 29 | "script": "${file}", 30 | "args": [], 31 | "cwd": "${file}" 32 | }, 33 | { 34 | "type": "PowerShell", 35 | "request": "launch", 36 | "name": "PowerShell Launch Current File in Temporary Console", 37 | "script": "${file}", 38 | "args": [], 39 | "cwd": "${file}", 40 | "createTemporaryIntegratedConsole": true 41 | }, 42 | { 43 | "type": "PowerShell", 44 | "request": "launch", 45 | "name": "PowerShell Launch Current File w/Args Prompt", 46 | "script": "${file}", 47 | "args": [ 48 | "${command:SpecifyScriptArgs}" 49 | ], 50 | "cwd": "${file}" 51 | }, 52 | { 53 | "type": "PowerShell", 54 | "request": "attach", 55 | "name": "PowerShell Attach to Host Process", 56 | "processId": "${command:PickPSHostProcess}", 57 | "runspaceId": 1 58 | }, 59 | { 60 | "type": "PowerShell", 61 | "request": "launch", 62 | "name": "PowerShell Interactive Session", 63 | "cwd": "" 64 | } 65 | ] 66 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "CSVs", 4 | "Clixml", 5 | "Cmdlet", 6 | "Computername", 7 | "Deserialize", 8 | "Deserialized", 9 | "HKLM", 10 | "Hashtable", 11 | "Inno", 12 | "LINQ", 13 | "Nuget", 14 | "PSSQ", 15 | "Recurse", 16 | "SSNs", 17 | "Scriptblock", 18 | "Serializer", 19 | "Silveira's", 20 | "Sqlite", 21 | "VARCHAR", 22 | "Veyor", 23 | "adddays", 24 | "birthdate", 25 | "endregion", 26 | "gmail", 27 | "isnot", 28 | "jsmith", 29 | "morelinq", 30 | "msiexec", 31 | "netstandard", 32 | "notin", 33 | "notlike", 34 | "notmatch", 35 | "prepend", 36 | "setvariable", 37 | "unencrypted" 38 | ] 39 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.3 - 2022/04/02 (Stable) 2 | ### Fixed 3 | * [Issues #16](https://github.com/ili101/Join-Object/issues/16) - Fixed escape error bug when not using `-DataTable`, and `-AddKey` parameter contains a spaces (thank you [franklesniak](https://github.com/franklesniak) for reporting). 4 | * Fixed escape error bug when using `-DataTable`, and `-AddKey` parameter contains single quotes. 5 | 6 | ## 2.0.2 - 2020/11/09 (Stable) 7 | ### New 8 | * Add support for [ArrayList] input in addition for [Array] And [DataTable]. 9 | 10 | ## 2.0.1 - 2019/08/24 (Stable) 11 | ### Documentation 12 | * Updated Description and Tags. 13 | 14 | ## 2.0.0 - 2019/08/22 (Stable) 15 | ### New 16 | * PowerShell Core 7 is now supported! 17 | * `-LeftJoinScript` and `-RightJoinScript` now support non string output (String is still used if scriptblock provided and not Func\`2). 18 | * `-Comparer` allow use of custom [EqualityComparer]. 19 | ### Fixed 20 | * Fixed DuplicateLines with non DataTable output can create output with "hidden" sub arrays. Linq Join seems to not unroll the output so for example you can get this output: 21 | ``` 22 | IDD Name Junk IntT R_Sub R_IntO 23 | --- ---- ---- ---- ----- ------ 24 | 1 A AAA 5 S1 6 25 | 1 A AAA 5 S12 62 26 | 3 C S3 27 | 4 D 28 | ``` 29 | It looks like an array with 4 lines of PSCustomObjects but it's actually an array with 3 lines as the first line is actually an array containing the first 2 lines. This will be unrolled in the new version to an array with 4 PSCustomObjects. :warning: breaking change. 30 | * PowerShell Core 7: Join-Object SubGroups [EmptyPartition`1] Fix. 31 | * `-PassThru` will now throw an error when used with `-Type OnlyIfInBoth` as lines cannot be removed, and with `-Type AllInLeft` + `-RightMultiMode DuplicateLines` as lines cannot be duplicated. :warning: breaking change. 32 | ### Changed 33 | `-AddKey` is now a String that takes the name of the key column to create. :warning: breaking change. 34 | ### Improved 35 | * `/Examples/Join-Object.Examples.ps1` was updated. 36 | ### Updated 37 | * MoreLinq Updated to 3.2.0. 38 | ### Testing 39 | * Update To Module.Template 2.0.1, Test now use Azure pipeline Windows 2019 and AppVeyor on Ubuntu1804 and Windows 2019 (PowerShell Framework and core 7.0.0 Preview 3). 40 | * Update to custom version of Assert 0.9.5. 41 | * PowerShell Core 7: Test 1 ExpectedErrorMessage Fix. 42 | * PowerShell Core 7: Tests PSCustomObject.Count -eq 1 Fix. 43 | ### Code Cleanup 44 | * Change format to VSCode default on all files. 45 | * cSpell.words updated and spelling corrections. 46 | * Refactor Tests. 47 | * Refactor Join-Object. 48 | 49 | ## 1.0.1 - 2018/12/17 (Stable) 50 | ### Added 51 | * **-AllowColumnsMerging** Allow duplicate columns in the Left and Right Objects, will overwrite the conflicting Left data with the Right data (Ignoring Nulls), Supported only on DataTable output for now. 52 | 53 | ## 1.0.0 - 2018/11/20 (Stable) 54 | ### Improved 55 | * Major rewrite of the code, The main Scriptblock is now dynamically constructed. 56 | * Error handling with $PSCmdlet.ThrowTerminatingError. 57 | ### Fixed 58 | * Undo "Fix JoinFunction scope to support JoinProperty" (Unnecessary feature). 59 | ### Added 60 | * **-LeftMultiMode** and **-RightMultiMode** with options ('SingleOnly', 'DuplicateLines', 'SubGroups'). 61 | * **-AddKey** can be used with "-Type AllInBoth" to add a column named "Key" containing the joining key. 62 | ### Removed 63 | * **-MultiLeft** replaced by MultiMode. 64 | * **-RightAsGroup** replaced by MultiMode. 65 | 66 | :warning: if you used the GitHub (Beta branch) version there are breaking changes. 67 | 68 | ## 0.1.8 - 2018/11/07 (Beta branch) 69 | ### Updated 70 | * MoreLinq updated to 3.0.0. 71 | ### Fixed 72 | * Fix Multi JoinProperty comparing column name. 73 | * Fix JoinFunction scope to support JoinProperty. 74 | * Minor bug fixes and improvements. 75 | 76 | ## 0.1.7 - 2018/03/13 (Beta branch) 77 | ### Added 78 | * **-Type AllInBoth** option. 79 | * **-DataTableTypes [Hashtable]** allow Overwrite of DataTable columns types. 80 | * **-RightAsGroup [String]** Join the right side as sub table in column with the selected name. 81 | * **-MultiLeft** allow multiple rows on the left side. 82 | * **-KeepRightJoinProperty** don't remove the right join property. 83 | ### Fixed 84 | * Remove unused parameter option **-Type AllInRight** 85 | 86 | ## 0.1.6 - 2018/03/13 (Beta) 87 | ### Fixed 88 | * Error "Cannot set Column 'foo' to be null. Please use DBNull instead." when using -DataTable and -AllInLeft and some Left lines don't have Right lines to join to. 89 | 90 | ## 0.1.5 - 2018/03/11 (Beta) 91 | ### Fixed 92 | * Error "Cannot set Column 'foo' to be null. Please use DBNull instead." when using -DataTable on DataTable data with nulls in it. 93 | 94 | ## 0.1.4 - 2018/03/08 (Beta) 95 | ### Added 96 | * **-DataTable** parameter to output as "DataTable". 97 | ### Fixed 98 | * Output when using PassThru on "-Left DataTable" was returning "Array" with "DataRow"s instead of "DataTable" with "DataRow"s Object. 99 | 100 | ## 0.1.2 - 2018/03/05 (Beta) 101 | ### Added 102 | * **-Type** parameter that supports "AllInLeft" and "OnlyIfInBoth". 103 | * **-RightJoinScript** and **-LeftJoinScript** parameters to support custom joining scripts. 104 | * **-RightJoinProperty** and **-LeftJoinProperty** now supports multiple Properties (String Array) to join on multiple columns. 105 | 106 | ## 0.1.1 - 2017-09-19 (Beta) 107 | ### Added 108 | * Convert DBNull to $null when going from DataTable to PSCustomObject. 109 | 110 | ## 0.1.0 - 2017-09-19 (Beta) 111 | * Join-Object initial release. 112 | 113 | ## 0.0.0 - 2017-07-28 (Alpha) 114 | * Join-Object LINQ Edition concept code. -------------------------------------------------------------------------------- /CI/Assert.ps1: -------------------------------------------------------------------------------- 1 | # Fix Could not create SSL/TLS secure channel 2 | #$SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol 3 | #[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 4 | $Branch = 'Test2' #'master' 5 | $Repository = 'Assert' 6 | $PathZip = Join-Path ([IO.Path]::GetTempPath()) "$Repository-$Branch.zip" 7 | $PathPsd1 = Join-Path ([IO.Path]::GetTempPath()) "$Repository-$Branch" | Join-Path -ChildPath "$Repository.psd1" 8 | 9 | if (!(Test-Path $PathPsd1)) { 10 | Invoke-WebRequest "https://github.com/ili101/$Repository/archive/$Branch.zip" -OutFile $PathZip 11 | #[Net.ServicePointManager]::SecurityProtocol = $SecurityProtocol 12 | Expand-Archive -Path $PathZip -DestinationPath ([IO.Path]::GetTempPath()) 13 | } 14 | Import-Module -Name $PathPsd1 -------------------------------------------------------------------------------- /CI/CI.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Handel Continuous Integration Testing in AppVeyor and Azure DevOps Pipelines. 4 | #> 5 | param 6 | ( 7 | # AppVeyor Only - Update AppVeyor build name. 8 | [Switch]$Initialize, 9 | # Installs the module and invoke the Pester tests with the current version of PowerShell. 10 | [Switch]$Test, 11 | # AppVeyor Only - Upload results to AppVeyor "Tests" tab. 12 | [Switch]$Finalize, 13 | # AppVeyor and Azure - Upload module as AppVeyor Artifact. 14 | [Switch]$Artifact, 15 | # Azure - Runs PsScriptAnalyzer against one or more folders and pivots the results to form a report. 16 | [Switch]$Analyzer 17 | ) 18 | $ErrorActionPreference = 'Stop' 19 | if ($Initialize) { 20 | $Psd1 = (Get-ChildItem -File -Filter *.psd1 -Name -Path (Split-Path $PSScriptRoot)).PSPath 21 | $ModuleVersion = (. ([Scriptblock]::Create((Get-Content -Path $Psd1 | Out-String)))).ModuleVersion 22 | Update-AppveyorBuild -Version "$ModuleVersion ($env:APPVEYOR_BUILD_NUMBER) $env:APPVEYOR_REPO_BRANCH" 23 | } 24 | if ($Test) { 25 | function Get-EnvironmentInfo { 26 | if ($null -eq $IsWindows -or $IsWindows) { 27 | # Get Windows Version 28 | try { 29 | $WinRelease, $WinVer = Get-ItemPropertyValue "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" ReleaseId, CurrentMajorVersionNumber, CurrentMinorVersionNumber, CurrentBuildNumber, UBR 30 | $WindowsVersion = "$($WinVer -join '.') ($WinRelease)" 31 | } 32 | catch { 33 | $WindowsVersion = [System.Environment]::OSVersion.Version 34 | } 35 | 36 | # Get .Net Version 37 | # https://stackoverflow.com/questions/3487265/powershell-script-to-return-versions-of-net-framework-on-a-machine 38 | $Lookup = @{ 39 | 378389 = [version]'4.5' 40 | 378675 = [version]'4.5.1' 41 | 378758 = [version]'4.5.1' 42 | 379893 = [version]'4.5.2' 43 | 393295 = [version]'4.6' 44 | 393297 = [version]'4.6' 45 | 394254 = [version]'4.6.1' 46 | 394271 = [version]'4.6.1' 47 | 394802 = [version]'4.6.2' 48 | 394806 = [version]'4.6.2' 49 | 460798 = [version]'4.7' 50 | 460805 = [version]'4.7' 51 | 461308 = [version]'4.7.1' 52 | 461310 = [version]'4.7.1' 53 | 461808 = [version]'4.7.2' 54 | 461814 = [version]'4.7.2' 55 | 528040 = [version]'4.8' 56 | 528049 = [version]'4.8' 57 | } 58 | 59 | # For One True framework (latest .NET 4x), change the Where-Object match 60 | # to PSChildName -eq "Full": 61 | $DotNetVersion = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -Recurse | 62 | Get-ItemProperty -name Version, Release -EA 0 | 63 | Where-Object { $_.PSChildName -eq "Full" } | 64 | Select-Object @{name = ".NET Framework"; expression = { $_.PSChildName } }, 65 | @{name = "Product"; expression = { $Lookup[$_.Release] } }, 66 | Version, Release 67 | 68 | # Output 69 | [PSCustomObject]($PSVersionTable + @{ 70 | ComputerName = $env:Computername 71 | WindowsVersion = $WindowsVersion 72 | '.Net Version' = '{0} (Version: {1}, Release: {2})' -f $DotNetVersion.Product, $DotNetVersion.Version, $DotNetVersion.Release 73 | #EnvironmentPath = $env:Path 74 | }) 75 | } 76 | else { 77 | # Output 78 | [PSCustomObject]($PSVersionTable + @{ 79 | ComputerName = $env:Computername 80 | #EnvironmentPath = $env:Path 81 | }) 82 | } 83 | } 84 | 85 | '[Info] Testing On:' 86 | Get-EnvironmentInfo 87 | '[Progress] Installing Module.' 88 | . .\Install.ps1 89 | '[Progress] Invoking Pester.' 90 | Invoke-Pester -OutputFile ('TestResultsPS{0}.xml' -f $PSVersionTable.PSVersion) 91 | } 92 | if ($Finalize) { 93 | '[Progress] Finalizing.' 94 | $Failure = $false 95 | $AppVeyorResultsUri = 'https://ci.appveyor.com/api/testresults/nunit/{0}' -f $env:APPVEYOR_JOB_ID 96 | foreach ($TestResultsFile in Get-ChildItem -Path 'TestResultsPS*.xml') { 97 | $TestResultsFilePath = $TestResultsFile.FullName 98 | "[Info] Uploading Files: $AppVeyorResultsUri, $TestResultsFilePath." 99 | # Add PowerShell version to test results 100 | $PSVersion = $TestResultsFile.Name.Replace('TestResults', '').Replace('.xml', '') 101 | [Xml]$Xml = Get-Content -Path $TestResultsFilePath 102 | Select-Xml -Xml $Xml -XPath '//test-case' | ForEach-Object { $_.Node.name = "$PSVersion " + $_.Node.name } 103 | $Xml.OuterXml | Out-File -FilePath $TestResultsFilePath 104 | 105 | #Invoke-RestMethod -Method Post -Uri $AppVeyorResultsUri -Body $Xml 106 | [Net.WebClient]::new().UploadFile($AppVeyorResultsUri, $TestResultsFilePath) 107 | 108 | if ($Xml.'test-results'.failures -ne '0') { 109 | $Failure = $true 110 | } 111 | } 112 | if ($Failure) { 113 | throw 'Tests failed.' 114 | } 115 | } 116 | if ($Artifact) { 117 | # Get Module Info 118 | $ModuleName = [System.IO.Path]::GetFileNameWithoutExtension((Get-ChildItem -File -Filter *.psm1 -Name -Path (Split-Path $PSScriptRoot))) 119 | $ModulePath = (Get-Module -Name $ModuleName -ListAvailable).ModuleBase | Split-Path 120 | $VersionLocal = ((Get-Module -Name $ModuleName -ListAvailable).Version | Measure-Object -Maximum).Maximum 121 | "[Progress] Artifact Start for Module: $ModuleName, Version: $VersionLocal." 122 | if ($env:APPVEYOR) { 123 | $ZipFileName = "{0} {1} {2} {3:yyyy-MM-dd HH-mm-ss}.zip" -f $ModuleName, $VersionLocal, $env:APPVEYOR_REPO_BRANCH, (Get-Date) 124 | $ZipFileFullPath = Join-Path -Path $PSScriptRoot -ChildPath $ZipFileName 125 | "[Info] Artifact. $ModuleName, ZipFileName: $ZipFileName." 126 | #Compress-Archive -Path $ModulePath -DestinationPath $ZipFileFullPath 127 | [System.IO.Compression.ZipFile]::CreateFromDirectory($ModulePath, $ZipFileFullPath, [System.IO.Compression.CompressionLevel]::Optimal, $true) 128 | Push-AppveyorArtifact $ZipFileFullPath -DeploymentName $ModuleName 129 | } 130 | elseif ($env:AGENT_NAME) { 131 | #Write-Host "##vso[task.setvariable variable=ModuleName]$ModuleName" 132 | Copy-Item -Path $ModulePath -Destination $env:Build_ArtifactStagingDirectory -Recurse 133 | } 134 | } 135 | if ($Analyzer) { 136 | if (!(Get-Module -Name PSScriptAnalyzer -ListAvailable)) { 137 | '[Progress] Installing PSScriptAnalyzer.' 138 | Install-Module -Name PSScriptAnalyzer -Force 139 | } 140 | 141 | if ($env:System_PullRequest_TargetBranch) { 142 | '[Progress] Get target branch.' 143 | $TempGitClone = Join-Path ([IO.Path]::GetTempPath()) (New-Guid) 144 | Copy-Item -Path $PWD -Destination $TempGitClone -Recurse 145 | (Get-Item (Join-Path $TempGitClone '.git')).Attributes += 'Hidden' 146 | "[Progress] git clean." 147 | git -C $TempGitClone clean -f 148 | "[Progress] git reset." 149 | git -C $TempGitClone reset --hard 150 | "[Progress] git checkout." 151 | git -C $TempGitClone checkout -q $env:System_PullRequest_TargetBranch 152 | 153 | $DirsToProcess = @{ 'Pull Request' = $PWD ; $env:System_PullRequest_TargetBranch = $TempGitClone } 154 | } 155 | else { 156 | $DirsToProcess = @{ 'GitHub' = $PWD } 157 | } 158 | 159 | "[Progress] Running Script Analyzer." 160 | $AnalyzerResults = $DirsToProcess.GetEnumerator() | ForEach-Object { 161 | $DirName = $_.Key 162 | Write-Verbose "[Progress] Running Script Analyzer on $DirName." 163 | Invoke-ScriptAnalyzer -Path $_.Value -Recurse -ErrorAction SilentlyContinue | 164 | Add-Member -MemberType NoteProperty -Name Location -Value $DirName -PassThru 165 | } 166 | 167 | if ($AnalyzerResults) { 168 | if (!(Get-Module -Name ImportExcel -ListAvailable)) { 169 | '[Progress] Installing ImportExcel.' 170 | Install-Module -Name ImportExcel -Force 171 | } 172 | '[Progress] Creating ScriptAnalyzer.xlsx.' 173 | $ExcelParams = @{ 174 | Path = 'ScriptAnalyzer.xlsx' 175 | WorksheetName = 'FullResults' 176 | Now = $true 177 | Activate = $true 178 | Show = $false 179 | } 180 | $PivotParams = @{ 181 | PivotTableName = 'BreakDown' 182 | PivotData = @{RuleName = 'Count' } 183 | PivotRows = 'Severity', 'RuleName' 184 | PivotColumns = 'Location' 185 | PivotTotals = 'Rows' 186 | } 187 | Remove-Item -Path $ExcelParams['Path'] -ErrorAction SilentlyContinue 188 | 189 | $PivotParams['PivotChartDefinition'] = New-ExcelChartDefinition -ChartType 'BarClustered' -Column (1 + $DirsToProcess.Count) -Title "Script analysis" -LegendBold 190 | $ExcelParams['PivotTableDefinition'] = New-PivotTableDefinition @PivotParams 191 | 192 | $AnalyzerResults | Export-Excel @ExcelParams 193 | '[Progress] Analyzer finished.' 194 | } 195 | else { 196 | "[Info] Invoke-ScriptAnalyzer didn't return any problems." 197 | } 198 | } -------------------------------------------------------------------------------- /CI/InstallPowerShell.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Installs PowerShell Core on Windows. 4 | #> 5 | [CmdLetBinding()] 6 | Param 7 | ( 8 | # Version to install in the format from the .msi, for example "7.0.0-preview.1" 9 | [Parameter(Mandatory)] 10 | [String]$Version 11 | ) 12 | $ErrorActionPreference = 'Stop' 13 | 14 | '[Progress] Downloading PowerShell Core.' 15 | $MsiPath = Join-Path $env:TEMP "PowerShell-$Version-win-x64.msi" 16 | [System.Net.WebClient]::new().DownloadFile("https://github.com/PowerShell/PowerShell/releases/download/v$Version/PowerShell-$Version-win-x64.msi", $MsiPath) 17 | 18 | '[Progress] Installing PowerShell Core.' 19 | Start-Process 'msiexec.exe' -Wait -ArgumentList "/i $MsiPath /quiet" 20 | Remove-Item -Path $MsiPath 21 | $PowerShellFolder = $Version[0] 22 | if ($Version -like "*preview*") { 23 | $PowerShellFolder += '-preview' 24 | } 25 | $env:Path = "$env:ProgramFiles\PowerShell\$PowerShellFolder;$env:Path" 26 | '[Progress] PowerShell Core Installed.' -------------------------------------------------------------------------------- /CI/Publish.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Deploy module to PowerShellGallery. 4 | #> 5 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "Success")] 6 | [CmdletBinding(DefaultParameterSetName = 'ModuleName')] 7 | Param 8 | ( 9 | # The name of the installed module to be deployed, if not provided the name of the .psm1 file in the parent folder is used. 10 | [Parameter(ParameterSetName = 'ModuleName')] 11 | [ValidateNotNullOrEmpty()] 12 | [String]$ModuleName, 13 | 14 | # Publish module from path (module folder), if not provided -ModuleName is used. 15 | [Parameter(Mandatory, ParameterSetName = 'Path')] 16 | [ValidateNotNullOrEmpty()] 17 | [String]$Path, 18 | 19 | # Key for PowerShellGallery deployment, if not provided $env:NugetApiKey is used. 20 | [ValidateNotNullOrEmpty()] 21 | [String]$NugetApiKey, 22 | 23 | # Skip Version verification for PowerShellGallery deployment, can be used for first release. 24 | [Switch]$Force 25 | ) 26 | $ErrorActionPreference = 'Stop' 27 | 28 | if ($Path) { 29 | $Path = Resolve-Path -Path $Path 30 | if ($Path.Count -ne 1) { 31 | throw ('Invalid Path, $Path.Count: {0}.' -f $Path.Count) 32 | } 33 | $Psd1Path = (Get-ChildItem -File -Filter *.psd1 -Path $Path -Recurse)[0].FullName 34 | $ModuleName = [System.IO.Path]::GetFileNameWithoutExtension($Psd1Path) 35 | $VersionLocal = (. ([Scriptblock]::Create((Get-Content -Path $Psd1Path | Out-String)))).ModuleVersion 36 | } 37 | else { 38 | # Get Script Root 39 | if ($PSScriptRoot) { 40 | $ScriptRoot = $PSScriptRoot 41 | } 42 | elseif ($psISE.CurrentFile.IsUntitled -eq $false) { 43 | $ScriptRoot = Split-Path -Path $psISE.CurrentFile.FullPath 44 | } 45 | elseif ($null -ne $psEditor.GetEditorContext().CurrentFile.Path -and $psEditor.GetEditorContext().CurrentFile.Path -notlike 'untitled:*') { 46 | $ScriptRoot = Split-Path -Path $psEditor.GetEditorContext().CurrentFile.Path 47 | } 48 | else { 49 | $ScriptRoot = '.' 50 | } 51 | 52 | # Get Module Info 53 | if (!$ModuleName) { 54 | $ModuleName = [System.IO.Path]::GetFileNameWithoutExtension((Get-ChildItem -File -Filter *.psm1 -Name -Path (Split-Path $ScriptRoot))) 55 | } 56 | $VersionLocal = ((Get-Module -Name $ModuleName -ListAvailable).Version | Measure-Object -Maximum).Maximum 57 | } 58 | 59 | "[Progress] Deploy Script Start for Module: $ModuleName, Version: $VersionLocal." 60 | 61 | # Deploy to PowerShell Gallery if run locally OR from AppVeyor & GitHub master 62 | if (!$env:APPVEYOR -or $env:APPVEYOR_REPO_BRANCH -eq 'master') { 63 | if ($env:APPVEYOR) { 64 | $Success = $true 65 | $AppVeyorProject = Invoke-RestMethod -Uri "https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG" 66 | $AppVeyorProject.build.jobs | ForEach-Object { 67 | '[Info] AppVeyor job name: "{0}", Id: {1}, Status: {2}.' -f $_.name, $_.jobId, $_.status 68 | if ($_.jobId -ne $env:APPVEYOR_JOB_ID -and $_.status -ne "success") { 69 | $Success = $false 70 | } 71 | } 72 | if (!$Success) { 73 | '[Info] There are filed jobs skipping PowerShell Gallery deploy.' 74 | break 75 | } 76 | } 77 | try { 78 | $VersionGallery = (Find-Module -Name $ModuleName -ErrorAction Stop).Version 79 | } 80 | catch { 81 | if ($_.Exception.Message -notlike 'No match was found for the specified search criteria*' -or !$Force) { 82 | throw $_ 83 | } 84 | } 85 | 86 | "[Info] PowerShellGallery. $ModuleName, VersionGallery: $VersionGallery, VersionLocal: $VersionLocal." 87 | if ($VersionGallery -lt $VersionLocal -or $Force) { 88 | if (!$NugetApiKey) { 89 | $NugetApiKey = $env:NugetApiKey 90 | } 91 | "[Info] PowerShellGallery. Deploying $ModuleName version $VersionLocal." 92 | if ($Path) { 93 | Publish-Module -NuGetApiKey $NugetApiKey -Path $Path 94 | } 95 | else { 96 | Publish-Module -NuGetApiKey $NugetApiKey -Name $ModuleName -RequiredVersion $VersionLocal 97 | } 98 | } 99 | else { 100 | '[Info] PowerShellGallery Deploy Skipped (Version Check).' 101 | } 102 | } 103 | else { 104 | '[Info] PowerShellGallery Deploy Skipped.' 105 | } 106 | '[Progress] Deploy Ended.' -------------------------------------------------------------------------------- /Examples/Join-Object.Examples.ps1: -------------------------------------------------------------------------------- 1 | $ExampleData = { 2 | # Left Object Example 3 | $PSCustomObject = @( 4 | [PSCustomObject]@{ ID = 1 ; Sub = 'S1' ; IntO = 6 } 5 | [PSCustomObject]@{ ID = 2 ; Sub = 'S2' ; IntO = 7 } 6 | [PSCustomObject]@{ ID = 3 ; Sub = 'S3' ; IntO = $null } 7 | ) 8 | # Right Object Example (DataTable) 9 | $DataTable = [Data.DataTable]::new('Test') 10 | $null = $DataTable.Columns.Add('IDD', [System.Int32]) 11 | $null = $DataTable.Columns.Add('Name') 12 | $null = $DataTable.Columns.Add('Junk') 13 | $null = $DataTable.Columns.Add('IntT', [System.Int32]) 14 | $null = $DataTable.Rows.Add(1, 'foo', 'AAA', 123456) 15 | $null = $DataTable.Rows.Add(3, 'Bar', 'S3', $null) 16 | $null = $DataTable.Rows.Add(4, 'D', $null, $null) 17 | } 18 | 19 | . $ExampleData 20 | # Example 1: Join the 2 together ("Left Join" in this case). 21 | Join-Object -Left $PSCustomObject -Right $DataTable -LeftJoinProperty 'ID' -RightJoinProperty 'IDD' | Format-Table 22 | <# Output 23 | ID Sub IntO Name Junk IntT 24 | -- --- ---- ---- ---- ---- 25 | 1 S1 6 foo AAA 123456 26 | 2 S2 7 27 | 3 S3 Bar S3 28 | #> 29 | 30 | . $ExampleData 31 | # Example 2A: Filtering columns. 32 | $Params = @{ 33 | Left = $PSCustomObject 34 | Right = $DataTable 35 | LeftJoinProperty = 'ID' 36 | RightJoinProperty = 'IDD' 37 | ExcludeRightProperties = 'Junk' # Exclude column "Junk" from the right columns. 38 | Prefix = 'R_' # Add Prefix to the right columns. 39 | LeftProperties = 'ID', 'Sub' # Select columns to include from the right. 40 | } 41 | Join-Object @Params | Format-Table 42 | <# Output 43 | ID Sub R_Name R_IntT 44 | -- --- ------ ------ 45 | 1 S1 foo 123456 46 | 2 S2 47 | 3 S3 Bar 48 | #> 49 | 50 | # Example 2B: Filtering renaming and reordering columns. 51 | $Params['LeftProperties'] = [ordered]@{ Sub = 'Subscription' ; ID = 'ID' } # Select columns to include from the right, rename and reorder them. 52 | Join-Object @Params | Format-Table 53 | <# Output 54 | Subscription ID R_Name R_IntT 55 | ------------ -- ------ ------ 56 | S1 1 foo 123456 57 | S2 2 58 | S3 3 Bar 59 | #> 60 | 61 | . $ExampleData 62 | # Example 3: -Type. Options: AllInLeft (default), OnlyIfInBoth, AllInBoth. 63 | $Params = @{ 64 | Left = $PSCustomObject 65 | Right = $DataTable 66 | LeftJoinProperty = 'ID' 67 | RightJoinProperty = 'IDD' 68 | Type = 'OnlyIfInBoth' 69 | } 70 | Join-Object @Params | Format-Table 71 | <# Output 72 | ID Sub IntO Name Junk IntT 73 | -- --- ---- ---- ---- ---- 74 | 1 S1 6 foo AAA 123456 75 | 3 S3 Bar S3 76 | #> 77 | 78 | . $ExampleData 79 | # Example 4: Output format. (When input is [DataTable] containing [DBNull]s if output is [PSCustomObject] they will be converted to $null). 80 | $Params = @{ 81 | Left = $PSCustomObject 82 | Right = $DataTable 83 | LeftJoinProperty = 'ID' 84 | RightJoinProperty = 'IDD' 85 | DataTable = $true # By default output format is PSCustomObject this changes it to DataTable. 86 | } 87 | Join-Object @Params | Format-Table 88 | <# This is a DataTable 89 | ID Sub IntO Name Junk IntT 90 | -- --- ---- ---- ---- ---- 91 | 1 S1 6 foo AAA 123456 92 | 2 S2 7 93 | 3 S3 Bar S3 94 | #> 95 | 96 | . $ExampleData 97 | # Example 5: -PassThru. Editing the existing left object preserving it's existing type PSCustomObject/DataTable. 98 | $Params = @{ 99 | Left = $PSCustomObject 100 | Right = $DataTable 101 | LeftJoinProperty = 'ID' 102 | RightJoinProperty = 'IDD' 103 | PassThru = $true 104 | } 105 | $null = Join-Object @Params 106 | $PSCustomObject | Format-Table 107 | <# $PSCustomObject changed to: 108 | ID Sub IntO Name Junk IntT 109 | -- --- ---- ---- ---- ---- 110 | 1 S1 6 foo AAA 123456 111 | 2 S2 7 112 | 3 S3 Bar S3 113 | #> 114 | 115 | . $ExampleData 116 | # Example 6: JoinScript. Manipulate the JoinProperty for the comparison with a Scriptblock. 117 | $Params = @{ 118 | Left = $PSCustomObject 119 | Right = $DataTable 120 | LeftJoinProperty = 'Sub' 121 | RightJoinProperty = 'IDD' 122 | LeftJoinScript = { param ($Line) $Line.Sub.Replace('S', '')} # For example change "Sub" column value from "S1" to "1" to compare to "IDD" column "1". 123 | } 124 | Join-Object @Params | Format-Table 125 | <# Output 126 | ID Sub IntO Name Junk IntT 127 | -- --- ---- ---- ---- ---- 128 | 1 S1 6 foo AAA 123456 129 | 2 S2 7 130 | 3 S3 Bar S3 131 | #> 132 | 133 | . $ExampleData 134 | # Example 7: -AddKey. can be used with "-Type AllInBoth" to add a column containing the joining key. 135 | $Params = @{ 136 | Left = $PSCustomObject 137 | Right = $DataTable 138 | LeftJoinProperty = 'ID' 139 | RightJoinProperty = 'IDD' 140 | LeftProperties = 'Sub' 141 | Type = 'AllInBoth' 142 | AddKey = 'Index' 143 | } 144 | Join-Object @Params | Format-Table 145 | <# Output 146 | Index Sub Name Junk IntT 147 | ----- --- ---- ---- ---- 148 | 1 S1 foo AAA 123456 149 | 2 S2 150 | 3 S3 Bar S3 151 | 4 D 152 | #> -------------------------------------------------------------------------------- /Install.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Installs module from Git clone or directly from GitHub. 4 | File must not have BOM for GitHub deploy to work. 5 | #> 6 | [CmdletBinding(DefaultParameterSetName = 'Default')] 7 | Param ( 8 | # Path to install the module to, if not provided -Scope used. 9 | [Parameter(Mandatory, ParameterSetName = 'ModulePath')] 10 | [ValidateNotNullOrEmpty()] 11 | [String]$ModulePath, 12 | 13 | # Path to install the module to, PSModulePath "CurrentUser" or "AllUsers", if not provided "CurrentUser" used. 14 | [Parameter(Mandatory, ParameterSetName = 'Scope')] 15 | [ValidateSet('CurrentUser', 'AllUsers')] 16 | [string] 17 | $Scope = 'CurrentUser', 18 | 19 | # Get module from GitHub instead of local Git clone, for example "https://raw.githubusercontent.com/ili101/Module.Template/master/Install.ps1" 20 | [ValidateNotNullOrEmpty()] 21 | [Uri]$FromGitHub 22 | ) 23 | # Set Files and Folders patterns to Include/Exclude. 24 | $IncludeFiles = @( 25 | '*.dll', 26 | '*.psd1', 27 | '*.psm1', 28 | '*.ps1', 29 | 'morelinq*' 30 | ) 31 | $ExcludeFiles = @( 32 | 'Install.ps1' 33 | ) 34 | 35 | 36 | function Invoke-MultiLike { 37 | [alias("LikeAny")] 38 | [CmdletBinding()] 39 | param 40 | ( 41 | $InputObject, 42 | [Parameter(Mandatory)] 43 | [String[]]$Filters, 44 | [Switch]$Not 45 | ) 46 | $FiltersRegex = foreach ($Filter In $Filters) { 47 | $Filter = [regex]::Escape($Filter) 48 | if ($Filter -match "^\\\*") { 49 | $Filter = $Filter.Remove(0, 2) 50 | } 51 | else { 52 | $Filter = '^' + $Filter 53 | } 54 | if ($Filter -match "\\\*$") { 55 | $Filter = $Filter.Substring(0, $Filter.Length - 2) 56 | } 57 | else { 58 | $Filter = $Filter + '$' 59 | } 60 | $Filter 61 | } 62 | if ($Not) { 63 | $InputObject -notmatch ($FiltersRegex -join '|').replace('\*', '.*').replace('\?', '.') 64 | } 65 | else { 66 | $InputObject -match ($FiltersRegex -join '|').replace('\*', '.*').replace('\?', '.') 67 | } 68 | } 69 | 70 | Try { 71 | Write-Verbose -Message 'Module installation started' 72 | 73 | if (!$ModulePath) { 74 | if ($Scope -eq 'CurrentUser') { 75 | $ModulePathIndex = 0 76 | } 77 | else { 78 | $ModulePathIndex = 1 79 | } 80 | if ($IsLinux -or $IsMacOS) { 81 | $ModulePathSeparator = ':' 82 | } 83 | else { 84 | $ModulePathSeparator = ';' 85 | } 86 | $ModulePath = ($env:PSModulePath -split $ModulePathSeparator)[$ModulePathIndex] 87 | } 88 | 89 | # Get $ModuleName, $TargetPath, [$Links] 90 | if ($FromGitHub) { 91 | # Fix Could not create SSL/TLS secure channel 92 | #$SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol 93 | #[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 94 | 95 | $WebClient = [System.Net.WebClient]::new() 96 | $GitUri = $FromGitHub.AbsolutePath.Split('/')[1, 2] -join '/' 97 | $GitBranch = $FromGitHub.AbsolutePath.Split('/')[3] 98 | $Links = (Invoke-RestMethod -Uri "https://api.github.com/repos/$GitUri/contents" -Body @{ref = $GitBranch }) | Where-Object { (LikeAny $_.name $IncludeFiles) -and (LikeAny $_.name $ExcludeFiles -Not) } 99 | 100 | $ModuleName = [System.IO.Path]::GetFileNameWithoutExtension(($Links | Where-Object { $_.name -like '*.psm1' }).name) 101 | $ModuleVersion = (. ([Scriptblock]::Create((Invoke-WebRequest -Uri ($Links | Where-Object { $_.name -eq "$ModuleName.psd1" }).download_url)))).ModuleVersion 102 | } 103 | else { 104 | $ModuleName = [System.IO.Path]::GetFileNameWithoutExtension((Get-ChildItem -File -Filter *.psm1 -Name -Path $PSScriptRoot)) 105 | $ModuleVersion = (. ([Scriptblock]::Create((Get-Content -Path (Join-Path $PSScriptRoot "$ModuleName.psd1") | Out-String)))).ModuleVersion 106 | } 107 | $TargetPath = Join-Path -Path $ModulePath -ChildPath $ModuleName 108 | $TargetPath = Join-Path -Path $TargetPath -ChildPath $ModuleVersion 109 | 110 | # Create Directory 111 | if (-not (Test-Path -Path $TargetPath)) { 112 | $null = New-Item -Path $TargetPath -ItemType Directory -ErrorAction Stop 113 | Write-Verbose -Message ('Created module folder: "{0}"' -f $TargetPath) 114 | } 115 | 116 | # Copy Files 117 | if ($FromGitHub) { 118 | foreach ($Link in $Links) { 119 | $TargetPathItem = Join-Path -Path $TargetPath -ChildPath $Link.name 120 | if ($Link.type -ne 'dir') { 121 | $WebClient.DownloadFile($Link.download_url, $TargetPathItem) 122 | Write-Verbose -Message ('Installed module file: "{0}"' -f $Link.name) 123 | } 124 | else { 125 | if (-not (Test-Path -Path $TargetPathItem)) { 126 | $null = New-Item -Path $TargetPathItem -ItemType Directory -ErrorAction Stop 127 | Write-Verbose -Message ('Created module folder: "{0}"' -f $TargetPathItem) 128 | } 129 | $SubLinks = (Invoke-RestMethod -Uri $Link.git_url -Body @{recursive = '1' }).tree 130 | foreach ($SubLink in $SubLinks) { 131 | $TargetPathSub = Join-Path -Path $TargetPathItem -ChildPath $SubLink.path 132 | if ($SubLink.'type' -EQ 'tree') { 133 | if (-not (Test-Path -Path $TargetPathSub)) { 134 | $null = New-Item -Path $TargetPathSub -ItemType Directory -ErrorAction Stop 135 | Write-Verbose -Message ('Created module folder: "{0}"' -f $TargetPathSub) 136 | } 137 | } 138 | else { 139 | $WebClient.DownloadFile( 140 | ('https://raw.githubusercontent.com/{0}/{1}/{2}/{3}' -f $GitUri, $GitBranch, $Link.name, $SubLink.path), 141 | $TargetPathSub 142 | ) 143 | } 144 | } 145 | } 146 | } 147 | } 148 | else { 149 | Get-ChildItem -Path $PSScriptRoot -Exclude $ExcludeFiles | Where-Object { LikeAny $_.Name $IncludeFiles } | ForEach-Object { 150 | if ($_.Attributes -ne 'Directory') { 151 | Copy-Item -Path $_ -Destination $TargetPath 152 | Write-Verbose -Message ('Installed module file "{0}"' -f $_) 153 | } 154 | else { 155 | Copy-Item -Path $_ -Destination $TargetPath -Recurse -Force 156 | Write-Verbose -Message ('Installed module folder "{0}"' -f $_) 157 | } 158 | } 159 | } 160 | 161 | # Import Module 162 | Write-Verbose -Message "$ModuleName module installation successful to $TargetPath" 163 | Import-Module -Name $ModuleName -Force 164 | Write-Verbose -Message "Module installed" 165 | } 166 | Catch { 167 | throw ('Failed installing module "{0}". Error: "{1}" in Line {2}' -f $ModuleName, $_, $_.InvocationInfo.ScriptLineNumber) 168 | } 169 | finally { 170 | #if ($FromGitHub) { 171 | # [Net.ServicePointManager]::SecurityProtocol = $SecurityProtocol 172 | #} 173 | Write-Verbose -Message 'Module installation end' 174 | } -------------------------------------------------------------------------------- /Join-Object.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Data 2 | Add-Type -AssemblyName System.Data.DataSetExtensions 3 | function Join-Object { 4 | <# 5 | .SYNOPSIS 6 | Join data from two sets of objects based on a common value 7 | 8 | .DESCRIPTION 9 | Join data from two sets of objects based on a common value 10 | 11 | For more details, see the accompanying blog post: 12 | http://ramblingcookiemonster.github.io/Join-Object/ 13 | 14 | For even more details, see the original code and discussions that this borrows from: 15 | Dave Wyatt's Join-Object - http://powershell.org/wp/forums/topic/merging-very-large-collections 16 | Lucio Silveira's Join-Object - http://blogs.msdn.com/b/powershell/archive/2012/07/13/join-object.aspx 17 | 18 | .PARAMETER Left 19 | 'Left' collection of objects to join. You can use the pipeline for Left. 20 | 21 | The objects in this collection should be consistent. 22 | We look at the properties on the first object for a baseline. 23 | 24 | .PARAMETER Right 25 | 'Right' collection of objects to join. 26 | 27 | The objects in this collection should be consistent. 28 | We look at the properties on the first object for a baseline. 29 | 30 | .PARAMETER LeftJoinProperty 31 | Property on Left collection objects that we match up with RightJoinProperty on the Right collection 32 | 33 | .PARAMETER RightJoinProperty 34 | Property on Right collection objects that we match up with LeftJoinProperty on the Left collection 35 | 36 | .PARAMETER LeftProperties 37 | One or more properties to keep from Left. Default is to keep all Left properties (*). 38 | 39 | Each property can: 40 | - Be a plain property name like "Name" 41 | - Contain wildcards like "*" 42 | - Be a Hashtable like @{Name="Product Name";Expression={$_.Name}}. 43 | Name is the output property name 44 | Expression is the property value ($_ as the current object) 45 | 46 | Alternatively, use the Suffix or Prefix parameter to avoid collisions 47 | Each property using this Hashtable syntax will be excluded from suffixes and prefixes 48 | 49 | .PARAMETER RightProperties 50 | One or more properties to keep from Right. Default is to keep all Right properties (*). 51 | 52 | Each property can: 53 | - Be a plain property name like "Name" 54 | - Contain wildcards like "*" 55 | - Be a Hashtable like @{Name="Product Name";Expression={$_.Name}}. 56 | Name is the output property name 57 | Expression is the property value ($_ as the current object) 58 | 59 | Alternatively, use the Suffix or Prefix parameter to avoid collisions 60 | Each property using this Hashtable syntax will be excluded from suffixes and prefixes 61 | 62 | .PARAMETER Prefix 63 | If specified, prepend Right object property names with this prefix to avoid collisions 64 | 65 | Example: 66 | Property Name = 'Name' 67 | Suffix = 'j_' 68 | Resulting Joined Property Name = 'j_Name' 69 | 70 | .PARAMETER Suffix 71 | If specified, append Right object property names with this suffix to avoid collisions 72 | 73 | Example: 74 | Property Name = 'Name' 75 | Suffix = '_j' 76 | Resulting Joined Property Name = 'Name_j' 77 | 78 | .PARAMETER Type 79 | Type of join. Default is AllInLeft. 80 | 81 | AllInLeft will have all elements from Left at least once in the output, and might appear more than once 82 | if the where clause is true for more than one element in right, Left elements with matches in Right are 83 | preceded by elements with no matches. 84 | SQL equivalent: outer left join (or simply left join) 85 | 86 | AllInRight is similar to AllInLeft. 87 | 88 | OnlyIfInBoth will cause all elements from Left to be placed in the output, only if there is at least one 89 | match in Right. 90 | SQL equivalent: inner join (or simply join) 91 | 92 | AllInBoth will have all entries in right and left in the output. Specifically, it will have all entries 93 | in right with at least one match in left, followed by all entries in Right with no matches in left, 94 | followed by all entries in Left with no matches in Right. 95 | SQL equivalent: full join 96 | 97 | .EXAMPLE 98 | # 99 | #Define some input data. 100 | 101 | $l = 1..5 | Foreach-Object { 102 | [PSCustomObject]@{ 103 | Name = "jsmith$_" 104 | Birthday = (Get-Date).adddays(-1) 105 | } 106 | } 107 | 108 | $r = 4..7 | Foreach-Object{ 109 | [PSCustomObject]@{ 110 | Department = "Department $_" 111 | Name = "Department $_" 112 | Manager = "jsmith$_" 113 | } 114 | } 115 | 116 | #We have a name and Birthday for each manager, how do we find their department, using an inner join? 117 | Join-Object -Left $l -Right $r -LeftJoinProperty Name -RightJoinProperty Manager -Type OnlyIfInBoth -RightProperties Department 118 | 119 | 120 | # Name Birthday Department 121 | # ---- -------- ---------- 122 | # jsmith4 4/14/2015 3:27:22 PM Department 4 123 | # jsmith5 4/14/2015 3:27:22 PM Department 5 124 | 125 | .EXAMPLE 126 | # 127 | #Define some input data. 128 | 129 | $l = 1..5 | Foreach-Object { 130 | [PSCustomObject]@{ 131 | Name = "jsmith$_" 132 | Birthday = (Get-Date).adddays(-1) 133 | } 134 | } 135 | 136 | $r = 4..7 | Foreach-Object{ 137 | [PSCustomObject]@{ 138 | Department = "Department $_" 139 | Name = "Department $_" 140 | Manager = "jsmith$_" 141 | } 142 | } 143 | 144 | #We have a name and Birthday for each manager, how do we find all related department data, even if there are conflicting properties? 145 | $l | Join-Object -Right $r -LeftJoinProperty Name -RightJoinProperty Manager -Type AllInLeft -Prefix j_ 146 | 147 | # Name Birthday j_Department j_Name j_Manager 148 | # ---- -------- ------------ ------ --------- 149 | # jsmith1 4/14/2015 3:27:22 PM 150 | # jsmith2 4/14/2015 3:27:22 PM 151 | # jsmith3 4/14/2015 3:27:22 PM 152 | # jsmith4 4/14/2015 3:27:22 PM Department 4 Department 4 jsmith4 153 | # jsmith5 4/14/2015 3:27:22 PM Department 5 Department 5 jsmith5 154 | 155 | .EXAMPLE 156 | # 157 | #Hey! You know how to script right? Can you merge these two CSVs, where Path1's IP is equal to Path2's IP_ADDRESS? 158 | 159 | #Get CSV data 160 | $s1 = Import-CSV $Path1 161 | $s2 = Import-CSV $Path2 162 | 163 | #Merge the data, using a full outer join to avoid omitting anything, and export it 164 | Join-Object -Left $s1 -Right $s2 -LeftJoinProperty IP_ADDRESS -RightJoinProperty IP -Prefix 'j_' -Type AllInBoth | 165 | Export-CSV $MergePath -NoTypeInformation 166 | 167 | .EXAMPLE 168 | # 169 | # "Hey Warren, we need to match up SSNs to Active Directory users, and check if they are enabled or not. 170 | # I'll e-mail you an unencrypted CSV with all the SSNs from gmail, what could go wrong?" 171 | 172 | # Import some SSNs. 173 | $SSNs = Import-CSV -Path D:\SSNs.csv 174 | 175 | #Get AD users, and match up by a common value, SamAccountName in this case: 176 | Get-ADUser -Filter "SamAccountName -like 'WFrame*'" | 177 | Join-Object -LeftJoinProperty SamAccountName -Right $SSNs ` 178 | -RightJoinProperty SamAccountName -RightProperties ssn ` 179 | -LeftProperties SamAccountName, enabled, ObjectClass 180 | 181 | .NOTES 182 | This borrows from: 183 | Dave Wyatt's Join-Object - http://powershell.org/wp/forums/topic/merging-very-large-collections/ 184 | Lucio Silveira's Join-Object - http://blogs.msdn.com/b/powershell/archive/2012/07/13/join-object.aspx 185 | 186 | Changes: 187 | Always display full set of properties 188 | Display properties in order (left first, right second) 189 | If specified, add suffix or prefix to right object property names to avoid collisions 190 | Use a Hashtable rather than OrderedDictionary (avoid case sensitivity) 191 | 192 | .LINK 193 | http://ramblingcookiemonster.github.io/Join-Object/ 194 | 195 | .FUNCTIONALITY 196 | PowerShell Language 197 | 198 | #> 199 | [CmdletBinding(DefaultParameterSetName = 'Default')] 200 | Param 201 | ( 202 | [Parameter(Mandatory = $true, ValueFromPipeLine = $true)] 203 | $Left, 204 | [Parameter(Mandatory = $true)] 205 | $Right, 206 | 207 | [Parameter(Mandatory = $true)] 208 | [string[]]$LeftJoinProperty, 209 | [Parameter(Mandatory = $true)] 210 | [string[]]$RightJoinProperty, 211 | 212 | $LeftJoinScript, 213 | $RightJoinScript, 214 | 215 | [ValidateScript( { $_ -is [Collections.Hashtable] -or $_ -is [string] -or $_ -is [Collections.Specialized.OrderedDictionary] } )] 216 | $LeftProperties = '*', 217 | [ValidateScript( { $_ -is [Collections.Hashtable] -or $_ -is [string] -or $_ -is [Collections.Specialized.OrderedDictionary] } )] 218 | $RightProperties = '*', 219 | 220 | [string[]]$ExcludeLeftProperties, 221 | [string[]]$ExcludeRightProperties, 222 | 223 | [switch]$KeepRightJoinProperty, 224 | 225 | [ValidateSet('AllInLeft', 'OnlyIfInBoth', 'AllInBoth')] 226 | [Parameter(Mandatory = $false)] 227 | [string]$Type = 'AllInLeft', 228 | 229 | [string]$Prefix, 230 | [string]$Suffix, 231 | 232 | [Parameter(Mandatory, ParameterSetName = 'PassThru')] 233 | [switch]$PassThru, 234 | [Parameter(Mandatory, ParameterSetName = 'DataTable')] 235 | [switch]$DataTable, 236 | [Parameter(ParameterSetName = 'PassThru')] 237 | [Parameter(ParameterSetName = 'DataTable')] 238 | [Hashtable]$DataTableTypes, 239 | 240 | [ValidateSet('SingleOnly', 'DuplicateLines', 'SubGroups')] 241 | [string]$LeftMultiMode = 'SingleOnly', 242 | [ValidateSet('SingleOnly', 'DuplicateLines', 'SubGroups')] 243 | [string]$RightMultiMode = 'SingleOnly', 244 | 245 | [String]$AddKey, 246 | [switch]$AllowColumnsMerging, 247 | [Collections.Generic.IEqualityComparer[Object]]$Comparer 248 | ) 249 | #region Validate Params 250 | if ($PassThru -and ($Type -in ('AllInBoth', 'OnlyIfInBoth') -or ($Type -eq 'AllInLeft' -and $RightMultiMode -eq 'DuplicateLines'))) { 251 | $PSCmdlet.ThrowTerminatingError( 252 | [Management.Automation.ErrorRecord]::new( 253 | [ArgumentException]::new('"-PassThru" compatible only with "-Type AllInLeft" with "-RightMultiMode" "SingleOnly" or "SubGroups"'), 254 | 'Incompatible Arguments', 255 | [Management.Automation.ErrorCategory]::InvalidArgument, 256 | $Type 257 | ) 258 | ) 259 | } 260 | 261 | if ($AddKey -and $Type -ne 'AllInBoth') { 262 | $PSCmdlet.ThrowTerminatingError( 263 | [Management.Automation.ErrorRecord]::new( 264 | [ArgumentException]::new('"-AddKey" support only "-Type AllInBoth"'), 265 | 'Incompatible Arguments', 266 | [Management.Automation.ErrorCategory]::InvalidArgument, 267 | $AddKey 268 | ) 269 | ) 270 | } 271 | 272 | if ($Type -in 'AllInLeft', 'OnlyIfInBoth') { 273 | if ($PSBoundParameters['LeftMultiMode'] -ne 'DuplicateLines' -and $null -ne $PSBoundParameters['LeftMultiMode']) { 274 | $PSCmdlet.ThrowTerminatingError( 275 | [Management.Automation.ErrorRecord]::new( 276 | [ArgumentException]::new('"-Type AllInLeft" and "-Type OnlyIfInBoth" support only "-LeftMultiMode DuplicateLines"'), 277 | 'Incompatible Arguments', 278 | [Management.Automation.ErrorCategory]::InvalidArgument, 279 | $Type 280 | ) 281 | ) 282 | } 283 | $Attributes = (Get-Variable 'LeftMultiMode').Attributes 284 | $null = $Attributes.Remove($Attributes.Where( { $_.TypeId.Name -eq 'ValidateSetAttribute' } )[0]) 285 | $ValidateSetAttribute = [System.Management.Automation.ValidateSetAttribute]::new('SingleOnly', 'DuplicateLines', 'SubGroups', $null) 286 | $Attributes.Add($ValidateSetAttribute) 287 | $LeftMultiMode = $null 288 | } 289 | if ($Type -in 'OnlyIfInBoth') { 290 | if ($PSBoundParameters['RightMultiMode'] -ne 'DuplicateLines' -and $null -ne $PSBoundParameters['RightMultiMode']) { 291 | $PSCmdlet.ThrowTerminatingError( 292 | [Management.Automation.ErrorRecord]::new( 293 | [ArgumentException]::new('"-Type OnlyIfInBoth" support only "-RightMultiMode DuplicateLines"'), 294 | 'Incompatible Arguments', 295 | [Management.Automation.ErrorCategory]::InvalidArgument, 296 | $Type 297 | ) 298 | ) 299 | } 300 | $Attributes = (Get-Variable 'RightMultiMode').Attributes 301 | $null = $Attributes.Remove($Attributes.Where( { $_.TypeId.Name -eq 'ValidateSetAttribute' } )[0]) 302 | $ValidateSetAttribute = [System.Management.Automation.ValidateSetAttribute]::new('SingleOnly', 'DuplicateLines', 'SubGroups', $null) 303 | $Attributes.Add($ValidateSetAttribute) 304 | $RightMultiMode = $null 305 | } 306 | 307 | if ($AllowColumnsMerging -and !$DataTable -and !($PassThru -and $Left -is [DataTable])) { 308 | $PSCmdlet.ThrowTerminatingError( 309 | [Management.Automation.ErrorRecord]::new( 310 | [ArgumentException]::new('"-AllowColumnsMerging" support only on DataTable output'), 311 | 'Incompatible Arguments', 312 | [Management.Automation.ErrorCategory]::InvalidArgument, 313 | $AllowColumnsMerging 314 | ) 315 | ) 316 | } 317 | #endregion Validate Params 318 | #region Set $SelectedLeftProperties and $SelectedRightProperties 319 | function Get-Properties { 320 | [CmdletBinding()] 321 | param 322 | ( 323 | $Object, 324 | $SelectProperties, 325 | $ExcludeProperties, 326 | $Prefix, 327 | $Suffix 328 | ) 329 | $Properties = [ordered]@{ } 330 | if ($Object -is [System.Data.DataTable]) { 331 | $ObjectProperties = $Object.Columns.ColumnName 332 | } 333 | else { 334 | $ObjectProperties = $Object[0].PSObject.Properties.Name 335 | } 336 | if ($SelectProperties -is [Hashtable] -or $SelectProperties -is [Collections.Specialized.OrderedDictionary]) { 337 | $SelectProperties.GetEnumerator() | Where-Object { $_.Key -notin $ExcludeProperties } | ForEach-Object { $Properties.Add($_.Key, $Prefix + $_.Value + $Suffix) } 338 | } 339 | elseif ($SelectProperties -eq '*') { 340 | $ObjectProperties | Where-Object { $_ -notin $ExcludeProperties } | ForEach-Object { $Properties.Add($_, $Prefix + $_ + $Suffix) } 341 | } 342 | else { 343 | $SelectProperties | Where-Object { $_ -notin $ExcludeProperties } | ForEach-Object { $Properties.Add($_, $Prefix + $_ + $Suffix) } 344 | } 345 | $Properties 346 | } 347 | 348 | $SelectedLeftProperties = Get-Properties -Object $Left -SelectProperties $LeftProperties -ExcludeProperties $ExcludeLeftProperties 349 | if (!$KeepRightJoinProperty) { 350 | $ExcludeRightProperties = @($ExcludeRightProperties) + @($RightJoinProperty) -ne $null 351 | } 352 | $SelectedRightProperties = Get-Properties -Object $Right -SelectProperties $RightProperties -ExcludeProperties $ExcludeRightProperties -Prefix $Prefix -Suffix $Suffix 353 | #endregion Set $SelectedLeftProperties and $SelectedRightProperties 354 | #region Importing package MoreLinq 355 | if ($Type -eq 'AllInBoth') { 356 | try { 357 | if ($PSScriptRoot) { 358 | $ScriptRoot = $PSScriptRoot 359 | } 360 | elseif ($psISE.CurrentFile.IsUntitled -eq $false) { 361 | $ScriptRoot = Split-Path -Path $psISE.CurrentFile.FullPath 362 | } 363 | elseif ($null -ne $psEditor.GetEditorContext().CurrentFile.Path -and $psEditor.GetEditorContext().CurrentFile.Path -notlike 'untitled:*') { 364 | $ScriptRoot = Split-Path -Path $psEditor.GetEditorContext().CurrentFile.Path 365 | } 366 | else { 367 | $ScriptRoot = '.' 368 | } 369 | if (!('MoreLinq.MoreEnumerable' -as [type])) { 370 | Add-Type -Path (Resolve-Path -Path "$ScriptRoot\morelinq.*\lib\netstandard2.0\MoreLinq.dll") 371 | } 372 | } 373 | catch { 374 | $PSCmdlet.ThrowTerminatingError( 375 | [Management.Automation.ErrorRecord]::new( 376 | [TypeLoadException]::new('Importing package MoreLinq failed: {0}' -f $_.Exception.Message, $_.Exception), 377 | 'Importing package', 378 | [Management.Automation.ErrorCategory]::NotInstalled, 379 | $null 380 | ) 381 | ) 382 | } 383 | } 384 | #endregion Importing package MoreLinq 385 | #region Set $RightJoinScript and $LeftJoinScript 386 | function Get-JoinScript { 387 | [CmdletBinding()] 388 | param 389 | ( 390 | $JoinScript, 391 | $JoinProperty, 392 | $Side, 393 | $Object 394 | ) 395 | if ($JoinScript) { 396 | if ($JoinScript.GetType().Name -eq 'Func`2') { 397 | $JoinScript #.GetNewClosure() 398 | } 399 | else { 400 | [System.Func[Object, String]]$JoinScript 401 | } 402 | } 403 | else { 404 | $JoinScript = if ($JoinProperty.Count -gt 1) { 405 | { 406 | param ($_Side_Line) 407 | ($_Side_Line | Select-Object -Property $_Side_JoinProperty).PSObject.Properties.Value 408 | } 409 | } 410 | else { 411 | if ($Object -is [Data.DataTable]) { 412 | { 413 | param ($_Side_Line) 414 | $_Side_Line[$_Side_JoinProperty] 415 | } 416 | } 417 | else { 418 | { 419 | param ($_Side_Line) 420 | $_Side_Line.$_Side_JoinProperty 421 | } 422 | } 423 | } 424 | [System.Func[Object, String]][Scriptblock]::Create($JoinScript.ToString().Replace('_Side_', $Side)) 425 | } 426 | } 427 | $LeftJoinScript = Get-JoinScript -JoinScript $LeftJoinScript -JoinProperty $LeftJoinProperty -Side 'Left' -Object $Left 428 | $RightJoinScript = Get-JoinScript -JoinScript $RightJoinScript -JoinProperty $RightJoinProperty -Side 'Right' -Object $Right 429 | #endregion Set $RightJoinScript and $LeftJoinScript 430 | #region Prepare Data 431 | function Set-OutDataTable { 432 | param 433 | ( 434 | $OutDataTable, 435 | $Object, 436 | $SelectedProperties, 437 | $AllowColumnsMerging 438 | ) 439 | # Create Columns 440 | foreach ($item in $SelectedProperties.GetEnumerator()) { 441 | if (!$AllowColumnsMerging -or !$OutDataTable.Columns.Item($item.Value)) { 442 | if ($Object -is [Data.DataTable]) { 443 | $null = $OutDataTable.Columns.Add($item.Value, $Object.Columns.Item($item.Name).DataType) 444 | } 445 | else { 446 | if ($null -ne $DataTableTypes.($item.Value)) { 447 | $null = $OutDataTable.Columns.Add($item.Value, $DataTableTypes.($item.Value)) 448 | } 449 | else { 450 | $null = $OutDataTable.Columns.Add($item.Value) 451 | } 452 | } 453 | } 454 | } 455 | } 456 | if ($DataTable) { 457 | $OutDataTable = [Data.DataTable]::new('Joined') 458 | if ($AddKey) { 459 | $null = $OutDataTable.Columns.Add($AddKey) 460 | } 461 | if ($LeftMultiMode -eq 'SubGroups') { 462 | $OutDataTableSubGroupTemplateLeft = [Data.DataTable]::new('LeftGroup') 463 | Set-OutDataTable -OutDataTable $OutDataTableSubGroupTemplateLeft -Object $Left -SelectedProperties $SelectedLeftProperties 464 | $null = $OutDataTable.Columns.Add('LeftGroup', [Object]) 465 | } 466 | else { 467 | Set-OutDataTable -OutDataTable $OutDataTable -Object $Left -SelectedProperties $SelectedLeftProperties 468 | } 469 | if ($RightMultiMode -eq 'SubGroups') { 470 | $OutDataTableSubGroupTemplateRight = [Data.DataTable]::new('RightGroup') 471 | Set-OutDataTable -OutDataTable $OutDataTableSubGroupTemplateRight -Object $Right -SelectedProperties $SelectedRightProperties 472 | $null = $OutDataTable.Columns.Add('RightGroup', [Object]) 473 | } 474 | else { 475 | Set-OutDataTable -OutDataTable $OutDataTable -Object $Right -SelectedProperties $SelectedRightProperties -AllowColumnsMerging $AllowColumnsMerging 476 | } 477 | } 478 | elseif ($PassThru) { 479 | if ($Left -is [Data.DataTable]) { 480 | # Remove LeftLine 481 | foreach ($ColumnName in $Left.Columns.ColumnName) { 482 | if ($ColumnName -notin $SelectedLeftProperties.Keys) { 483 | $Left.Columns.Remove($ColumnName) 484 | } 485 | } 486 | # Rename LeftLine 487 | foreach ($item in $SelectedLeftProperties.GetEnumerator()) { 488 | if ($item.Key -ne $item.value -and ($Column = $Left.Columns.Item($item.Key))) { 489 | $Column.ColumnName = $item.value 490 | } 491 | } 492 | if ($RightMultiMode -eq 'SubGroups') { 493 | $null = $Left.Columns.Add('RightGroup', [Object]) 494 | } 495 | else { 496 | # Add RightLine to LeftLine 497 | foreach ($item in $SelectedRightProperties.GetEnumerator()) { 498 | if (!$AllowColumnsMerging -or !$Left.Columns.Item($item.Value)) { 499 | if ($null -ne $DataTableTypes.($item.Value)) { 500 | $null = $Left.Columns.Add($item.Value, $DataTableTypes.($item.Value)) 501 | } 502 | else { 503 | $null = $Left.Columns.Add($item.Value) 504 | } 505 | } 506 | } 507 | } 508 | } 509 | if ($Right -is [Data.DataTable] -and $RightMultiMode -eq 'SubGroups') { 510 | $OutDataTableSubGroupTemplateRight = [Data.DataTable]::new('RightGroup') 511 | Set-OutDataTable -OutDataTable $OutDataTableSubGroupTemplateRight -Object $Right -SelectedProperties $SelectedRightProperties 512 | } 513 | } 514 | #endregion Prepare Data 515 | #region Main 516 | #region Main: Set $QueryParts 517 | $QueryParts = @{ 518 | 'IfSideLine' = { 519 | if ($_Side_Line) { 520 | _SideScript_ 521 | } 522 | } 523 | 'DataTableFromAny' = { 524 | foreach ($item in $Selected_Side_Properties.GetEnumerator()) { 525 | if ($null -ne ($Value = $_Side_Line.($item.Key))) { 526 | $_Row_[$item.Value] = $Value 527 | } 528 | } 529 | } 530 | 'DataTableFromDataTable' = { 531 | foreach ($item in $Selected_Side_Properties.GetEnumerator()) { 532 | if (($Value = $_Side_Line[$item.Key]) -isnot [DBNull]) { 533 | $_Row_[$item.Value] = $Value 534 | } 535 | } 536 | } 537 | 'DataTableFromSubGroup' = { 538 | if ($_Side_Lines) { 539 | _SubGroup_ 540 | $_Row_['_Side_Group'] = $OutSubGroup_Side_ 541 | } 542 | } 543 | 'SubGroupFromDataTable' = { 544 | $OutSubGroup_Side_ = $OutDataTableSubGroupTemplate_Side_.Clone() 545 | foreach ($_Side_Line in $_Side_Lines) { 546 | $RowSubGroup = $OutSubGroup_Side_.Rows.Add() 547 | _DataTable_ 548 | } 549 | } 550 | 'SubGroupFromPSCustomObject' = { 551 | $OutSubGroup_Side_ = @() 552 | foreach ($_Side_Line in $_Side_Lines) { 553 | $RowSubGroup = [ordered]@{ } 554 | _PSCustomObject_ 555 | $OutSubGroup_Side_ += [PSCustomObject]$RowSubGroup 556 | } 557 | } 558 | 'PSCustomObjectFromPSCustomObject' = { 559 | foreach ($item in $Selected_Side_Properties.GetEnumerator()) { 560 | $_Row_.Add($item.Value, $_Side_Line.($item.Key)) 561 | } 562 | } 563 | 'PSCustomObjectFromAny' = { 564 | foreach ($item in $Selected_Side_Properties.GetEnumerator()) { 565 | if (($Value = $_Side_Line.($item.Key)) -is [DBNull]) { 566 | $Value = $null 567 | } 568 | $_Row_.Add($item.Value, $Value) 569 | } 570 | } 571 | 'PSCustomObjectFromSubGroup' = { 572 | if ($_Side_Lines) { 573 | _SubGroup_ 574 | $_Row_.Add('_Side_Group', $OutSubGroup_Side_) 575 | } 576 | else { 577 | $_Row_.Add('_Side_Group', $null) 578 | } 579 | } 580 | } 581 | 582 | foreach ($Item in @($QueryParts.GetEnumerator())) { 583 | $QueryParts[$Item.Key] = $Item.Value.ToString() 584 | } 585 | #endregion Main: Set $QueryParts 586 | #region Main: Set $Query 587 | $Query = if ($PassThru) { 588 | if ($Left -is [Data.DataTable]) { 589 | $QueryTemp = @{ 590 | Main = { _SidesScript_ } 591 | SideReplace = '_Row_', 'LeftLine' 592 | } 593 | if ($RightMultiMode -eq 'SubGroups') { 594 | $QueryTemp['SideSubGroupBase'] = $QueryParts['DataTableFromSubGroup'] 595 | if ($Right -is [Data.DataTable]) { 596 | $QueryTemp['SideSubGroup'] = $QueryParts['SubGroupFromDataTable'] 597 | } 598 | else { 599 | $QueryTemp['SideSubGroup'] = $QueryParts['SubGroupFromPSCustomObject'] 600 | } 601 | } 602 | else { 603 | $QueryTemp['Right'] = { _DataTable_ } 604 | } 605 | $QueryTemp 606 | } 607 | else { 608 | # Left is PSCustomObject 609 | $QueryTemp = @{ 610 | # Edit PSCustomObject 611 | Main = { 612 | # Add to LeftLine (Rename) 613 | foreach ($item in $SelectedLeftProperties.GetEnumerator()) { 614 | if ($item.Value -notin $LeftLine.PSObject.Properties.Name) { 615 | $LeftLine.PSObject.Properties.Add([Management.Automation.PSNoteProperty]::new($item.Value, $LeftLine.($item.Key))) 616 | } 617 | } 618 | # Remove from LeftLine 619 | foreach ($item in $LeftLine.PSObject.Properties.Name) { 620 | if ($item -notin $SelectedLeftProperties.Values) { 621 | $LeftLine.PSObject.Properties.Remove($item) 622 | } 623 | } 624 | _SidesScript_ 625 | } 626 | SideReplace = '_Row_\.Add([^\r\n]*)', 'LeftLine.PSObject.Properties.Add([Management.Automation.PSNoteProperty]::new$1)' 627 | } 628 | if ($RightMultiMode -eq 'SubGroups') { 629 | $QueryTemp['SideSubGroupBase'] = $QueryParts['PSCustomObjectFromSubGroup'] 630 | if ($Right -is [Data.DataTable]) { 631 | $QueryTemp['SideSubGroup'] = $QueryParts['SubGroupFromDataTable'] 632 | } 633 | else { 634 | $QueryTemp['SideSubGroup'] = $QueryParts['SubGroupFromPSCustomObject'] 635 | } 636 | } 637 | else { 638 | $QueryTemp['Right'] = { _PSCustomObject_ } 639 | } 640 | $QueryTemp 641 | } 642 | } 643 | elseif ($DataTable) { 644 | $QueryTemp = @{ 645 | Main = { 646 | $RowMain = $OutDataTable.Rows.Add() 647 | _SidesScript_ 648 | } 649 | SideReplace = '_Row_', 'RowMain' 650 | } 651 | if ($LeftMultiMode -eq 'SubGroups' -or $RightMultiMode -eq 'SubGroups') { 652 | $QueryTemp['SideSubGroupBase'] = $QueryParts['DataTableFromSubGroup'] 653 | $QueryTemp['SideSubGroup'] = $QueryParts['SubGroupFromDataTable'] 654 | } 655 | if ($LeftMultiMode -ne 'SubGroups' -or $RightMultiMode -ne 'SubGroups') { 656 | $QueryTemp['Side'] = { _DataTable_ } 657 | } 658 | $QueryTemp 659 | } 660 | else { 661 | # PSCustomObject 662 | $QueryTemp = @{ 663 | Main = { 664 | $RowMain = [ordered]@{ } 665 | _SidesScript_ 666 | [PSCustomObject]$RowMain 667 | } 668 | SideReplace = '_Row_', 'RowMain' 669 | } 670 | if ($LeftMultiMode -eq 'SubGroups' -or $RightMultiMode -eq 'SubGroups') { 671 | $QueryTemp['SideSubGroupBase'] = $QueryParts['PSCustomObjectFromSubGroup'] 672 | $QueryTemp['SideSubGroup'] = $QueryParts['SubGroupFromPSCustomObject'] 673 | } 674 | if ($LeftMultiMode -ne 'SubGroups' -or $RightMultiMode -ne 'SubGroups') { 675 | $QueryTemp['Side'] = { _PSCustomObject_ } 676 | } 677 | $QueryTemp 678 | } 679 | 680 | $Query['Base'] = { 681 | param( 682 | #_$Key_, 683 | $LeftLine, 684 | $RightLine 685 | ) 686 | } 687 | 688 | foreach ($Item in @($Query.GetEnumerator())) { 689 | if ($Item.Value -is [Scriptblock]) { 690 | $Query[$Item.Key] = $Item.Value.ToString() 691 | } 692 | } 693 | #endregion Main: Set $Query 694 | #region Main: Assemble $Query 695 | function Invoke-AssembledQuery { 696 | param ( 697 | $MultiMode, 698 | $Side, 699 | $Object 700 | ) 701 | if ($MultiMode -eq 'SingleOnly') { 702 | $Query[$Side + 'Enumerable'] = { $_Side_Line = [System.Linq.Enumerable]::SingleOrDefault($_Side_Line) } 703 | } 704 | elseif ($MultiMode -eq 'DuplicateLines') { 705 | $Query[$Side + 'Enumerable'] = { $_Side_Lines = [System.Linq.Enumerable]::DefaultIfEmpty($_Side_Line) } 706 | $Query['Main'] = { 707 | foreach ($_Side_Line in $_Side_Lines) { 708 | _MainScript_ 709 | } 710 | }.ToString().Replace('_Side_', $Side).Replace('_MainScript_', $Query['Main']) 711 | } 712 | if ($MultiMode -eq 'SubGroups') { 713 | $Query[$Side + 'Enumerable'] = { $_Side_Lines = if ($_Side_Line.Count) { $_Side_Line } } 714 | $Query[$Side] = if ($Object -is [Data.DataTable]) { 715 | ($Query['SideSubGroupBase'] -Replace $Query['SideReplace']).Replace('_SubGroup_', $Query['SideSubGroup']).Replace('_DataTable_', $QueryParts['DataTableFromDataTable']).Replace('_PSCustomObject_', $QueryParts['PSCustomObjectFromAny']) 716 | } 717 | else { 718 | ($Query['SideSubGroupBase'] -Replace $Query['SideReplace']).Replace('_SubGroup_', $Query['SideSubGroup']).Replace('_DataTable_', $QueryParts['DataTableFromAny']).Replace('_PSCustomObject_', $QueryParts['PSCustomObjectFromPSCustomObject']) 719 | } 720 | $Query[$Side] = $Query[$Side].Replace('_Row_', 'RowSubGroup') 721 | } 722 | else { 723 | if ($Query[$Side] -or $Query['Side']) { 724 | if ($null -eq $Query[$Side]) { 725 | $Query[$Side] = $Query['Side'] 726 | } 727 | if ($null -ne $MultiMode -and $Query[$Side] -like '*_DataTable_*') { 728 | $Query[$Side] = $QueryParts['IfSideLine'].Replace('_SideScript_', $Query[$Side]) 729 | } 730 | $Query[$Side] = if ($Object -is [Data.DataTable]) { 731 | $Query[$Side].Replace('_DataTable_', $QueryParts['DataTableFromDataTable']).Replace('_PSCustomObject_', $QueryParts['PSCustomObjectFromAny']) 732 | } 733 | else { 734 | $Query[$Side].Replace('_DataTable_', $QueryParts['DataTableFromAny']).Replace('_PSCustomObject_', $QueryParts['PSCustomObjectFromPSCustomObject']) 735 | } 736 | $Query[$Side] = $Query[$Side] -Replace $Query['SideReplace'] 737 | } 738 | } 739 | if ($Query[$Side]) { 740 | $Query[$Side] = $Query[$Side].Replace('_Side_', $Side) 741 | } 742 | if ($Query[$Side + 'Enumerable']) { 743 | $Query[$Side + 'Enumerable'] = $Query[$Side + 'Enumerable'].ToString().Replace('_Side_', $Side) 744 | } 745 | } 746 | Invoke-AssembledQuery -MultiMode $LeftMultiMode -Side 'Left' -Object $Left 747 | Invoke-AssembledQuery -MultiMode $RightMultiMode -Side 'Right' -Object $Right 748 | 749 | if ($AddKey) { 750 | $KeyScript = { 751 | $RowMain._Key_ = $Key 752 | _SidesScript_ 753 | }.ToString() 754 | $KeyScript = if ($DataTable) { 755 | $KeyScript.Replace('._Key_', '[$AddKey]') 756 | } 757 | else { 758 | $KeyScript.Replace('_Key_', '$AddKey') 759 | } 760 | $Query['Main'] = $Query['Main'].Replace('_SidesScript_', $KeyScript) 761 | } 762 | 763 | $Query['Main'] = $Query['Main'].Replace('_SidesScript_', $Query['Left'] + $Query['Right']) 764 | 765 | if ($Type -eq 'OnlyIfInBoth') { 766 | [System.Func[System.Object, System.Object, System.Object]]$Query = [Scriptblock]::Create($Query['Base'] + $Query['Main']) 767 | } 768 | elseif ($Type -eq 'AllInLeft') { 769 | [System.Func[System.Object, [Collections.Generic.IEnumerable[System.Object]], System.Object]]$Query = [Scriptblock]::Create($Query['Base'] + $Query['RightEnumerable'] + $Query['Main']) 770 | } 771 | elseif ($Type -eq 'AllInBoth') { 772 | [System.Func[System.Object, [Collections.Generic.IEnumerable[System.Object]], [Collections.Generic.IEnumerable[System.Object]], System.Object]]$Query = [Scriptblock]::Create($Query['Base'].Replace('#_$Key_', '$Key') + $Query['LeftEnumerable'] + "`n" + $Query['RightEnumerable'] + $Query['Main']) 773 | } 774 | #endregion Main: Assemble $Query 775 | #endregion Main 776 | #region Execute 777 | if ($Left -is [Data.DataTable]) { 778 | $LeftNew = [DataTableExtensions]::AsEnumerable($Left) 779 | } 780 | elseif ($Left -is [PSCustomObject] -or $Left -is [Collections.ArrayList]) { 781 | $LeftNew = @($Left) 782 | } 783 | else { 784 | $LeftNew = $Left 785 | } 786 | if ($Right -is [Data.DataTable]) { 787 | $RightNew = [DataTableExtensions]::AsEnumerable($Right) 788 | } 789 | elseif ($Right -is [PSCustomObject] -or $Right -is [Collections.ArrayList]) { 790 | $RightNew = @($Right) 791 | } 792 | else { 793 | $RightNew = $Right 794 | } 795 | 796 | try { 797 | $Result = if ($Type -eq 'OnlyIfInBoth') { 798 | [System.Linq.Enumerable]::ToArray( 799 | [System.Linq.Enumerable]::Join($LeftNew, $RightNew, $LeftJoinScript, $RightJoinScript, $query, $Comparer) 800 | ) 801 | } 802 | elseif ($Type -eq 'AllInBoth') { 803 | [System.Linq.Enumerable]::ToArray( 804 | [MoreLinq.MoreEnumerable]::FullGroupJoin($LeftNew, $RightNew, $LeftJoinScript, $RightJoinScript, $query, $Comparer) 805 | ) 806 | } 807 | else { 808 | [System.Linq.Enumerable]::ToArray( 809 | [System.Linq.Enumerable]::GroupJoin($LeftNew, $RightNew, $LeftJoinScript, $RightJoinScript, $query, $Comparer) 810 | ) 811 | } 812 | 813 | if ($PassThru) { 814 | , $Left 815 | } 816 | elseif ($DataTable) { 817 | , $OutDataTable 818 | } 819 | elseif ($LeftMultiMode -eq 'DuplicateLines' -or $RightMultiMode -eq 'DuplicateLines') { 820 | $Result.ForEach( { $_ } ) 821 | } 822 | else { 823 | $Result 824 | } 825 | } 826 | catch { 827 | $PSCmdlet.ThrowTerminatingError($_) 828 | } 829 | #endregion Execute 830 | } -------------------------------------------------------------------------------- /Join-Object.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'Join-Object' 3 | # 4 | # Generated by: ili 5 | # 6 | # Generated on: 06/03/2018 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'Join-Object.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '2.0.3' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = 'a7f9497a-ec8e-46e6-ad3a-39201b151458' 22 | 23 | # Author of this module 24 | Author = 'ili' 25 | 26 | # Company or vendor of this module 27 | #CompanyName = 'Unknown' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) 2017 ili. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | Description = @' 34 | Join-Object LINQ Edition. 35 | Join data from two sets of objects based on a common value. 36 | Aims to provide the exact functionality of https://github.com/RamblingCookieMonster/PowerShell/blob/master/Join-Object.ps1 with much better performance. 37 | Initial testing shows at last 100 times faster. 38 | 39 | More info at https://github.com/ili101/Join-Object/blob/master/README.md 40 | '@ 41 | 42 | # Minimum version of the Windows PowerShell engine required by this module 43 | # PowerShellVersion = '' 44 | 45 | # Name of the Windows PowerShell host required by this module 46 | # PowerShellHostName = '' 47 | 48 | # Minimum version of the Windows PowerShell host required by this module 49 | # PowerShellHostVersion = '' 50 | 51 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 52 | # DotNetFrameworkVersion = '' 53 | 54 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 55 | # CLRVersion = '' 56 | 57 | # Processor architecture (None, X86, Amd64) required by this module 58 | # ProcessorArchitecture = '' 59 | 60 | # Modules that must be imported into the global environment prior to importing this module 61 | # RequiredModules = @() 62 | 63 | # Assemblies that must be loaded prior to importing this module 64 | # RequiredAssemblies = @() 65 | 66 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 67 | # ScriptsToProcess = @() 68 | 69 | # Type files (.ps1xml) to be loaded when importing this module 70 | # TypesToProcess = @() 71 | 72 | # Format files (.ps1xml) to be loaded when importing this module 73 | # FormatsToProcess = @() 74 | 75 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 76 | # NestedModules = @() 77 | 78 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 79 | FunctionsToExport = 'Join-Object' 80 | 81 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 82 | CmdletsToExport = @() 83 | 84 | # Variables to export from this module 85 | # VariablesToExport = '' 86 | 87 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 88 | AliasesToExport = @() 89 | 90 | # DSC resources to export from this module 91 | # DscResourcesToExport = @() 92 | 93 | # List of all modules packaged with this module 94 | # ModuleList = @() 95 | 96 | # List of all files packaged with this module 97 | # FileList = @() 98 | 99 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData Hashtable with additional module metadata used by PowerShell. 100 | PrivateData = @{ 101 | 102 | PSData = @{ 103 | 104 | # Tags applied to this module. These help with module discovery in online galleries. 105 | Tags = @('Join', 'Join-Object', 'LINQ', 'Combine', 'Merge', 'Union', 'Compare', 'Table', 'DataTable') 106 | 107 | # A URL to the license for this module. 108 | # LicenseUri = '' 109 | 110 | # A URL to the main website for this project. 111 | ProjectUri = 'https://github.com/ili101/Join-Object' 112 | 113 | # A URL to an icon representing this module. 114 | # IconUri = '' 115 | 116 | # ReleaseNotes of this module 117 | ReleaseNotes = @' 118 | Change Log 119 | https://github.com/ili101/Join-Object/blob/master/CHANGELOG.md 120 | '@ 121 | 122 | } # End of PSData Hashtable 123 | 124 | } # End of PrivateData Hashtable 125 | 126 | # HelpInfo URI of this module 127 | # HelpInfoURI = '' 128 | 129 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 130 | # DefaultCommandPrefix = '' 131 | 132 | } -------------------------------------------------------------------------------- /Join-Object.psm1: -------------------------------------------------------------------------------- 1 | #Get-ChildItem -Path $PSScriptRoot | Unblock-File 2 | Get-ChildItem -Path "$PSScriptRoot\*.ps1" -Exclude 'Class.ps1', 'Install.ps1' | ForEach-Object { . $_.FullName } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Illy Metuki 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Join-Object 2 | Join-Object LINQ Edition. 3 | Aims to provide the exact functionality of https://github.com/RamblingCookieMonster/PowerShell/blob/master/Join-Object.ps1 with much better performance. 4 | Initial testing shows at last 100 times faster. 5 | 6 | [![PowerShell Gallery](https://img.shields.io/powershellgallery/v/Join-Object.svg)](https://www.powershellgallery.com/packages/Join-Object/) 7 | [![Downloads](https://img.shields.io/powershellgallery/dt/Join-Object.svg)](https://www.powershellgallery.com/packages/Join-Object/) 8 | 9 | | CI System | Environment | Master | Beta | 10 | |--------------|-------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 11 | | AppVeyor | Windows, Core preview, Ubuntu | [![Build status](https://ci.appveyor.com/api/projects/status/sk2d54q6q85i1ejm/branch/master?svg=true)](https://ci.appveyor.com/project/ili101/Join-Object) | [![Build status](https://ci.appveyor.com/api/projects/status/sk2d54q6q85i1ejm/branch/Beta?svg=true)](https://ci.appveyor.com/project/ili101/Join-Object) | 12 | | Azure DevOps | Windows | [![Build Status](https://dev.azure.com/ili101/Join-Object/_apis/build/status/ili101.Join-Object?branchName=master&jobName=Windows)](https://dev.azure.com/ili101/Join-Object/_build/latest?definitionId=1&branchName=master) | [![Build Status](https://dev.azure.com/ili101/Join-Object/_apis/build/status/ili101.Join-Object?branchName=Beta&jobName=Windows)](https://dev.azure.com/ili101/Join-Object/_build/latest?definitionId=1&branchName=Beta) | 13 | | Azure DevOps | Windows (Core) | [![Build Status](https://dev.azure.com/ili101/Join-Object/_apis/build/status/ili101.Join-Object?branchName=master&jobName=WindowsPSCore)](https://dev.azure.com/ili101/Join-Object/_build/latest?definitionId=1&branchName=master) | [![Build Status](https://dev.azure.com/ili101/Join-Object/_apis/build/status/ili101.Join-Object?branchName=Beta&jobName=WindowsPSCore)](https://dev.azure.com/ili101/Join-Object/_build/latest?definitionId=1&branchName=Beta) | 14 | | Azure DevOps | Ubuntu | [![Build Status](https://dev.azure.com/ili101/Join-Object/_apis/build/status/ili101.Join-Object?branchName=master&jobName=Ubuntu)](https://dev.azure.com/ili101/Join-Object/_build/latest?definitionId=1&branchName=master) | [![Build Status](https://dev.azure.com/ili101/Join-Object/_apis/build/status/ili101.Join-Object?branchName=Beta&jobName=Ubuntu)](https://dev.azure.com/ili101/Join-Object/_build/latest?definitionId=1&branchName=Beta) | 15 | | Azure DevOps | macOS | [![Build Status](https://dev.azure.com/ili101/Join-Object/_apis/build/status/ili101.Join-Object?branchName=master&jobName=macOS)](https://dev.azure.com/ili101/Join-Object/_build/latest?definitionId=1&branchName=master) | [![Build Status](https://dev.azure.com/ili101/Join-Object/_apis/build/status/ili101.Join-Object?branchName=Beta&jobName=macOS)](https://dev.azure.com/ili101/Join-Object/_build/latest?definitionId=1&branchName=Beta) | 16 | 17 | ## Explanation and usage Examples 18 | See RamblingCookieMonster guide http://ramblingcookiemonster.github.io/Join-Object/ and [Join-Object.Examples.ps1](https://github.com/ili101/Join-Object/blob/master/Examples/Join-Object.Examples.ps1). 19 | And also [Join-Object.Tests.ps1](https://github.com/ili101/Join-Object/blob/master/Tests/Join-Object.Tests.ps1). 20 | 21 | ``` PowerShell 22 | # Left Object Example 23 | $PSCustomObject = @( 24 | [PSCustomObject]@{ID = 1 ; Sub = 'S1' ; IntO = 6} 25 | [PSCustomObject]@{ID = 2 ; Sub = 'S2' ; IntO = 7} 26 | [PSCustomObject]@{ID = 3 ; Sub = 'S3' ; IntO = $null} 27 | ) 28 | # Right Object Example (DataTable) 29 | $DataTable = [Data.DataTable]::new('Test') 30 | $null = $DataTable.Columns.Add('IDD', [System.Int32]) 31 | $null = $DataTable.Columns.Add('Name') 32 | $null = $DataTable.Columns.Add('Junk') 33 | $null = $DataTable.Columns.Add('IntT', [System.Int32]) 34 | $null = $DataTable.Rows.Add(1, 'foo', 'AAA', 123456) 35 | $null = $DataTable.Rows.Add(3, 'Bar', 'S3', $null) 36 | $null = $DataTable.Rows.Add(4, 'D', $null, $null) 37 | 38 | # Join the 2 together ("Left Join" in this example) 39 | Join-Object -Left $PSCustomObject -Right $DataTable -LeftJoinProperty 'ID' -RightJoinProperty 'IDD' -ExcludeRightProperties 'Junk' -Prefix 'R_' | Format-Table 40 | 41 | <# Output 42 | ID Sub IntO R_Name R_IntT 43 | -- --- ---- ------ ------ 44 | 1 S1 6 foo 123456 45 | 2 S2 7 46 | 3 S3 Bar 47 | #> 48 | ``` 49 | 50 | ## Additional functionality 51 | * Supports DataTable object type. 52 | * Additional parameters **-ExcludeLeftProperties** and **-ExcludeRightProperties**. 53 | * Additional parameter **-PassThru**, If added changes the original Left Object 54 | * Converts DBNull to $null 55 | * **-RightJoinScript** and **-LeftJoinScript** parameters to support custom joining scripts. 56 | * -RightJoinProperty and -LeftJoinProperty supports multiple Properties (String Array) to **join on multiple columns**. 57 | * **-DataTable** parameter to output as "DataTable". 58 | * **-AddKey** can be used with "-Type AllInBoth" to add a column containing the joining key. 59 | * **-AllowColumnsMerging** Allow duplicate columns in the Left and Right Objects, will overwrite the conflicting Left data with the Right data (Ignoring Nulls), Supported only on DataTable output for now. 60 | * **-Comparer** allow use of custom [EqualityComparer]. 61 | 62 | ## Missing functionality 63 | * -Type "AllInRight". 64 | 65 | ## To do 66 | * Noting for now, You can open an Issues if something is needed. 67 | 68 | ## Install 69 | From repository (Recommended) 70 | ```PowerShell 71 | Install-Module -Name Join-Object -Scope CurrentUser 72 | ``` 73 | From GitHub Branch (For testing) 74 | ```PowerShell 75 | $Uri = 'https://raw.githubusercontent.com/ili101/Join-Object/master/Install.ps1'; & ([Scriptblock]::Create((irm $Uri))) -FromGitHub $Uri 76 | ``` 77 | 78 | ## Contributing 79 | If you fund a bug or added functionality or anything else just fork and send pull requests. Thank you! 80 | 81 | ## Changelog 82 | [CHANGELOG.md](https://github.com/ili101/Join-Object/blob/master/CHANGELOG.md) 83 | 84 | ## Default and supported join modes 85 | | Join Type | Linq mode | LeftMultiMode supported | RightMultiMode supported | PassThru | 86 | |---------------|---------------|-------------------------------------------|-------------------------------------------|-----------------------| 87 | | OnlyIfInBoth | Join | **DuplicateLines** | **DuplicateLines** | Not Supported | 88 | | **AllInLeft** | GroupJoin | **DuplicateLines** | **SingleOnly**, DuplicateLines, SubGroups | SingleOnly, SubGroups | 89 | | AllInBoth | FullGroupJoin | **SingleOnly**, DuplicateLines, SubGroups | **SingleOnly**, DuplicateLines, SubGroups | Not Supported | 90 | 91 | \* **Bold** signifies the default setting. 92 | 93 | # More PowerShell stuff 94 | https://github.com/ili101/PowerShell -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. AllInBoth. DataTable.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. AllInBoth. DataTable.xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. AllInBoth.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. AllInBoth.xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. DataTable. (DataTableTypes).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. DataTable. (DataTableTypes).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. DataTable. (Error, Mismatch).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. DataTable. (Error, Mismatch).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. DataTable.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. DataTable.xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. OnlyIfInBoth. DataTable.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. OnlyIfInBoth. DataTable.xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. OnlyIfInBoth.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. OnlyIfInBoth.xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. PassThru. (DataTableTypes).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. PassThru. (DataTableTypes).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. PassThru.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTable - PSCustomObject. PassThru.xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTable - PSCustomObject.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTable - PSCustomObject.xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTable - PSCustomObjectJunk. DataTable. (AllowColumnsMerging).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTable - PSCustomObjectJunk. DataTable. (AllowColumnsMerging).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTable - PSCustomObjectJunk. PassThru. (AllowColumnsMerging).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTable - PSCustomObjectJunk. PassThru. (AllowColumnsMerging).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTable - PSCustomObjectKeyArray. (Comparer).xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | System.Object[] 5 | System.Array 6 | System.Object 7 | 8 | 9 | 10 | 11 | System.Management.Automation.PSCustomObject 12 | System.Object 13 | 14 | 15 | 1 16 | A 17 | AAA 18 | 5 19 | S1 20 | 6 21 | 22 | 23 | 24 | 25 | 26 | 3 27 | C 28 | S3 29 | 30 | S1 31 | 6 32 | 33 | 34 | 35 | 36 | 37 | 4 38 | D 39 | 40 | 41 | S4 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTable - PSCustomObjectMulti. (SubArray).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTable - PSCustomObjectMulti. (SubArray).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTable - [DataTable]PSCustomObjectJunk. PassThru. (AllowColumnsMerging).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTable - [DataTable]PSCustomObjectJunk. PassThru. (AllowColumnsMerging).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTableMulti - PSCustomObjectMulti. AllInBoth. (SubGroups).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTableMulti - PSCustomObjectMulti. AllInBoth. (SubGroups).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTableMulti - PSCustomObjectMulti. AllInBoth. DataTable. (DuplicateLines).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTableMulti - PSCustomObjectMulti. AllInBoth. DataTable. (DuplicateLines).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTableMulti - PSCustomObjectMulti. AllInBoth. DataTable. (SubGroups).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTableMulti - PSCustomObjectMulti. AllInBoth. DataTable. (SubGroups).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTableMulti - PSCustomObjectMulti. AllInLeft. PassThru. (SubGroups).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTableMulti - PSCustomObjectMulti. AllInLeft. PassThru. (SubGroups).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, DataTableMulti - [DataTable]PSCustomObjectMulti. AllInLeft. PassThru. (SubGroups).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, DataTableMulti - [DataTable]PSCustomObjectMulti. AllInLeft. PassThru. (SubGroups).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. (DBNull to $null).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. (DBNull to $null).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. (Error, Mismatch).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. (Error, Mismatch).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. (JoinScript String).xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | System.Object[] 5 | System.Array 6 | System.Object 7 | 8 | 9 | 10 | 11 | System.Management.Automation.PSCustomObject 12 | System.Object 13 | 14 | 15 | 1 16 | S1 17 | A 18 | 5 19 | 20 | 21 | 22 | 23 | 24 | 2 25 | S2 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 3 34 | S3 35 | C 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. (JoinScript).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. (JoinScript).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. (KeepRightJoinProperty).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. (KeepRightJoinProperty).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. (Multi Join).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. (Multi Join).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. (Ordered).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. (Ordered).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. AllInBoth. DataTable.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. AllInBoth. DataTable.xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. AllInBoth.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. AllInBoth.xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. DataTable.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. DataTable.xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. OnlyIfInBoth. DataTable.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. OnlyIfInBoth. DataTable.xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. OnlyIfInBoth.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. OnlyIfInBoth.xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. PassThru.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObject - DataTable. PassThru.xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObject - DataTable.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObject - DataTable.xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObjectMulti - DataTable. (SubArray).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObjectMulti - DataTable. (SubArray).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInBoth. (SubGroups Key).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInBoth. (SubGroups Key).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInBoth. (SubGroups).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInBoth. (SubGroups).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInBoth. DataTable. (DuplicateLines).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInBoth. DataTable. (DuplicateLines).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInBoth. DataTable. (SubGroups Key).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInBoth. DataTable. (SubGroups Key).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInBoth. DataTable. (SubGroups).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInBoth. DataTable. (SubGroups).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInLeft. PassThru. (SubGroups).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObjectMulti - DataTableMulti. AllInLeft. PassThru. (SubGroups).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObjectMulti - [PSCustomObject]DataTableMulti. AllInLeft. PassThru. (SubGroups).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObjectMulti - [PSCustomObject]DataTableMulti. AllInLeft. PassThru. (SubGroups).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, PSCustomObject[0] - DataTable. (Single).xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/CompareData/DataSetSmall, PSCustomObject[0] - DataTable. (Single).xml -------------------------------------------------------------------------------- /Tests/CompareData/DataSetSmall, [Collections.ArrayList]PSCustomObject - DataTable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | System.Object[] 5 | System.Array 6 | System.Object 7 | 8 | 9 | 10 | 11 | System.Management.Automation.PSCustomObject 12 | System.Object 13 | 14 | 15 | 1 16 | S1 17 | A 18 | 5 19 | 20 | 21 | 22 | 23 | 24 | 2 25 | S2 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 3 34 | S3 35 | C 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Tests/Join-Object.Tests.Big.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules Pester 2 | #Requires -Modules PSSQLite 3 | 4 | if ($PSScriptRoot) { 5 | $ScriptRoot = $PSScriptRoot 6 | } 7 | elseif ($psISE.CurrentFile.IsUntitled -eq $false) { 8 | $ScriptRoot = Split-Path -Path $psISE.CurrentFile.FullPath 9 | } 10 | elseif ($null -ne $psEditor.GetEditorContext().CurrentFile.Path -and $psEditor.GetEditorContext().CurrentFile.Path -notlike 'untitled:*') { 11 | $ScriptRoot = Split-Path -Path $psEditor.GetEditorContext().CurrentFile.Path 12 | } 13 | else { 14 | $ScriptRoot = '.' 15 | } 16 | . "$ScriptRoot\TestHelpers.ps1" 17 | 18 | $DataSetBig10k = "$ScriptRoot\TestDataSetBig10k.db" 19 | $DataSetBig100k = "$ScriptRoot\TestDataSetBig100k.db" 20 | 21 | Describe -Name 'Join-Object' -Fixture { 22 | Context -Name ($DataSetName = 'DataSetBig100k') -Fixture { 23 | It -Name "Testing: " -TestCases @( 24 | Format-Test @{ 25 | Params = @{ 26 | Left = @{ Table = 'authors' ; As = 'DataTable' } 27 | Right = @{ Table = 'posts' ; As = 'DataTable' } 28 | LeftJoinProperty = 'author_id' 29 | RightJoinProperty = 'author_id' 30 | #DataTable = $true 31 | #RightMultiMode = 'SubGroups' 32 | } 33 | } 34 | ) -Test { 35 | param ( 36 | $Params, 37 | $DataSet, 38 | $TestName, 39 | $RunScript, 40 | $ExpectedErrorMessage, 41 | [ValidateSet('Test', 'Run')] 42 | $ExpectedErrorOn 43 | ) 44 | if ($RunScript) { 45 | . $RunScript 46 | } 47 | 48 | # Load Data 49 | try { 50 | $DbConnection = New-SQLiteConnection -DataSource $DataSet 51 | $Params.Left = Get-Params -Param $Params.Left -DbConnection $DbConnection 52 | $Params.Right = Get-Params -Param $Params.Right -DbConnection $DbConnection 53 | } 54 | finally { 55 | $DbConnection.Dispose() 56 | } 57 | 58 | # Execute Cmdlet 59 | $Measure = Measure-Command { 60 | $JoinedOutput = Join-Object @Params 61 | } 62 | 63 | if ($JoinedOutput -is [Data.DataTable]) { 64 | Write-Host ("Execution Time: {0}, Count: {1}, Type: {2}." -f $Measure, $JoinedOutput.Rows.Count, $JoinedOutput.GetType()) 65 | Write-Host ('Sample:' + ($JoinedOutput.Rows[$JoinedOutput.Rows.Count - 1] | Out-String).TrimEnd()) 66 | } 67 | else { 68 | Write-Host ("Execution Time: {0}, Count: {1}, Type: {2}." -f $Measure, $JoinedOutput.Count, $JoinedOutput.GetType()) 69 | Write-Host ('Sample:' + ($JoinedOutput[-1] | Out-String).TrimEnd()) 70 | } 71 | } 72 | } 73 | } 74 | 75 | <# http://filldb.info 76 | CREATE TABLE `authors` ( 77 | `author_id` INT(11) NOT NULL AUTO_INCREMENT, 78 | `first_name` VARCHAR(50) NOT NULL COLLATE 'utf8_unicode_ci', 79 | `last_name` VARCHAR(50) NOT NULL COLLATE 'utf8_unicode_ci', 80 | `email` VARCHAR(100) NOT NULL COLLATE 'utf8_unicode_ci', 81 | `birthdate` DATE NOT NULL, 82 | `added` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 83 | PRIMARY KEY (`author_id`), 84 | UNIQUE INDEX `email` (`email`) 85 | ) 86 | COLLATE='utf8_unicode_ci' 87 | ENGINE=InnoDB; 88 | 89 | CREATE TABLE `posts` ( 90 | `post_id` INT(11) NOT NULL AUTO_INCREMENT, 91 | `author_id` INT(11) NOT NULL, 92 | `title` VARCHAR(255) NOT NULL COLLATE 'utf8_unicode_ci', 93 | `description` VARCHAR(500) NOT NULL COLLATE 'utf8_unicode_ci', 94 | `content` TEXT NOT NULL COLLATE 'utf8_unicode_ci', 95 | `date` DATE NOT NULL, 96 | PRIMARY KEY (`post_id`) 97 | ) 98 | COLLATE='utf8_unicode_ci' 99 | ENGINE=InnoDB; 100 | #> 101 | <# SQLite 102 | DROP TABLE IF EXISTS `authors`; 103 | CREATE TABLE `authors` ( 104 | `author_id` int(11) NOT NULL, 105 | `first_name` varchar(50) NOT NULL, 106 | `last_name` varchar(50) NOT NULL, 107 | `email` varchar(100) NOT NULL UNIQUE, 108 | `birthdate` date NOT NULL, 109 | `added` timestamp NOT NULL DEFAULT current_timestamp, 110 | PRIMARY KEY (`author_id`) 111 | ); 112 | DROP TABLE IF EXISTS `posts`; 113 | CREATE TABLE `posts` ( 114 | `post_id` int(11) NOT NULL, 115 | `author_id` int(11) NOT NULL, 116 | `title` varchar(255) NOT NULL, 117 | `description` varchar(500) NOT NULL, 118 | `content` text NOT NULL, 119 | `date` date NOT NULL, 120 | PRIMARY KEY (`post_id`) 121 | ); 122 | #> -------------------------------------------------------------------------------- /Tests/Join-Object.Tests.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules Pester 2 | #Requires -Modules @{ ModuleName = 'Assert' ; ModuleVersion = '999.9.5' } 3 | [CmdletBinding()] 4 | Param 5 | ( 6 | [Switch]$SaveMode, 7 | [String[]]$FilterTests 8 | ) 9 | $EquivalencyOption = Get-EquivalencyOption -Comparator 'StrictEquality'-StrictOrder 10 | 11 | if ($PSScriptRoot) { 12 | $ScriptRoot = $PSScriptRoot 13 | } 14 | elseif ($psISE.CurrentFile.IsUntitled -eq $false) { 15 | $ScriptRoot = Split-Path -Path $psISE.CurrentFile.FullPath 16 | } 17 | elseif ($null -ne $psEditor.GetEditorContext().CurrentFile.Path -and $psEditor.GetEditorContext().CurrentFile.Path -notlike 'untitled:*') { 18 | $ScriptRoot = Split-Path -Path $psEditor.GetEditorContext().CurrentFile.Path 19 | } 20 | else { 21 | $ScriptRoot = '.' 22 | } 23 | . "$ScriptRoot\TestHelpers.ps1" 24 | 25 | $DataSetSmall = { 26 | $PSCustomObject = @( 27 | [PSCustomObject]@{ ID = 1 ; Sub = 'S1' ; IntO = 6 } 28 | [PSCustomObject]@{ ID = 2 ; Sub = 'S2' ; IntO = 7 } 29 | [PSCustomObject]@{ ID = 3 ; Sub = 'S3' ; IntO = $null } 30 | ) 31 | 32 | $PSCustomObjectJunk = @( 33 | [PSCustomObject]@{ ID = 1 ; Sub = 'S1' ; IntO = 6 ; Junk = 'New1' } 34 | [PSCustomObject]@{ ID = 2 ; Sub = 'S2' ; IntO = 7 ; Junk = $null } 35 | [PSCustomObject]@{ ID = 3 ; Sub = 'S3' ; IntO = $null ; Junk = $null } 36 | ) 37 | 38 | $PSCustomObjectKeyArray = @( 39 | [PSCustomObject]@{ID = 1, 2, 3 ; Sub = 'S1' ; IntO = 6 } 40 | [PSCustomObject]@{ID = 4 ; Sub = 'S4' ; IntO = $null } 41 | ) 42 | 43 | $DataTable = [Data.DataTable]::new('Test') 44 | $null = $DataTable.Columns.Add('IDD', [System.Int32]) 45 | $null = $DataTable.Columns.Add('Name') 46 | $null = $DataTable.Columns.Add('Junk') 47 | $null = $DataTable.Columns.Add('IntT', [System.Int32]) 48 | $null = $DataTable.Rows.Add(1, 'A', 'AAA', 5) 49 | $null = $DataTable.Rows.Add(3, 'C', 'S3', $null) 50 | $null = $DataTable.Rows.Add(4, 'D', $null, $null) 51 | 52 | $PSCustomObjectMulti = @( 53 | [PSCustomObject]@{ ID = 1 ; Sub = 'S1' ; IntO = 6 } 54 | [PSCustomObject]@{ ID = 1 ; Sub = 'S12' ; IntO = 62 } 55 | [PSCustomObject]@{ ID = 2 ; Sub = 'S2' ; IntO = 7 } 56 | [PSCustomObject]@{ ID = 2 ; Sub = 'S22' ; IntO = 72 } 57 | ) 58 | 59 | $DataTableMulti = [Data.DataTable]::new('Test') 60 | $null = $DataTableMulti.Columns.Add('IDD', [System.Int32]) 61 | $null = $DataTableMulti.Columns.Add('Name') 62 | $null = $DataTableMulti.Columns.Add('Junk') 63 | $null = $DataTableMulti.Columns.Add('IntT', [System.Int32]) 64 | $null = $DataTableMulti.Rows.Add(1, 'A', 'AAA', 5) 65 | $null = $DataTableMulti.Rows.Add(1, 'A2', 'AAA2', 52) 66 | $null = $DataTableMulti.Rows.Add(3, 'C', 'S3', $null) 67 | $null = $DataTableMulti.Rows.Add(3, 'C2', 'S32', $null) 68 | } 69 | #. $DataSetSmall 70 | 71 | Describe -Name 'Join-Object' -Fixture { 72 | Context -Name ($DataSetName = 'DataSetSmall') -Fixture { 73 | Class EqualityComparerMy : Collections.Generic.EqualityComparer[Object] { 74 | [bool] Equals([Object]$Object1 , [Object]$Object2) { 75 | if ($Object1 -contains $Object2 -or $Object1 -in $Object2) { 76 | return $true 77 | } 78 | else { 79 | return $false 80 | } 81 | } 82 | [int] GetHashCode([Object]$Object) { 83 | return 1 84 | } 85 | } 86 | $TestCases = @( 87 | Format-Test @{ 88 | Description = 'Error, Mismatch' 89 | ExpectedErrorOn = 'Test' 90 | ExpectedErrorMessage = "Object{ID=3; R_IntT=; R_Name=X; Subscription=S3}'." 91 | Params = @{ 92 | Left = 'PSCustomObject' 93 | Right = 'DataTable' 94 | LeftJoinProperty = 'ID' 95 | RightJoinProperty = 'IDD' 96 | LeftProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 97 | ExcludeRightProperties = 'Junk' 98 | Prefix = 'R_' 99 | } 100 | } 101 | Format-Test @{ 102 | Description = 'Error, Mismatch' 103 | ExpectedErrorOn = 'Test' 104 | ExpectedErrorMessage = "but some values were missing: 'PSObject{IDD=3; IntT=; Junk=S3; Name=X; R_IntO=; R_Sub=S3}" 105 | Params = @{ 106 | Left = 'DataTable' 107 | Right = 'PSCustomObject' 108 | LeftJoinProperty = 'IDD' 109 | RightJoinProperty = 'ID' 110 | ExcludeRightProperties = 'Junk' 111 | Prefix = 'R_' 112 | DataTable = $true 113 | } 114 | } 115 | 116 | Format-Test @{ 117 | Params = @{ 118 | Left = 'PSCustomObject' 119 | Right = 'DataTable' 120 | LeftJoinProperty = 'ID' 121 | RightJoinProperty = 'IDD' 122 | LeftProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 123 | ExcludeRightProperties = 'Junk' 124 | Prefix = 'R_' 125 | } 126 | } 127 | Format-Test @{ 128 | Params = @{ 129 | Left = 'DataTable' 130 | Right = 'PSCustomObject' 131 | LeftJoinProperty = 'IDD' 132 | RightJoinProperty = 'ID' 133 | RightProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 134 | ExcludeLeftProperties = 'Junk' 135 | Suffix = '_R' 136 | } 137 | } 138 | Format-Test @{ 139 | Params = @{ 140 | Left = 'PSCustomObject' 141 | Right = 'DataTable' 142 | LeftJoinProperty = 'ID' 143 | RightJoinProperty = 'IDD' 144 | LeftProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 145 | ExcludeRightProperties = 'Junk' 146 | Prefix = 'R_' 147 | Type = 'AllInBoth' 148 | } 149 | } 150 | Format-Test @{ 151 | Params = @{ 152 | Left = 'DataTable' 153 | Right = 'PSCustomObject' 154 | LeftJoinProperty = 'IDD' 155 | RightJoinProperty = 'ID' 156 | RightProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 157 | ExcludeLeftProperties = 'Junk' 158 | Suffix = '_R' 159 | Type = 'AllInBoth' 160 | } 161 | } 162 | Format-Test @{ 163 | Params = @{ 164 | Left = 'PSCustomObject' 165 | Right = 'DataTable' 166 | LeftJoinProperty = 'ID' 167 | RightJoinProperty = 'IDD' 168 | LeftProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 169 | ExcludeRightProperties = 'Junk' 170 | Prefix = 'R_' 171 | Type = 'OnlyIfInBoth' 172 | } 173 | } 174 | Format-Test @{ 175 | Params = @{ 176 | Left = 'DataTable' 177 | Right = 'PSCustomObject' 178 | LeftJoinProperty = 'IDD' 179 | RightJoinProperty = 'ID' 180 | RightProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 181 | ExcludeLeftProperties = 'Junk' 182 | Suffix = '_R' 183 | Type = 'OnlyIfInBoth' 184 | } 185 | } 186 | 187 | Format-Test @{ 188 | Params = @{ 189 | Left = 'PSCustomObject' 190 | Right = 'DataTable' 191 | LeftJoinProperty = 'ID' 192 | RightJoinProperty = 'IDD' 193 | LeftProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 194 | ExcludeRightProperties = 'Junk' 195 | Prefix = 'R_' 196 | PassThru = $true 197 | } 198 | } 199 | Format-Test @{ 200 | Params = @{ 201 | Left = 'DataTable' 202 | Right = 'PSCustomObject' 203 | LeftJoinProperty = 'IDD' 204 | RightJoinProperty = 'ID' 205 | LeftProperties = @{ IDD = 'IDD' ; Name = 'NewName' } 206 | RightProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 207 | ExcludeLeftProperties = 'Junk' 208 | Suffix = '_R' 209 | PassThru = $true 210 | } 211 | } 212 | Format-Test @{ 213 | Description = 'Error, PassThru+AllInBoth' 214 | ExpectedErrorOn = 'Run' 215 | ExpectedErrorMessage = '"-PassThru" compatible only with "-Type AllInLeft" with "-RightMultiMode" "SingleOnly" or "SubGroups"' 216 | Params = @{ 217 | Left = 'PSCustomObject' 218 | Right = 'DataTable' 219 | LeftJoinProperty = 'ID' 220 | RightJoinProperty = 'IDD' 221 | LeftProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 222 | ExcludeRightProperties = 'Junk' 223 | Prefix = 'R_' 224 | PassThru = $true 225 | Type = 'AllInBoth' 226 | } 227 | } 228 | # PassThru cannot add lines. 229 | Format-Test @{ 230 | Description = 'Error, PassThru+AllInBoth' 231 | ExpectedErrorOn = 'Run' 232 | ExpectedErrorMessage = '"-PassThru" compatible only with "-Type AllInLeft" with "-RightMultiMode" "SingleOnly" or "SubGroups"' 233 | Params = @{ 234 | Left = 'DataTable' 235 | Right = 'PSCustomObject' 236 | LeftJoinProperty = 'IDD' 237 | RightJoinProperty = 'ID' 238 | LeftProperties = @{ IDD = 'IDD' ; Name = 'NewName' } 239 | RightProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 240 | ExcludeLeftProperties = 'Junk' 241 | Suffix = '_R' 242 | PassThru = $true 243 | Type = 'AllInBoth' 244 | } 245 | } 246 | # PassThru cannot remove lines. 247 | Format-Test @{ 248 | Description = 'Error, PassThru+OnlyIfInBoth' 249 | ExpectedErrorOn = 'Run' 250 | ExpectedErrorMessage = '"-PassThru" compatible only with "-Type AllInLeft" with "-RightMultiMode" "SingleOnly" or "SubGroups"' 251 | Params = @{ 252 | Left = 'PSCustomObjectMulti' 253 | Right = 'DataTableMulti' 254 | LeftJoinProperty = 'ID' 255 | RightJoinProperty = 'IDD' 256 | ExcludeRightProperties = 'Junk' 257 | Prefix = 'R_' 258 | PassThru = $true 259 | Type = 'OnlyIfInBoth' 260 | } 261 | } 262 | # PassThru cannot duplicate lines. 263 | Format-Test @{ 264 | Description = 'Error, PassThru+AllInLeft+RightDuplicate' 265 | ExpectedErrorOn = 'Run' 266 | ExpectedErrorMessage = '"-PassThru" compatible only with "-Type AllInLeft" with "-RightMultiMode" "SingleOnly" or "SubGroups"' 267 | Params = @{ 268 | Left = 'PSCustomObjectMulti' 269 | Right = 'DataTableMulti' 270 | LeftJoinProperty = 'ID' 271 | RightJoinProperty = 'IDD' 272 | ExcludeRightProperties = 'Junk' 273 | Prefix = 'R_' 274 | PassThru = $true 275 | Type = 'AllInLeft' 276 | RightMultiMode = 'DuplicateLines' 277 | } 278 | } 279 | 280 | Format-Test @{ 281 | Params = @{ 282 | Left = 'DataTable' 283 | Right = 'PSCustomObject' 284 | LeftJoinProperty = 'IDD' 285 | RightJoinProperty = 'ID' 286 | ExcludeRightProperties = 'Junk' 287 | Prefix = 'R_' 288 | DataTable = $true 289 | } 290 | } 291 | Format-Test @{ 292 | Params = @{ 293 | Left = 'PSCustomObject' 294 | Right = 'DataTable' 295 | LeftJoinProperty = 'ID' 296 | RightJoinProperty = 'IDD' 297 | ExcludeRightProperties = 'Junk' 298 | Prefix = 'R_' 299 | DataTable = $true 300 | } 301 | } 302 | Format-Test @{ 303 | Params = @{ 304 | Left = 'PSCustomObject' 305 | Right = 'DataTable' 306 | LeftJoinProperty = 'ID' 307 | RightJoinProperty = 'IDD' 308 | ExcludeRightProperties = 'Junk' 309 | Prefix = 'R_' 310 | DataTable = $true 311 | Type = 'AllInBoth' 312 | } 313 | } 314 | Format-Test @{ 315 | Params = @{ 316 | Left = 'DataTable' 317 | Right = 'PSCustomObject' 318 | LeftJoinProperty = 'IDD' 319 | RightJoinProperty = 'ID' 320 | ExcludeRightProperties = 'Junk' 321 | Prefix = 'R_' 322 | DataTable = $true 323 | Type = 'AllInBoth' 324 | } 325 | } 326 | Format-Test @{ 327 | Params = @{ 328 | Left = 'PSCustomObject' 329 | Right = 'DataTable' 330 | LeftJoinProperty = 'ID' 331 | RightJoinProperty = 'IDD' 332 | ExcludeRightProperties = 'Junk' 333 | Prefix = 'R_' 334 | DataTable = $true 335 | Type = 'OnlyIfInBoth' 336 | } 337 | } 338 | Format-Test @{ 339 | Params = @{ 340 | Left = 'DataTable' 341 | Right = 'PSCustomObject' 342 | LeftJoinProperty = 'IDD' 343 | RightJoinProperty = 'ID' 344 | ExcludeRightProperties = 'Junk' 345 | Prefix = 'R_' 346 | DataTable = $true 347 | Type = 'OnlyIfInBoth' 348 | } 349 | } 350 | 351 | Format-Test @{ 352 | Description = 'Ordered' 353 | Params = @{ 354 | Left = 'PSCustomObject' 355 | Right = 'DataTable' 356 | LeftJoinProperty = 'ID' 357 | RightJoinProperty = 'IDD' 358 | LeftProperties = [Ordered]@{ Sub = 'Subscription' ; ID = 'ID' } 359 | ExcludeRightProperties = 'Junk' 360 | Prefix = 'R_' 361 | } 362 | } 363 | Format-Test @{ 364 | Description = 'DBNull to $null' 365 | Params = @{ 366 | Left = 'PSCustomObject' 367 | Right = 'DataTable' 368 | LeftJoinProperty = 'ID' 369 | RightJoinProperty = 'IDD' 370 | } 371 | } 372 | Format-Test @{ 373 | Description = 'Single' 374 | Params = @{ 375 | Left = 'PSCustomObject[0]' 376 | Right = 'DataTable' 377 | LeftJoinProperty = 'ID' 378 | RightJoinProperty = 'IDD' 379 | LeftProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 380 | ExcludeRightProperties = 'Junk' 381 | Prefix = 'R_' 382 | } 383 | } 384 | Format-Test @{ 385 | Description = 'KeepRightJoinProperty' 386 | Params = @{ 387 | Left = 'PSCustomObject' 388 | Right = 'DataTable' 389 | LeftJoinProperty = 'ID' 390 | RightJoinProperty = 'IDD' 391 | LeftProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 392 | ExcludeRightProperties = 'Junk' 393 | Prefix = 'R_' 394 | KeepRightJoinProperty = $true 395 | } 396 | } 397 | Format-Test @{ 398 | Description = 'Multi Join' 399 | Params = @{ 400 | Left = 'PSCustomObject' 401 | Right = 'DataTable' 402 | LeftJoinProperty = 'ID', 'Sub' 403 | RightJoinProperty = 'IDD', 'Junk' 404 | LeftProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 405 | ExcludeRightProperties = 'Junk' 406 | Prefix = 'R_' 407 | } 408 | } 409 | Format-Test @{ 410 | Description = 'JoinScript' 411 | Params = @{ 412 | Left = 'PSCustomObject' 413 | Right = 'DataTable' 414 | LeftJoinProperty = 'Sub' 415 | RightJoinProperty = 'IDD' 416 | LeftProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 417 | ExcludeRightProperties = 'Junk' 418 | Prefix = 'R_' 419 | LeftJoinScript = { param ($Line) ($Line.Sub).Replace('S', 'X') } 420 | RightJoinScript = { param ($Line) 'X' + $Line.IDD } 421 | } 422 | } 423 | Format-Test @{ 424 | Description = 'JoinScript String' 425 | Params = @{ 426 | Left = 'PSCustomObject' 427 | Right = 'DataTable' 428 | LeftJoinProperty = 'Sub' 429 | RightJoinProperty = 'IDD' 430 | LeftProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 431 | ExcludeRightProperties = 'Junk' 432 | Prefix = 'R_' 433 | LeftJoinScript = { param ($Line) ($Line.Sub).Replace('S', '') } 434 | } 435 | } 436 | Format-Test @{ 437 | Description = 'DataTableTypes' 438 | Params = @{ 439 | Left = 'DataTable' 440 | Right = 'PSCustomObject' 441 | LeftJoinProperty = 'IDD' 442 | RightJoinProperty = 'ID' 443 | ExcludeRightProperties = 'Junk' 444 | Prefix = 'R_' 445 | DataTable = $true 446 | DataTableTypes = @{ R_IntO = [Int] } 447 | } 448 | } 449 | Format-Test @{ 450 | Description = 'DataTableTypes' 451 | Params = @{ 452 | Left = 'DataTable' 453 | Right = 'PSCustomObject' 454 | LeftJoinProperty = 'IDD' 455 | RightJoinProperty = 'ID' 456 | ExcludeRightProperties = 'Junk' 457 | Prefix = 'R_' 458 | PassThru = $true 459 | DataTableTypes = @{ R_IntO = [Int] } 460 | } 461 | } 462 | Format-Test @{ 463 | Description = 'Error, No AllowColumnsMerging' 464 | ExpectedErrorOn = 'Run' 465 | ExpectedErrorMessage = "Item has already been added. Key in dictionary: 'Junk'" 466 | Params = @{ 467 | Left = 'DataTable' 468 | Right = 'PSCustomObjectJunk' 469 | LeftJoinProperty = 'IDD' 470 | RightJoinProperty = 'ID' 471 | } 472 | } 473 | Format-Test @{ 474 | Description = 'Error, AllowColumnsMerging-DataTable' 475 | ExpectedErrorOn = 'Run' 476 | ExpectedErrorMessage = '"-AllowColumnsMerging" support only on DataTable output' 477 | Params = @{ 478 | Left = 'DataTable' 479 | Right = 'PSCustomObjectJunk' 480 | LeftJoinProperty = 'IDD' 481 | RightJoinProperty = 'ID' 482 | AllowColumnsMerging = $true 483 | } 484 | } 485 | Format-Test @{ 486 | Description = 'AllowColumnsMerging' 487 | Params = @{ 488 | Left = 'DataTable' 489 | Right = 'PSCustomObjectJunk' 490 | LeftJoinProperty = 'IDD' 491 | RightJoinProperty = 'ID' 492 | AllowColumnsMerging = $true 493 | DataTable = $true 494 | } 495 | } 496 | Format-Test @{ 497 | Description = 'Error, AllowColumnsMerging-OutDataTable' 498 | ExpectedErrorOn = 'Run' 499 | ExpectedErrorMessage = '"-AllowColumnsMerging" support only on DataTable output' 500 | Params = @{ 501 | Left = 'PSCustomObjectJunk' 502 | Right = 'DataTable' 503 | LeftJoinProperty = 'IDD' 504 | RightJoinProperty = 'ID' 505 | AllowColumnsMerging = $true 506 | PassThru = $true 507 | } 508 | } 509 | Format-Test @{ 510 | Description = 'AllowColumnsMerging' 511 | Params = @{ 512 | Left = 'DataTable' 513 | Right = 'PSCustomObjectJunk' 514 | LeftJoinProperty = 'IDD' 515 | RightJoinProperty = 'ID' 516 | AllowColumnsMerging = $true 517 | PassThru = $true 518 | } 519 | } 520 | Format-Test @{ 521 | Description = 'AllowColumnsMerging' 522 | Params = @{ 523 | Left = 'DataTable' 524 | Right = '[DataTable]PSCustomObjectJunk' 525 | LeftJoinProperty = 'IDD' 526 | RightJoinProperty = 'ID' 527 | AllowColumnsMerging = $true 528 | PassThru = $true 529 | } 530 | } 531 | Format-Test @{ 532 | Description = 'Comparer' 533 | Params = @{ 534 | Left = 'DataTable' 535 | Right = 'PSCustomObjectKeyArray' 536 | LeftJoinProperty = 'IDD' 537 | RightJoinProperty = 'ID' 538 | ExcludeRightProperties = 'Junk' 539 | Prefix = 'R_' 540 | RightJoinScript = [System.Func[Object, Object]] { param ($Line) $Line.ID } 541 | Comparer = [EqualityComparerMy]::new() 542 | } 543 | } 544 | 545 | Format-Test @{ 546 | Description = 'Error, SingleOnly+BasicJoin' 547 | ExpectedErrorOn = 'Run' 548 | ExpectedErrorMessage = '"-Type AllInLeft" and "-Type OnlyIfInBoth" support only "-LeftMultiMode DuplicateLines"' 549 | Params = @{ 550 | Left = 'PSCustomObjectMulti' 551 | Right = 'DataTableMulti' 552 | LeftJoinProperty = 'ID' 553 | RightJoinProperty = 'IDD' 554 | ExcludeRightProperties = 'Junk' 555 | Prefix = 'R_' 556 | DataTable = $true 557 | Type = 'AllInLeft' 558 | LeftMultiMode = 'SingleOnly' 559 | } 560 | } 561 | Format-Test @{ 562 | Description = 'Error, SingleOnly+BasicJoin' 563 | ExpectedErrorOn = 'Run' 564 | ExpectedErrorMessage = '"-Type OnlyIfInBoth" support only "-RightMultiMode DuplicateLines"' 565 | Params = @{ 566 | Left = 'DataTableMulti' 567 | Right = 'PSCustomObjectMulti' 568 | LeftJoinProperty = 'IDD' 569 | RightJoinProperty = 'ID' 570 | Prefix = 'R_' 571 | DataTable = $true 572 | Type = 'OnlyIfInBoth' 573 | RightMultiMode = 'SingleOnly' 574 | } 575 | } 576 | Format-Test @{ 577 | Description = 'Error, SingleOnly' 578 | ExpectedErrorOn = 'Run' 579 | ExpectedErrorMessage = 'Sequence contains more than one element' 580 | Params = @{ 581 | Left = 'PSCustomObjectMulti' 582 | Right = 'DataTableMulti' 583 | LeftJoinProperty = 'ID' 584 | RightJoinProperty = 'IDD' 585 | ExcludeRightProperties = 'Junk' 586 | Prefix = 'R_' 587 | DataTable = $true 588 | Type = 'AllInLeft' 589 | RightMultiMode = 'SingleOnly' 590 | } 591 | } 592 | Format-Test @{ 593 | Description = 'Error, SingleOnly' 594 | ExpectedErrorOn = 'Run' 595 | ExpectedErrorMessage = 'Sequence contains more than one element' 596 | Params = @{ 597 | Left = 'PSCustomObjectMulti' 598 | Right = 'DataTableMulti' 599 | LeftJoinProperty = 'ID' 600 | RightJoinProperty = 'IDD' 601 | ExcludeRightProperties = 'Junk' 602 | Prefix = 'R_' 603 | DataTable = $true 604 | Type = 'AllInBoth' 605 | LeftMultiMode = 'SingleOnly' 606 | } 607 | } 608 | Format-Test @{ 609 | Description = 'DuplicateLines' 610 | Params = @{ 611 | Left = 'PSCustomObjectMulti' 612 | Right = 'DataTableMulti' 613 | LeftJoinProperty = 'ID' 614 | RightJoinProperty = 'IDD' 615 | ExcludeRightProperties = 'Junk' 616 | Prefix = 'R_' 617 | DataTable = $true 618 | Type = 'AllInBoth' 619 | LeftMultiMode = 'DuplicateLines' 620 | RightMultiMode = 'DuplicateLines' 621 | } 622 | } 623 | Format-Test @{ 624 | Description = 'DuplicateLines' 625 | Params = @{ 626 | Left = 'DataTableMulti' 627 | Right = 'PSCustomObjectMulti' 628 | LeftJoinProperty = 'IDD' 629 | RightJoinProperty = 'ID' 630 | Prefix = 'R_' 631 | DataTable = $true 632 | Type = 'AllInBoth' 633 | LeftMultiMode = 'DuplicateLines' 634 | RightMultiMode = 'DuplicateLines' 635 | } 636 | } 637 | Format-Test @{ 638 | Description = 'SubGroups' 639 | Params = @{ 640 | Left = 'PSCustomObjectMulti' 641 | Right = 'DataTableMulti' 642 | LeftJoinProperty = 'ID' 643 | RightJoinProperty = 'IDD' 644 | ExcludeRightProperties = 'Junk' 645 | Prefix = 'R_' 646 | DataTable = $true 647 | Type = 'AllInBoth' 648 | LeftMultiMode = 'SubGroups' 649 | RightMultiMode = 'SubGroups' 650 | } 651 | } 652 | Format-Test @{ 653 | Description = 'SubGroups' 654 | Params = @{ 655 | Left = 'DataTableMulti' 656 | Right = 'PSCustomObjectMulti' 657 | LeftJoinProperty = 'IDD' 658 | RightJoinProperty = 'ID' 659 | Prefix = 'R_' 660 | DataTable = $true 661 | Type = 'AllInBoth' 662 | LeftMultiMode = 'SubGroups' 663 | RightMultiMode = 'SubGroups' 664 | } 665 | } 666 | Format-Test @{ 667 | Description = 'SubGroups' 668 | Params = @{ 669 | Left = 'PSCustomObjectMulti' 670 | Right = 'DataTableMulti' 671 | LeftJoinProperty = 'ID' 672 | RightJoinProperty = 'IDD' 673 | ExcludeRightProperties = 'Junk' 674 | Prefix = 'R_' 675 | Type = 'AllInBoth' 676 | LeftMultiMode = 'SubGroups' 677 | RightMultiMode = 'SubGroups' 678 | } 679 | } 680 | Format-Test @{ 681 | Description = 'SubGroups' 682 | Params = @{ 683 | Left = 'DataTableMulti' 684 | Right = 'PSCustomObjectMulti' 685 | LeftJoinProperty = 'IDD' 686 | RightJoinProperty = 'ID' 687 | Prefix = 'R_' 688 | Type = 'AllInBoth' 689 | LeftMultiMode = 'SubGroups' 690 | RightMultiMode = 'SubGroups' 691 | } 692 | } 693 | Format-Test @{ 694 | Description = 'SubGroups' 695 | Params = @{ 696 | Left = 'PSCustomObjectMulti' 697 | Right = 'DataTableMulti' 698 | LeftJoinProperty = 'ID' 699 | RightJoinProperty = 'IDD' 700 | ExcludeRightProperties = 'Junk' 701 | Prefix = 'R_' 702 | Type = 'AllInLeft' 703 | RightMultiMode = 'SubGroups' 704 | PassThru = $true 705 | } 706 | } 707 | Format-Test @{ 708 | Description = 'SubGroups' 709 | Params = @{ 710 | Left = 'PSCustomObjectMulti' 711 | Right = '[PSCustomObject]DataTableMulti' 712 | LeftJoinProperty = 'ID' 713 | RightJoinProperty = 'IDD' 714 | ExcludeRightProperties = 'Junk' 715 | Prefix = 'R_' 716 | Type = 'AllInLeft' 717 | RightMultiMode = 'SubGroups' 718 | PassThru = $true 719 | } 720 | } 721 | Format-Test @{ 722 | Description = 'SubGroups' 723 | Params = @{ 724 | Left = 'DataTableMulti' 725 | Right = 'PSCustomObjectMulti' 726 | LeftJoinProperty = 'IDD' 727 | RightJoinProperty = 'ID' 728 | Prefix = 'R_' 729 | Type = 'AllInLeft' 730 | RightMultiMode = 'SubGroups' 731 | PassThru = $true 732 | } 733 | } 734 | Format-Test @{ 735 | Description = 'SubGroups' 736 | Params = @{ 737 | Left = 'DataTableMulti' 738 | Right = '[DataTable]PSCustomObjectMulti' 739 | LeftJoinProperty = 'IDD' 740 | RightJoinProperty = 'ID' 741 | Prefix = 'R_' 742 | Type = 'AllInLeft' 743 | RightMultiMode = 'SubGroups' 744 | PassThru = $true 745 | } 746 | } 747 | Format-Test @{ 748 | Description = 'SubGroups Key' 749 | Params = @{ 750 | Left = 'PSCustomObjectMulti' 751 | Right = 'DataTableMulti' 752 | LeftJoinProperty = 'ID' 753 | RightJoinProperty = 'IDD' 754 | ExcludeRightProperties = 'Junk' 755 | Prefix = 'R_' 756 | Type = 'AllInBoth' 757 | LeftMultiMode = 'SubGroups' 758 | RightMultiMode = 'SubGroups' 759 | AddKey = 'JoinKey' 760 | } 761 | } 762 | Format-Test @{ 763 | Description = 'Error, AddKey-AllInBoth' 764 | ExpectedErrorOn = 'Run' 765 | ExpectedErrorMessage = '"-AddKey" support only "-Type AllInBoth"' 766 | Params = @{ 767 | Left = 'PSCustomObjectMulti' 768 | Right = 'DataTableMulti' 769 | LeftJoinProperty = 'ID' 770 | RightJoinProperty = 'IDD' 771 | ExcludeRightProperties = 'Junk' 772 | Prefix = 'R_' 773 | LeftMultiMode = 'SubGroups' 774 | RightMultiMode = 'SubGroups' 775 | AddKey = 'JoinKey' 776 | } 777 | } 778 | Format-Test @{ 779 | Description = 'SubGroups Key' 780 | Params = @{ 781 | Left = 'PSCustomObjectMulti' 782 | Right = 'DataTableMulti' 783 | LeftJoinProperty = 'ID' 784 | RightJoinProperty = 'IDD' 785 | ExcludeRightProperties = 'Junk' 786 | ExcludeLeftProperties = 'ID' 787 | Prefix = 'R_' 788 | Type = 'AllInBoth' 789 | LeftMultiMode = 'DuplicateLines' 790 | RightMultiMode = 'SubGroups' 791 | AddKey = 'JoinKey' 792 | DataTable = $true 793 | KeepRightJoinProperty = $true 794 | } 795 | } 796 | Format-Test @{ 797 | Description = 'SubArray' 798 | Params = @{ 799 | Left = 'PSCustomObjectMulti' 800 | Right = 'DataTable' 801 | LeftJoinProperty = 'ID' 802 | RightJoinProperty = 'IDD' 803 | Prefix = 'R_' 804 | } 805 | } 806 | Format-Test @{ 807 | Description = 'SubArray' 808 | #RunScript = { Set-ItResult -Pending -Because 'Bug?' } 809 | Params = @{ 810 | Left = 'DataTable' 811 | Right = 'PSCustomObjectMulti' 812 | LeftJoinProperty = 'IDD' 813 | RightJoinProperty = 'ID' 814 | Prefix = 'R_' 815 | RightMultiMode = 'DuplicateLines' 816 | } 817 | } 818 | Format-Test @{ 819 | Params = @{ 820 | Left = '[Collections.ArrayList]PSCustomObject' 821 | Right = 'DataTable' 822 | LeftJoinProperty = 'ID' 823 | RightJoinProperty = 'IDD' 824 | LeftProperties = @{ ID = 'ID' ; Sub = 'Subscription' } 825 | ExcludeRightProperties = 'Junk' 826 | Prefix = 'R_' 827 | } 828 | } 829 | ) 830 | if ($FilterTests) { 831 | $TestCases = $TestCases | Where-Object TestName -In $FilterTests 832 | } 833 | It -Name "Testing: " -TestCases $TestCases -Test { 834 | param ( 835 | $Params, 836 | $DataSet, 837 | $TestName, 838 | $RunScript, 839 | $ExpectedErrorMessage, 840 | [ValidateSet('Test', 'Run')] 841 | $ExpectedErrorOn 842 | ) 843 | if ($RunScript) { 844 | . $RunScript 845 | } 846 | 847 | # Load Data 848 | . $DataSet 849 | $Params.Left = Get-Params -Param $Params.Left 850 | $Params.Right = Get-Params -Param $Params.Right 851 | 852 | # Save Before Data Copy 853 | $BeforeLeft = [System.Management.Automation.PSSerializer]::Deserialize([System.Management.Automation.PSSerializer]::Serialize($Params.Left, 3)) 854 | $BeforeRight = [System.Management.Automation.PSSerializer]::Deserialize([System.Management.Automation.PSSerializer]::Serialize($Params.Right, 3)) 855 | 856 | # Execute Cmdlet 857 | if ($ExpectedErrorOn -eq 'Run') { 858 | { $JoinedOutput = Join-Object @Params } | Should -Throw -ExpectedMessage $ExpectedErrorMessage 859 | Continue 860 | } 861 | else { 862 | $JoinedOutput = Join-Object @Params 863 | } 864 | Write-Verbose ('it returns:' + ($JoinedOutput | Format-Table | Out-String)) 865 | 866 | # Save CompareData (Xml) 867 | if ($SaveMode) { 868 | try { 869 | $CompareDataXml = (Get-Content -LiteralPath "$ScriptRoot\CompareData\$TestName.xml" -ErrorAction 'Stop') -join [Environment]::NewLine 870 | $CompareDataNew = [System.Management.Automation.PSSerializer]::Deserialize($CompareDataXml) 871 | } 872 | catch { 873 | $CompareDataNew = 'None' 874 | } 875 | Write-Host ("{0}`nLeft:{1}Right:{2}Params:{3}Result:{4}Replacing:{5}" -f $TestName, 876 | ($Params.Left | Format-Table | Out-String), ($Params.Right | Format-Table | Out-String), 877 | (([PSCustomObject]$Params) | Select-Object -ExcludeProperty 'Left', 'Right' -Property '*' | Format-List | Out-String), 878 | ($JoinedOutput | Format-Table | Out-String), ($CompareDataNew | Format-Table | Out-String) 879 | ) 880 | 881 | if ($JoinedOutput -is [Array] -and ($SubArrayTest = $JoinedOutput | ForEach-Object { $_ -is [Array] }) -contains $true) { 882 | Write-Warning ("SubArrayTest $SubArrayTest") 883 | } 884 | Export-Clixml -LiteralPath "$ScriptRoot\CompareData\$TestName.xml" -InputObject $JoinedOutput -Depth 3 -Confirm 885 | } 886 | 887 | # Get CompareData 888 | $CompareDataXml = (Get-Content -LiteralPath "$ScriptRoot\CompareData\$TestName.xml") -join [Environment]::NewLine 889 | $CompareDataNew = [System.Management.Automation.PSSerializer]::Deserialize($CompareDataXml) 890 | Write-Verbose ('it should return:' + ($CompareDataNew | Format-Table | Out-String)) 891 | 892 | # Test 893 | if ($ExpectedErrorOn -eq 'Test') { 894 | { Assert-Equivalent -Actual $JoinedOutput -Expected $CompareDataNew -Options $EquivalencyOption } | Should -Throw -ExpectedMessage $ExpectedErrorMessage 895 | } 896 | else { 897 | Assert-Equivalent -Actual $JoinedOutput -Expected $CompareDataNew -Options $EquivalencyOption 898 | } 899 | 900 | if ($Params.PassThru) { 901 | Assert-Equivalent -Actual $Params.Left -Expected $CompareDataNew -Options $EquivalencyOption 902 | } 903 | else { 904 | Assert-Equivalent -Actual $Params.Left -Expected $BeforeLeft -Options $EquivalencyOption 905 | } 906 | Assert-Equivalent -Actual $Params.Right -Expected $BeforeRight -Options $EquivalencyOption 907 | 908 | if (!$Params.PassThru) { 909 | if ($Params.DataTable) { 910 | Should -BeOfType -ActualValue $JoinedOutput -ExpectedType 'System.Data.DataTable' 911 | $JoinedOutput | Should -BeOfType -ExpectedType 'System.Data.DataRow' 912 | } 913 | else { 914 | if ($JoinedOutput.Count -gt 1) { 915 | Should -BeOfType -ActualValue $JoinedOutput -ExpectedType 'System.Array' 916 | } 917 | else { 918 | Should -BeOfType -ActualValue $JoinedOutput -ExpectedType 'PSCustomObject' 919 | } 920 | $JoinedOutput | Should -BeOfType -ExpectedType 'PSCustomObject' 921 | } 922 | } 923 | } 924 | } 925 | } -------------------------------------------------------------------------------- /Tests/TestDataSetBig100K.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/TestDataSetBig100K.db -------------------------------------------------------------------------------- /Tests/TestDataSetBig10k.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ili101/Join-Object/4aa6b018c731cbf7cb386d0f2c9b814802fc668a/Tests/TestDataSetBig10k.db -------------------------------------------------------------------------------- /Tests/TestHelpers.ps1: -------------------------------------------------------------------------------- 1 | function ConvertFrom-DataTable { 2 | <# 3 | .SYNOPSIS 4 | Convert DataTable to PSCustomObject, Support Deserialized DataTable. 5 | #> 6 | [CmdletBinding()] 7 | param 8 | ( 9 | [Parameter(Mandatory, ValueFromPipeline)] 10 | [ValidateScript( { $_ -is [System.Data.DataRow] } )] 11 | $InputObject 12 | ) 13 | begin { 14 | $First = $true 15 | } 16 | process { 17 | if ($First) { 18 | $First = $false 19 | $Properties = if ($InputObject -is [System.Data.DataTable]) { 20 | $InputObject.Columns.ColumnName 21 | } 22 | else { 23 | $InputObject.PSObject.Properties.Name | Where-Object { $_ -notin ('RowError', 'RowState', 'Table', 'ItemArray', 'HasErrors') } 24 | } 25 | } 26 | foreach ($DataRow in $InputObject) { 27 | $RowHash = [ordered]@{ } 28 | foreach ($Property in $Properties) { 29 | if ($DataRow.$Property -is [DBNull]) { 30 | $RowHash[$Property] = $null 31 | } 32 | else { 33 | $RowHash[$Property] = $DataRow.$Property 34 | } 35 | } 36 | [PSCustomObject]$RowHash 37 | } 38 | } 39 | } 40 | function ConvertTo-DataTable { 41 | <# 42 | .SYNOPSIS 43 | Convert PSCustomObject to DataTable. 44 | Warning: Column type taken from firs line, null will be [Object]. 45 | #> 46 | [CmdletBinding()] 47 | param( 48 | [Parameter(Mandatory, ValueFromPipeline)] 49 | $InputObject 50 | ) 51 | begin { 52 | $OutputDataTable = [Data.DataTable]::new() 53 | $First = $true 54 | } 55 | process { 56 | foreach ($PSCustomObject in $InputObject) { 57 | if ($First) { 58 | $First = $false 59 | foreach ($Property in $PSCustomObject.PSObject.Properties) { 60 | $null = $OutputDataTable.Columns.Add($Property.Name, $Property.TypeNameOfValue) 61 | } 62 | } 63 | $null = $OutputDataTable.Rows.Add($PSCustomObject.PSObject.Properties.Value) 64 | } 65 | } 66 | end { 67 | , $OutputDataTable 68 | } 69 | } 70 | 71 | function Format-Test { 72 | <# 73 | .SYNOPSIS 74 | Adds "TestName" and "DataSet" to the test. 75 | Assumes $DataSetName and its value exists. 76 | #> 77 | [CmdletBinding()] 78 | param 79 | ( 80 | [Parameter(Mandatory)] 81 | [Hashtable]$Test 82 | ) 83 | if ($DataSetName, $Test.Params.Left, $Test.Params.Right -contains $null) { 84 | Throw 'Missing param' 85 | } 86 | 87 | if ($Test.Params.Left -is [Hashtable]) { $Test.Params.Left = [PSCustomObject]$Test.Params.Left } 88 | if ($Test.Params.Right -is [Hashtable]) { $Test.Params.Right = [PSCustomObject]$Test.Params.Right } 89 | 90 | $Data = '{0}, {1} - {2}' -f $DataSetName, $Test.Params.Left, $Test.Params.Right 91 | $OutputType = switch ($Test.Params) { 92 | { $_.DataTable } { 'DataTable' } 93 | { $_.PassThru } { 'PassThru' } 94 | } 95 | $OutputType = if ($OutputType) { 96 | '{0}' -f ($OutputType -join '&') 97 | } 98 | $Description = if ($Test.Description) { 99 | '({0})' -f $Test.Description 100 | } 101 | 102 | $Test.TestName = (($Data, $Test.Params.Type, $OutputType, $Description) -ne $null) -join '. ' 103 | $Test.DataSet = Get-Variable -Name $DataSetName -ValueOnly 104 | $Test 105 | } 106 | function Get-Params { 107 | <# 108 | .SYNOPSIS 109 | Get Data to test. 110 | If not DbConnection Param is String containing variable name, It will be fetched and optionally converted and selected on. 111 | If DbConnection Param is Hashtable with "Table" to get and "As" to set output type. 112 | #> 113 | [CmdletBinding()] 114 | param 115 | ( 116 | [Parameter(Mandatory)] 117 | $Param, 118 | [System.Data.Common.DbConnection] 119 | $DbConnection 120 | ) 121 | if ($DbConnection) { 122 | $Output = Invoke-SqliteQuery -SQLiteConnection $DbConnection -Query ('SELECT * FROM {0}' -f $Param.Table) -As $Param.As 123 | , $Output 124 | } 125 | else { 126 | $ParamSplit = $Param -split '(?<=\])(?