Write-ObjectToSQL.psm1
#REQUIRES -Version 4.0 <# .Synopsis Writes the properties of an object into a database table. The table will be created if it doesnt exist. .DESCRIPTION Writes an object into a database table. If the table does not exist it will be created based on the properties of the object. For every property of the object a column will be created. The data type for each column will be converted from .Net data types into SQL Server data types. Not all data types are supported. Unsupported data types will be ignored (but can be listed). If several objects are sent through the pipeline only the first object will be used for creating the template for the table. Make sure that all objects in the pipeline have the exact same properties (this is usually the case). While creating the table the script will also add two default columns. One called "id" which is a regular auto counter (integer which increases with 1 for every row) and another column called "inserted_at" which will have a default value of GetDate() which represents the timestamp for when the row was inserted. If a property is named the same as one of these default columns then a "x" will be added before the name of those columns to avoid duplication. (if propertyname=id, then propertyname=xid, etc.) Hashtables are handled slightly different. When using hashtables the script will simply use the keys as columns. Keep in mind that properties on the objects are used. Some objects, like strings, might only have a length property but what you really want to insert into the table is the value of the string. The following command would generate a table with one column called Length which would contain the length of the strings (probably not what you want): 'oink','meo' | Write-ObjectToSQL -Server localhost\sqlexpress -Database MyDB -TableName myTable The following command is a better way to do it. Instead of piping the strings directly you should create custom objects or, as in this example, hash tables. This will generate a table with a column called 'text' which will contain the values 'oink' and 'meo': @{'text'='oink'}, @{'text'='meo'} | Write-ObjectToSQL -Server localhost\sqlexpress -Database MyDB -TableName myTable Another thing to note is that this script will only take Property and NoteProperty into consideration. So for example ScriptProperty and ParameterizedProperty will be ignored. You can verify your objects with the Get-Member cmdlet and check the MemberType. Currently the script supports the following data types: Int32 UInt32 Int16 UInt16 Int64 UInt64 long int Decimal Single Double Byte SByte String DateTime TimeSpan datetime string bool Boolean Guid Version 1.13 Created by John Roos Email: john@roostech.se Web: http://blog.roostech.se Repo: https://github.com/JohnRoos/PowerShell .EXAMPLE PS C:\> Get-Process | Write-ObjectToSQL -Server localhost\sqlexpress -Database MyDB -TableName ProcessTable Creates a table called ProcessTable (if it doesnt exist) based on the result from Get-Process. After the table is created all the objects will be inserted into the table. Some properties will be ignored because the data types are not supported. .EXAMPLE PS C:\> Get-Process | Write-ObjectToSQL -Server localhost\sqlexpress -Database MyDB -TableName ProcessTable -ShowIgnoredPropertiesOnly DependentServices ServiceHandle Status ServicesDependedOn Container RequiredServices ServiceType Site This is useful for debugging. When using the parameter switch ShowIgnoredPropertiesOnly the cmdlet will not do anything in the database. Instead it will show which properties that will be ignored (unsupported data types). A complete run is still simulated so this command takes about the same time as if you would have inserted the objects into the table. .EXAMPLE PS C:\> Get-Process | Write-ObjectToSQL -Server localhost\sqlexpress -Database MyDB -TableName ProcessTable -ReportEveryXObject 20 2014-12-20 11:56:12 - Rows inserted: 20 (100 rows per second) 2014-12-20 11:56:12 - Rows inserted: 40 (250 rows per second) 2014-12-20 11:56:12 - Rows inserted: 60 (250 rows per second) Inserted 68 rows into ProcessTable in 0.41 seconds (165.85 rows per second) This will insert objects into the database as usual with one difference: It will report back to the console every 20th object so that some kind of progress is shown. The cmdlet does not know how many objects are coming down the pipeline so this only shows what has happened, not how many objects that are left to process. .EXAMPLE PS C:\> Get-Process | Write-ObjectToSQL -Server localhost\sqlexpress -Database MyDB -TableName ProcessTable -TimeSpanType Milliseconds Inserted 68 rows into ProcessTable in 500.04 milliseconds (136 rows per second) The TimeSpanType parameter can be used if you want to get the time it takes to run the script in a different kind of timespan type. There are four alternatives: Hours, Hinutes, Seconds and Milliseconds. .EXAMPLE PS C:\> Get-WmiObject -ComputerName 'localhost' -Query "select * from win32_networkadapter where NetConnectionStatus = 2" | Write-ObjectToSQL -Server localhost\sqlexpress -Database MyDB -TableName wminetworkadapter This is where the script becomes useful. This inserts information about the network adapters in localhost into a table called wminetworkadapter. Replace 'localhost' with a list of servers and you quickly have a simple inventory of network adapters on your servers. .EXAMPLE PS C:\> Get-Process | Write-ObjectToSQL -ConnectionString "Provider=MySQL Provider; Data Source=Server01; User ID=MyUserName; Password=MyPassword; Initial Catalog=MyDatabaseName;" -TableName MyTableName Experimental: This will insert the objects into a MySQL database. Running this multiple times will generate an error when attempting to create the table since the script is not able to check if it already exist (inserts will still work). When inserting objects into an existing table, use the parameter '-DoNotCreateTable' to avoid these errors if you are using a custom connection string. NOTE: As this is experimental it migth not work. I got it to work with MySQL as the example above shows, but I give no guarantees :) NOTE: It might work with other database engines as long as you have the proper connection string but I havent tested that. If you want to try it out, check www.connectionstrings.com to find the connection string you need. You might also need to install ODBC or OLEDB drivers. .INPUTS The script accepts an object as input from the pipeline. .OUTPUTS Outputs the result from the insert queries to the console together with some statistics on how long it took and the speed (rows per second). .NOTES Fixed: Several new data types added Now both creates the table and inserts the values Fix empty property bug on subsequent objects in pipeline Rewrote the property parsing completely Moved repeated code to BEGIN for performance reasons Generate SQL code only as a string with tablename validation New parameter - Do not create table (fail if not exist) New parameter - Only output "properties ignored" (easier to use when adding new data types) New parameter to report progress at every X processed object - ReportEveryXObject Add measure time functionality. Show time taken in seconds when showing how many rows were inserted. Tidied up the code, removed obsolete comments Fix proper verbose and warnings Verified 'System.UInt32' compared to SQL Server data types Make sure that ' is handled properly when inserting values Add N'' for strings in inserts for SQL Server instead of just '' Remove ' when creating table (replace "'" with "") Add custom DB connection to avoid dependencies to other cmdlets Fail the whole script if the first SELECT statement fails Added several new data types (avoided BigInteger on purpose since it does not have proper max/min values) Updated examples in the header Add parameter TimespanType for selecting what to use when using Timespan. Options: Hours, Minutes, Seconds, Milliseconds Add parameter sets to validate properly when using SQL Server compared to custom connection string If any parameter is missing, fail the whole script (parameter grouping is needed) Merge $modconnection and $connection since they are basically the same Add OLEDB connection string functionality Verify with MySQL and OLEDB Drivers and providers for other database types can be found on NuGet.org or PowerShellGallery.com Fix so that properties can be handled even if NULL Add switch TryAllPropertyTypes in case someone wants to insert more than just regular properties Modified the loops generating database queries so that not that much code is repeating Added support for hashtables. The scripts will now insert the hash values into columns named as the hash keys. Rewrote the script to use a few functions instead. Added better error handling if all properties are ignored. Fixed a bug where insert statements based on a string property that had the same name as a reserved property was failing Added support for Timespan data type. Timespan will be converted to Ticks when stored in the table. Added SQL support for datetime data type Added credential parameter Added support for SQL Server accounts (using the credential parameter) Improved error handling to avoid getting WriteErrorException Added support for System.Guid Fixed bug where paranteses in property names would cause errors (thanks to Ruud Borst for finding this!) Changed declaration of PSCredential to follow best practice with CredentialAttribute. Removed a couple of variables which was never used. Changed Write-Host to Write-Output to follow best practice. Removed initialization of Database parameter in param block. Now the PSScriptAnalyzer can shut up. Added logic to handle System.Nullable data types (thanks to beanska on GitHub for adding this) Improved the GUID support to use uniqueidentifier when creating the table Added the optional parameter PrimaryKey (thanks to lw-schick on GitHub for this idea) When PrimaryKey is used, that column will be set to NOT NULL when creating the table Added the SchemaName parameter to select which schema the table should have (thanks to lw-schick on GitHub for adding this) Fixed a bug where zeroes were sometimes treated as null values (thanks to acheung456 in GitHub for reporting this) .LINK SQL Server data types http://msdn.microsoft.com/en-us/library/ms187752.aspx C# data types http://msdn.microsoft.com/en-us/library/ya5y69ds.aspx Built-In Types Table (C# Reference) http://msdn.microsoft.com/en-us/library/ya5y69ds.aspx VB data types http://msdn.microsoft.com/en-us/library/47zceaw7.aspx VB.Net data types http://www.tutorialspoint.com/vb.net/vb.net_data_types.htm #> function Write-ObjectToSQL { [CmdletBinding(DefaultParametersetName='mssql')] Param ( # Object to base the table on and insert into the table. # Piping several objects is supported, and even recommended to get the full potential of this script. [Parameter(Position=0, Mandatory=$True, ParameterSetName='othersql', HelpMessage="Please specify an object to insert into the database table", ValueFromPipeline=$True)] [Parameter(Position=0, Mandatory=$True, ParameterSetName='mssql', HelpMessage="Please specify an object to insert into the database table", ValueFromPipeline=$True)] [ValidateNotNullorEmpty()] [object]$InputObject, # The SQL Server instance (commonly called "server" nowadays among SQL Server geeks, or MS dudes). [Parameter(Mandatory=$True, ParameterSetName='mssql', HelpMessage="Please specify the SQL Server instance.", ValueFromPipeline=$False)] [ValidateNotNullorEmpty()] [string]$Server, # The name of the database where the table exists (or will be created) [Parameter(Mandatory=$True, ParameterSetName='mssql', HelpMessage="Please specify a database name.", ValueFromPipeline=$False)] [ValidateNotNullorEmpty()] [string]$Database, # Credential to use when opening the connection to the server. [Parameter(Mandatory=$False, ParameterSetName='mssql', HelpMessage="Please specify a credential.", ValueFromPipeline=$False)] [PSCredential] [System.Management.Automation.CredentialAttribute()] $Credential, # Connection string when not using SQL Server # This is experimental. See examples. [Parameter(Mandatory=$True, ParameterSetName='othersql', HelpMessage="Please specify a connection string if you do not want to use SQL Server.", ValueFromPipeline=$False)] [ValidateNotNullorEmpty()] [string]$ConnectionString, # The name of the table where the object will be inserted into [Parameter(Mandatory=$True, ParameterSetName='mssql', HelpMessage="Please specify a table name where the object will be inserted.", ValueFromPipeline=$False)] [Parameter(Mandatory=$True, ParameterSetName='othersql', HelpMessage="Please specify a table name where the object will be inserted.", ValueFromPipeline=$False)] [ValidateNotNullorEmpty()] [string]$TableName, # The name of the schema where the table is [Parameter(Mandatory=$False, ParameterSetName='mssql', HelpMessage="Please specify the schema name. If empty, dbo will be used.", ValueFromPipeline=$False)] [string]$SchemaName, # Use this switch if you do not want the table to be created. # If the table does not exist an error (per object) will be thrown. # If large numbers of objects are piped to the script with this switch property set you will get lots of bleeding if the table does not exist. [Parameter(Mandatory=$False, ParameterSetName='mssql', ValueFromPipeline=$False)] [Parameter(Mandatory=$False, ParameterSetName='othersql', ValueFromPipeline=$False)] [switch]$DoNotCreateTable, # Use this switch to only show which properties that are ignored. When this switch is used no table will be created and no rows will be inserted. # However, all the logic will still be performed just like if you run the script normally so it will take the same amount of time to process. # This switch parameter is especially useful when troubleshooting or if you want to improve the script to handle more data types. [Parameter(Mandatory=$False, ParameterSetName='mssql', ValueFromPipeline=$False)] [Parameter(Mandatory=$False, ParameterSetName='othersql', ValueFromPipeline=$False)] [switch]$ShowIgnoredPropertiesOnly, # This switch forces the script to attempt to handle all property member types (AliasProperty, ScriptProperty etc.) instead of only the regular Property and NoteProperty member types. # Results can vary a bit when using this. The recommendation is not to use this unless you know what it does. It probably wont break anything but the results might get inconsistent. [Parameter(Mandatory=$False, ParameterSetName='mssql', ValueFromPipeline=$False)] [Parameter(Mandatory=$False, ParameterSetName='othersql', ValueFromPipeline=$False)] [switch]$TryAllPropertyTypes, # This parameter is used if you want to get informed about the progress. After every X amount of objects the script will output how many objects it has inserted into the table. # Useful when inserting large amounts of objects so that you can see that there is some king of progress happening. [Parameter(Mandatory=$False, ParameterSetName='mssql', ValueFromPipeline=$False)] [Parameter(Mandatory=$False, ParameterSetName='othersql', ValueFromPipeline=$False)] [ValidateNotNullorEmpty()] [Int64]$ReportEveryXObject, # Type of time to report when statistics are shown in the end of the script. # Options are Hours, Minutes, Seconds and Milliseconds. [Parameter(Mandatory=$False, ParameterSetName='mssql', ValueFromPipeline=$False)] [Parameter(Mandatory=$False, ParameterSetName='othersql', ValueFromPipeline=$False)] [ValidateNotNullorEmpty()] [ValidateSet('Hours', 'Minutes', 'Seconds', 'Milliseconds')] [string]$TimeSpanType, # Optional parameter which sets which object property will be used as primary key when creating the table # Should be a string with the name of the property. [Parameter(Mandatory=$False, ParameterSetName='othersql', ValueFromPipeline=$False)] [Parameter(Mandatory=$False, ParameterSetName='mssql', ValueFromPipeline=$False)] [ValidateNotNullorEmpty()] [string]$PrimaryKey ) Begin { function CheckIfTableExistsSQL () { $command = $modconnection.CreateCommand() $dataset = New-Object -TypeName System.Data.DataSet $rowCount = 0 Write-Verbose 'Checking if the table exist.' $ErrorActionPreference = "Stop" $command.CommandText = "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '$SchemaName' AND TABLE_NAME LIKE '$tablename'" $adapter = New-Object -TypeName System.Data.SqlClient.SqlDataAdapter $command $rowCount = $adapter.Fill($dataset) $ErrorActionPreference = $originalErrorActionPreference if ( $rowCount -gt 0){ return $True }else{ return $False } } #### BEGIN #### $starttime = Get-Date $reportstarttime = $starttime if ($DoNotCreateTable){ $tablecreated = $true }else{ $tablecreated = $false } $rowsinserted = 0 $rowsfailed = 0 $datasample = [ordered]@{} $removeFromSample = [ordered]@{} $generatefirst = $true $numbertypes = @{ # PS datatype = SQL data type 'System.Int32' = 'int'; 'System.UInt32' = 'bigint'; 'UInt32' = 'bigint'; 'System.Int16' = 'smallint'; 'System.UInt16' = 'int'; 'System.Int64' = 'bigint'; 'System.UInt64' = 'decimal(20,0)'; 'long' = 'bigint'; 'int' = 'int'; 'System.Decimal' = 'decimal(20,5)'; 'System.Single' = 'bigint'; 'System.Double' = 'float'; 'System.Byte' = 'tinyint'; 'System.SByte' = 'smallint'; 'System.TimeSpan' = 'bigint'; 'timespan' = 'bigint'; } $stringtypes = @{ # PS datatype = SQL data type 'System.String' = 'nvarchar(1000)'; 'System.DateTime' = 'datetime'; 'datetime' = 'datetime'; 'string' = 'nvarchar(1000)'; 'bool' = 'bit'; 'System.Boolean' = 'bit'; 'Guid' = 'uniqueidentifier'; 'System.Guid' = 'uniqueidentifier'; } $reservedcolumns = @{ # Column name = What to add before the string (example: id becomes xid) 'id' = 'x'; 'inserted_at' = 'x' } if ($ReportEveryXObject){ $whentoreport = $ReportEveryXObject }else{ $whentoreport = 0 } $reportcompare = 1 $createtable = $true $doinserts = $true if ($ShowIgnoredPropertiesOnly){ $createtable = $false $doinserts = $false } if ($DoNotCreateTable){ $createtable = $false } # Prepare the Primary Key if selected # The name of the primary key need to be unique, so lets add a GUID to the name # This does not look very nice in the database, but its optional # Also make sure that the name is changed according to the reserved columns variable to be consistent # This SQL syntax should work in MySQL / SQL Server / Oracle / MS Access $pkGUID = ([guid]::NewGuid()).tostring().replace('-','') if ($PrimaryKey) { $PrimaryKeyCorrected = $PrimaryKey.Replace(' ','_') if ($reservedcolumns.Keys -contains $PrimaryKeyCorrected) { $PrimaryKeyCorrected = "$($reservedcolumns.$PrimaryKeyCorrected)$PrimaryKeyCorrected" } $PrimaryKeyString = ", CONSTRAINT pk_$($PrimaryKeyCorrected)_$pkGUID PRIMARY KEY ($PrimaryKeyCorrected)" } else { $PrimaryKeyString = '' } $ignoredSamplesRemoved = $false ## Custom connection instead of using Invoke-Sqlcmd if ($ConnectionString) { Write-Verbose 'Not using SQL Server.' $constr = $ConnectionString $script:modconnection = New-Object -TypeName System.Data.OleDb.OleDbConnection } else { Write-Verbose 'Using SQL Server.' if ($Credential){ $constr = "Server=$Server;Database=$Database;Trusted_Connection=False;User ID=`"$($Credential.UserName)`";Password=`"$($Credential.GetNetworkCredential().Password)`"" }else{ $constr = "Server=$Server;Database=$Database;Trusted_Connection=True;" } $script:modconnection = New-Object -TypeName System.Data.SqlClient.SqlConnection } $modconnection.ConnectionString = $constr # if we can't connect to the database its better to just break the whole script $script:originalErrorActionPreference = $ErrorActionPreference $ErrorActionPreference = "Stop" Write-Verbose 'Attempting to connect to the database.' try { $modconnection.Open() Write-Verbose 'Connection to database opened successfully.' } catch { Write-Warning "Could not open the connection. See error message below3." Write-Error $_.Exception.Message throw } $ErrorActionPreference = $originalErrorActionPreference # SQL Server likes [ and ] but other database engines prefer single quotes if ($ConnectionString){ $quoteFirst = "'" $quoteLast = "'" }else{ $quoteFirst = '[' $quoteLast = ']' } $reportrowsinsertedstart = 0 $ObjectIsHash = $false if ([string]::IsNullOrEmpty($SchemaName)) { $SchemaName = 'dbo' } # are we using SQL Server (empty connection string)? # does a table exist with this name? # as long as 'DoNotCreateTable' is not used ($tablecreated is true if DoNotCreateTable is used). if (!$tablecreated -and !$ConnectionString) { if ( CheckIfTableExistsSQL ){ if ($ShowIgnoredPropertiesOnly){ $tablecreated = $False Write-Verbose 'Table exists in the database (simulating create table based on parameter selection)' }else{ $tablecreated = $True Write-Verbose 'Table exists in the database (no creation needed)' } }else{ $tablecreated = $False Write-Verbose 'Table does not exist in database (creation needed)' } } } Process { function GetPropertyTypes ($InputObject, $ObjectIsHash,$TryAllPropertyTypes) { $typeresult = @{} if ($ObjectIsHash){ $typetest = $InputObject.keys foreach ($t in $typetest){ $typeresult.Add($t,$t.GetType().name) } }else{ if ($TryAllPropertyTypes){ $typetest = ($inputobject | Get-Member -MemberType Properties ) } else{ $typetest = ($inputobject | Get-Member -MemberType Properties | Where-Object {$_.MemberType -eq 'Property' -or $_.MemberType -eq 'NoteProperty'} ) } foreach ($t in $typetest){ $typeresult.Add($t.Name,$t.Definition.Remove($t.Definition.IndexOf(' '))) } } return $typeresult } #### PROCESS #### # if this is the first object in the pipeline, start processing the starting stuff (only needed on the first object) if ($generatefirst){ Write-Verbose 'Processing first object (there is a little bit more to do on the first one to improve the speed on the rest)' # if the object is a hashtable it should be processed differently than a regular object. # check if the object is a hashtable if ($InputObject.GetType().Name -eq 'Hashtable'){ $ObjectIsHash = $true Write-Verbose 'Input object is a hash table.' } # if the inputobject is a hashtable just take the keys instead of going through all properties like with regular objects if ($ObjectIsHash){ $names = $InputObject.keys }else{ # if TryAllPropertyTypes parameter is used then all types of properties will be checked # otherwise only Property and NoteProperty will be checked (default) if ($TryAllPropertyTypes){ $names = $InputObject | Get-Member -MemberType properties | Select-Object -ExpandProperty name }else{ $names = $InputObject | Get-Member -MemberType properties | Where-Object {$_.MemberType -eq 'Property' -or $_.MemberType -eq 'NoteProperty' } | Select-Object -ExpandProperty name } } # add the object properties to the hash table Write-Verbose 'Starting to process properties.' $names | ForEach-Object { Write-Verbose "Adding property $_" $datasample.Add($_,$inputobject.$_) } # foreach # get all property types by parsing the output from Get-Member # this is one of the two ways the script will check fo the property types # if the property is empty this is the only way to figure out what the type is (limitation in Powershell?) $typeresult = @{} $typeresult = GetPropertyTypes($InputObject, $ObjectIsHash,$TryAllPropertyTypes) $generatefirst = $false } # if generatefirst Write-Verbose "Object properties inserted into the hash" $querystring = '' if (!$tablecreated){ # table does not exist Write-Verbose 'Starting to generate create table query.' $ignoredcolumns = 0 $acceptedcolumns = 0 # Go through all the keys in the data sample and generate the create table query foreach($key in $datasample.Keys){ $prekey = '' foreach ($rescol in $reservedcolumns.Keys){ if ($key -eq $rescol){ $prekey = $reservedcolumns.$rescol } } # Two ways of getting the data type. Either via GetType or with Get-Member, parcing the output (function GetPropertyTypes performs this part) # First try GetType. If that doesnt work then use the output from GetPropertyTypes function. If none of them work, set the data type to empty so that it will be ignored later. try { $datatype = $datasample.$key.GetType().ToString() } catch { try { $datatype = $typeresult.$key } catch { $datatype = '' } } # If data type shows as "System.Nullable[DataType]" then show as just 'DataType' $datatype = $datatype.Replace('System.Nullable[', '').Replace(']', '') # go through all property types to see if they exist in the hash tables with supported data types ($numbertypes and $stringtypes) # if a data type doesnt exist in the hash tables then add it to ignore list ($removeFromSample) try { if ( $numbertypes.ContainsKey( $datatype ) ){ $querystring += ", $quoteFirst$prekey$($key.Replace(' ','_'))$quoteLast $($numbertypes.($datatype))" if ($PrimaryKey) { if ($key -ne $PrimaryKey) { $querystring += " NULL" } } $acceptedcolumns++ }elseif ( $stringtypes.ContainsKey( $datatype ) ){ $columnname = $key -replace "'", "" $columnname = $key -replace '"', '' $querystring += ", $quoteFirst$prekey$($columnname.Replace(' ','_'))$quoteLast $($stringtypes.($datatype))" if ($PrimaryKey) { if ($key -ne $PrimaryKey) { $querystring += " NULL" } } $acceptedcolumns++ }else{ Write-Verbose "$key contains an unsupported data type. ($datatype)" $ignoredcolumns++ $removeFromSample.Add($ignoredcolumns,$key) } } catch { Write-Verbose "$key contains an unknown data type ($datatype)" $ignoredcolumns++ $removeFromSample.Add($ignoredcolumns,$key) } } # foreach key in data sample # remove unsupported data types from sample so that they get ignored foreach ($remsamp in $removeFromSample.Values){ $datasample.Remove($remsamp) } $ignoredSamplesRemoved = $true # did the object have any properties that we can use? Otherwise break. # if no supported properties were found then the query string should be empty if (!$querystring){ Throw 'The input object did not contain any supported properties.' } # complete the query string depending on what kind of database engine we are working with # if $ConnectionString is not empty then we are probably not using SQL Server if ($ConnectionString){ # remove the first comma in $querystring $querystring = $querystring.Substring(1) # If the Primary Key parameter is used, validate it against the data sample to make sure its an existing column if ($PrimaryKey) { if ($datasample.keys -contains $PrimaryKey) { $createquery = "CREATE TABLE $tablename ($querystring $PrimaryKeyString)" } else { # If we arrive here that means that a Primary Key was used which has not been included among the columns in the create table string # This could mean that the property has been ignored (unhandled data type) or that it was spelled wrong Write-Error "The selected column for primary key ($PrimaryKey) does not exist." break } } else { $createquery = "CREATE TABLE $tablename ($querystring)" } }else{ # if we are using SQL Server then we want to have an 'id' column and a 'inserted_at' column and because of that we dont have to remove the first comma # If the Primary Key parameter is used, validate it against the data sample to make sure its an existing column if ($PrimaryKey) { if ($datasample.keys -contains $PrimaryKey) { $createquery = "CREATE TABLE $SchemaName.$tablename ([id] [int] IDENTITY(1,1) NOT NULL, [inserted_at] [datetime] NULL default(getdate()) $querystring $PrimaryKeyString)" } else { # If we arrive here that means that a Primary Key was used which has not been included among the columns in the create table string # This could mean that the property has been ignored (unhandled data type) or that it was spelled wrong Write-Error "The selected column for primary key ($PrimaryKey) does not exist." break } } else { $createquery = "CREATE TABLE $SchemaName.$tablename ([id] [int] IDENTITY(1,1) NOT NULL, [inserted_at] [datetime] NULL default(getdate()) $querystring)" } } Write-Verbose "Create query: $createquery" # lets create the table in the database if $createtable is true # if $createtable is false its because the DoNotCreateTable or ShowIgnoredProperties parameter is used if ($createtable){ $command = $modconnection.CreateCommand() $command.CommandText = $createquery $command.ExecuteNonQuery() | Out-Null Write-Verbose "Table created with $acceptedcolumns columns ($ignoredcolumns ignored)" }else{ Write-Verbose "Skipping table creation based on parameter selection" Write-Verbose "Ignored properties:" foreach ($remsamp in $removeFromSample.Values){ Write-Verbose $remsamp } } $tablecreated = $true } # if !tablecreated ####################################################################### ##### TABLE SHOULD NOW EXIST - START INSERTING VALUES ##### ####################################################################### # reset values and columns when starting to process each object in the pipeline $insertcolumns = '' $insertvalues = '' $strtmp = '' if (!$ignoredSamplesRemoved) { $removeFromSample = @{} } # StringBuilder is faster than strings when appending text (especially when appending a lot) $strBuilderColumns = New-Object System.Text.StringBuilder $strBuilderValues = New-Object System.Text.StringBuilder # Start to generate insert query based on every key in the data sample write-verbose "Starting to generate insert query" foreach($key in $datasample.Keys){ $prekey = '' # if the property has the same name as one of the reserved columns then rename it foreach ($rescol in $reservedcolumns.Keys){ if ($key -eq $rescol){ $prekey = $reservedcolumns.$rescol } } # get the data type for the key try { $datatype = $datasample.$key.GetType().ToString() } catch { try { $datatype = $typeresult.$key } catch { Write-Warning "Could not figure out data type for $key" $datatype = '' } } # If data type shows as "System.Nullable[DataType]" then show as just 'DataType' $datatype = $datatype.Replace('System.Nullable[', '').Replace(']', '') # go through the number types and the string types array to see if the property type is supported # if its supported, generate the strings for the database query # if its not supported, add it to the $removeFromSample variable so that we can skip to check that one on the next object (for performance) try { if ( $numbertypes.ContainsKey( $datatype ) ){ $null = $strBuilderColumns.Append(", $quoteFirst$prekey$($key.Replace(' ','_'))$quoteLast") if ($($InputObject.$key) -ne $null){ if ($datatype -eq 'timespan' -or $datatype -eq 'System.TimeSpan') { Write-Verbose "Timespan found ($key). Converting to ticks." $null = $strBuilderValues.Append(", $(($InputObject.$key).Ticks)") }else{ $null = $strBuilderValues.Append(", $($InputObject.$key)") } }else{ $null = $strBuilderValues.Append(", NULL") } }elseif ( $stringtypes.ContainsKey( $datatype ) ){ $null = $strBuilderColumns.Append(", $quoteFirst$prekey$($key.Replace(' ','_'))$quoteLast") $strtmp = $InputObject.$key -replace "'", "''" if ($ConnectionString){ $null = $strBuilderValues.Append(", '$strtmp'") }else{ $null = $strBuilderValues.Append(", N'$strtmp'") } }else{ if (!$ignoredSamplesRemoved) { write-verbose "Ignoring (No data type match): $key ($datatype)" $ignoredcolumns++ $removeFromSample.Add($ignoredcolumns,$key) }else{ Write-Warning 'Ignored columns does not match' } } } catch { write-verbose "Ignoring (No data type match): $key ($datatype)" } } # foreach key # did the input object contain any supported properties? If not, break the script # if no supported properties were found then either $strBuilderColumns or $strBuilderValues (or probably both) will be empty if ( !$strBuilderColumns.ToString() ){ Throw 'The input object did not contain any supported properties. Unable to generate columns for insert.' } if ( !$strBuilderValues.ToString() ){ Throw 'The input object did not contain any supported properties. Unable to generate values for insert.' } # remove the first two characters which should be a space and a comma in both strings # (the comma was added when going through the supported number types and string types) $insertcolumns = $strBuilderColumns.ToString().Remove(0,2) $insertvalues = $strBuilderValues.ToString().Remove(0,2) # complete the insert statement if ($ConnectionString){ $insertstring = "INSERT INTO $tablename ( $insertcolumns ) VALUES ( $insertvalues )" }else{ $insertstring = "INSERT INTO $SchemaName.$tablename ( $insertcolumns ) VALUES ( $insertvalues )" } Write-Verbose "Insert query generated: $insertstring" # now lets try to do the insert in the database if ($doinserts) { try { $command = $modconnection.CreateCommand() $command.CommandText = $insertstring $command.ExecuteNonQuery() | Out-Null Write-Verbose "Row inserted" $rowsinserted++ } catch { Write-Error $_.Exception.Message Write-Warning "Could not insert row into database. See SQL query error message above." $rowsfailed++ } }else{ Write-Verbose "Skipping insert based on parameter selection" } <# If its the first object in the pipeline we are working with then its the first row we are inserting. If its the first row, make sure that we are removing all ignored object properties from datasample. This makes the scrips a bit faster for all the objects coming in later since we only have to go through the properties that we expect is supported. #> if (!$ignoredSamplesRemoved){ if ($rowsinserted -eq 0){ foreach ($remsamp in $removeFromSample.Values){ $datasample.Remove($remsamp) } } } # if the ReportEveryXObject parameter is used and $reportcompare has the same value, then we want to output some fancy text to show the progress. # if not, just increase $reportcompare with one if ($whentoreport -eq $reportcompare) { $reportendtime = Get-Date $reporttimespent = [math]::round((NEW-TIMESPAN –Start $reportstarttime –End $reportendtime).TotalSeconds,2) Write-Output "$((get-date -Format s).Replace('T',' ')) - Rows inserted: $rowsinserted ($([math]::round( ($rowsinserted-$reportrowsinsertedstart)/$reporttimespent ,2)) rows per second)" $reportrowsinsertedstart = $rowsinserted # reset $reportcompare and $reportstarttime so that we can use them again for the next time we want to report progress. $reportcompare = 1 $reportstarttime = Get-Date }else{ $reportcompare++ } } End { # now we are done. Close the connection to the database. $modconnection.close() # prepare the output $endtime = Get-Date $timespentseconds = [math]::round((NEW-TIMESPAN –Start $starttime –End $endtime).TotalSeconds,2) # if timespantype is used, we use that time type. Otherwise we use seconds. if ($TimeSpanType){ $TimeSpanTypeTotal = "Total$TimeSpanType" $timespent = [math]::round((NEW-TIMESPAN –Start $starttime –End $endtime).$TimeSpanTypeTotal,2) }else{ $timespent = $timespentseconds $TimeSpanType = 'seconds' } # calculate how many rows were inserted per second $rowspersecond = [math]::round(($rowsinserted/$timespentseconds),2) # if the parameter ShowIgnoredPropertiesOnly was used we want to show the ignored properties # otherwise we just write the regular output to the console if ($ShowIgnoredPropertiesOnly){ $ignoredobjects = @() foreach ($remsamp in $removeFromSample.Values) { $ignoredBastard = New-Object -TypeName PSObject $ignoredBastard | Add-Member -Type NoteProperty -Name 'IgnoredProperty' -Value $remsamp $ignoredBastard | Add-Member -Type NoteProperty -Name 'TypeName' -Value $typeresult.$remsamp $ignoredobjects += $ignoredBastard } Write-Verbose 'Returning ignored properties:' $ignoredobjects }else{ # write how many rows we inserted to the console if ($rowsinserted -ne 1) { Write-Output "Inserted $rowsinserted rows into $tablename in $timespent $($TimeSpanType.ToLower()) ($rowspersecond rows per second)" }else{ Write-Output "Inserted $rowsinserted row into $tablename in $timespent $($TimeSpanType.ToLower()) ($rowspersecond rows per second)" } # write how many rows that failed to the console if ($rowsfailed -gt 0){ if ($rowsfailed -gt 1){ Write-Warning "$rowsfailed rows failed" }else{ Write-Warning "$rowsfailed row failed" } } } # if ShowIgnoredPropertiesOnly } } |