src/SqlCommandBuilder.psm1
|
using namespace System.Data using namespace System.Data.Common using namespace System.Diagnostics.CodeAnalysis using module ./DbColumnInfo.psm1 using module ./DbTableInfo.psm1 using module ./SqlCommand.psm1 using module ./SqlMapper.psm1 using module ./SqlParameter.psm1 using module ./SqlParameterCollection.psm1 <# .SYNOPSIS Automatically generates single-table commands. #> class SqlCommandBuilder { <# .SYNOPSIS The position of the catalog name in a qualified table name. #> [CatalogLocation] $CatalogLocation = [CatalogLocation]::Start <# .SYNOPSIS The string used as the catalog separator. #> [ValidateNotNullOrEmpty()] [string] $CatalogSeparator = "." <# .SYNOPSIS The SQL function to use when the `RETURNING` clause is not supported. #> [ValidateNotNullOrEmpty()] [string] $LastInsertIdFunction = "SCOPE_IDENTITY()" <# .SYNOPSIS The beginning string to use for naming parameters. #> [ValidateNotNullOrEmpty()] [string] $ParameterPrefix = "@" <# .SYNOPSIS The beginning string to use when specifying database objects. #> [ValidateNotNullOrEmpty()] [string] $QuotePrefix = "[" <# .SYNOPSIS The ending string to use when specifying database objects. #> [ValidateNotNullOrEmpty()] [string] $QuoteSuffix = "]" <# .SYNOPSIS The string used as the schema separator. #> [ValidateNotNullOrEmpty()] [string] $SchemaSeparator = "." <# .SYNOPSIS Value indicating whether the ADO.NET provider supports the `RETURNING` clause. #> [bool] $SupportsReturningClause <# .SYNOPSIS Value indicating whether the ADO.NET provider uses positional parameters. #> [bool] $UsePositionalParameters <# .SYNOPSIS Creates a new command builder. .PARAMETER Connection The connection to the data source. #> SqlCommandBuilder([IDbConnection] $Connection) { switch ($Connection.GetType().FullName) { { $_ -in "MySql.Data.MySqlClient.MySqlConnection", "MySqlConnector.MySqlConnection" } { $this.QuotePrefix = $this.QuoteSuffix = '`' $this.LastInsertIdFunction = "LAST_INSERT_ID()" break } { $_ -in "FirebirdSql.Data.FirebirdClient.FbConnection", "Microsoft.Data.Sqlite.SqliteConnection", "Npgsql.NpgsqlConnection", "System.Data.SQLite.SQLiteConnection" } { $this.QuotePrefix = $this.QuoteSuffix = '"' $this.SupportsReturningClause = $true break } "Oracle.ManagedDataAccess.Client.OracleConnection" { $this.CatalogLocation = CatalogLocation.End $this.CatalogSeparator = "@" $this.ParameterPrefix = ":" $this.QuotePrefix = $this.QuoteSuffix = '"' $this.SupportsReturningClause = $true break } { $_ -in "System.Data.Odbc.OdbcConnection", "System.Data.OleDb.OleDbConnection" } { $this.UsePositionalParameters = $true break } } } <# .SYNOPSIS Gets the generated command to delete an entity. .PARAMETER Entity The entity to delete. .OUTPUTS The generated command to delete an entity. #> [ValueTuple[SqlCommand, SqlParameterCollection]] GetDeleteCommand([object] $Entity) { $table = [SqlMapper]::Instance.GetTable($Entity.GetType()) $idColumn = $table.IdentityColumn if (-not $idColumn) { throw [InvalidOperationException] "The identity column could not be found." } $parameter = [SqlParameter]::new($this.UsePositionalParameters ? "?1" : $this.GetParameterName($idColumn), $this.GetParameterValue($idColumn, $Entity)) $text = " DELETE FROM $($this.GetTableName($table)) WHERE $($this.QuoteIdentifier($idColumn.Name)) = $($this.UsePositionalParameters ? "?" : $parameter.Name)" return [ValueTuple]::Create[SqlCommand, SqlParameterCollection]($text.Trim(), [SqlParameterCollection]::new($parameter)) } <# .SYNOPSIS Gets the generated command to check the existence of an entity. .PARAMETER Type The entity type. .PARAMETER Id The value of the entity's primary key. .OUTPUTS The generated command to check the existence of an entity. #> [ValueTuple[SqlCommand, SqlParameterCollection]] GetExistsCommand([Type] $Type, [object] $Id) { $table = [SqlMapper]::Instance.GetTable($Type) $idColumn = $table.IdentityColumn if (-not $idColumn) { throw [InvalidOperationException] "The identity column could not be found." } $parameter = [SqlParameter]::new($this.UsePositionalParameters ? "?1" : $this.GetParameterName($idColumn), $Id) $text = " SELECT 1 FROM $($this.GetTableName($table)) WHERE $($this.QuoteIdentifier($idColumn.Name)) = $($this.UsePositionalParameters ? "?" : $parameter.Name)" return [ValueTuple]::Create[SqlCommand, SqlParameterCollection]($text.Trim(), [SqlParameterCollection]::new($parameter)) } <# .SYNOPSIS Gets the generated command to find an entity. .PARAMETER Type The entity type. .PARAMETER Id The value of the entity's primary key. .OUTPUTS The generated command to find an entity. #> [ValueTuple[SqlCommand, SqlParameterCollection]] GetFindCommand([Type] $Type, [object] $Id) { return $this.GetFindCommand($Type, $Id, @()) } <# .SYNOPSIS Gets the generated command to find an entity. .PARAMETER Type The entity type. .PARAMETER Id The value of the entity's primary key. .PARAMETER Columns The list of columns to select. By default, all columns. .OUTPUTS The generated command to find an entity. #> [ValueTuple[SqlCommand, SqlParameterCollection]] GetFindCommand([Type] $Type, [object] $Id, [string[]] $Columns) { $table = [SqlMapper]::Instance.GetTable($Type) $idColumn = $table.IdentityColumn if (-not $idColumn) { throw [InvalidOperationException] "The identity column could not be found." } $fields = ($Columns ? $table.Columns.Values.Where{ $Columns.Contains($_.Name) } : $table.Columns.Values).Where{ $_.CanWrite }.ForEach{ $_.Name } if (-not $fields.Contains($idColumn.Name)) { $fields += $idColumn.Name } $parameter = [SqlParameter]::new($this.UsePositionalParameters ? "?1" : $this.GetParameterName($idColumn), $id) $text = " SELECT $($fields.ForEach{ $this.QuoteIdentifier($_) } -join ", ") FROM $($this.GetTableName($table)) WHERE $($this.QuoteIdentifier($idColumn.Name)) = $($this.UsePositionalParameters ? "?" : $parameter.Name)" return [ValueTuple]::Create[SqlCommand, SqlParameterCollection]($text.Trim(), [SqlParameterCollection]::new($parameter)) } <# .SYNOPSIS Gets the generated command to insert an entity. .PARAMETER Entity The entity to insert. .OUTPUTS The generated command to insert an entity. #> [ValueTuple[SqlCommand, SqlParameterCollection]] GetInsertCommand([object] $Entity) { $table = [SqlMapper]::Instance.GetTable($Entity.GetType()) $idColumn = $table.IdentityColumn if (-not $idColumn) { throw [InvalidOperationException] "The identity column could not be found." } $fields = $table.Columns.Values.Where{ $_.CanRead -and (-not $_.IsComputed) } $text = " INSERT INTO $($this.GetTableName($table)) ($($fields.ForEach{ $this.QuoteIdentifier($_.Name) } -join ", ")) VALUES ($($fields.ForEach{ $this.UsePositionalParameters ? "?" : $this.GetParameterName($_) } -join ", ")) $($this.SupportsReturningClause ? "RETURNING $($this.QuoteIdentifier($idColumn.Name))" : "; SELECT $($this.LastInsertIdFunction);")" $parameters = [SqlParameterCollection]::new() for ($index = 0; $index -lt $fields.Count; $index++) { $parameterName = $this.UsePositionalParameters ? "?$($index + 1)" : $this.GetParameterName($fields[$index]) $parameters.AddWithValue($parameterName, $this.GetParameterValue($fields[$index], $Entity)) } return [ValueTuple]::Create[SqlCommand, SqlParameterCollection]($text.Trim(), $parameters) } <# .SYNOPSIS Gets the generated command to update an entity. .PARAMETER Entity The entity to update. .OUTPUTS The generated command to update an entity. #> [ValueTuple[SqlCommand, SqlParameterCollection]] GetUpdateCommand([object] $Entity) { return $this.GetUpdateCommand($Entity, @()) } <# .SYNOPSIS Gets the generated command to update an entity. .PARAMETER Entity The entity to update. .PARAMETER Columns The list of columns to update. By default, all columns. .OUTPUTS The generated command to update an entity. #> [ValueTuple[SqlCommand, SqlParameterCollection]] GetUpdateCommand([object] $Entity, [string[]] $Columns) { $table = [SqlMapper]::Instance.GetTable($Entity.GetType()) $idColumn = $table.IdentityColumn if (-not $idColumn) { throw [InvalidOperationException] "The identity column could not be found." } $fields = ($Columns ? $table.Columns.Values.Where{ $Columns.Contains($_.Name) } : $table.Columns.Values).Where{ $_.CanRead -and (-not $_.IsComputed) } $text = " UPDATE $($this.GetTableName($table)) SET $($fields.ForEach{ "$($this.QuoteIdentifier($_.Name)) = $($this.UsePositionalParameters ? "?" : $this.GetParameterName($_))" } -join ", ") WHERE $($this.QuoteIdentifier($idColumn.Name)) = $($this.UsePositionalParameters ? "?" : $this.GetParameterName($idColumn))" $parameters = [SqlParameterCollection]::new() for ($index = 0; $index -lt $fields.Count; $index++) { $parameterName = $this.UsePositionalParameters ? "?$($index + 1)" : $this.GetParameterName($fields[$index]) $parameters.AddWithValue($parameterName, $this.GetParameterValue($fields[$index], $Entity)) } $parameterName = $this.UsePositionalParameters ? "?$($fields.Count + 1)" : $this.GetParameterName($idColumn) $parameters.AddWithValue($parameterName, $this.GetParameterValue($idColumn, $Entity)) return [ValueTuple]::Create[SqlCommand, SqlParameterCollection]($text.Trim(), $parameters) } <# .SYNOPSIS Given an unquoted identifier, returns the correct quoted form of that identifier. .PARAMETER UnquotedIdentifier The original unquoted identifier. .OUTPUTS The quoted version of the identifier. #> [string] QuoteIdentifier([string] $UnquotedIdentifier) { return "$($this.QuotePrefix)$($UnquotedIdentifier.Replace($this.QuoteSuffix, $this.QuoteSuffix + $this.QuoteSuffix))$($this.QuoteSuffix)" } <# .SYNOPSIS Given a quoted identifier, returns the correct unquoted form of that identifier. .PARAMETER QuotedIdentifier The original quoted identifier. .OUTPUTS The unquoted version of the identifier. #> [string] UnquoteIdentifier([string] $QuotedIdentifier) { if ($QuotedIdentifier.StartsWith($this.QuotePrefix, [StringComparison]::Ordinal)) { $QuotedIdentifier = $QuotedIdentifier.Substring($this.QuotePrefix.Length) } if ($QuotedIdentifier.EndsWith($this.QuoteSuffix, [StringComparison]::Ordinal)) { $QuotedIdentifier = $QuotedIdentifier.Substring(0, $QuotedIdentifier.Length - $this.QuoteSuffix.Length) } return $QuotedIdentifier.Replace($this.QuoteSuffix + $this.QuoteSuffix, $this.QuoteSuffix) } <# .SYNOPSIS Returns the parameter name corresponding to the specified column. .PARAMETER Column The column providing the parameter name. .OUTPUTS The parameter name corresponding to the specified column. #> hidden [string] GetParameterName([DbColumnInfo] $Column) { return "$($this.ParameterPrefix)$($Column.Name)" } <# .SYNOPSIS Returns the parameter value corresponding to the specified column. .PARAMETER Column The column providing the parameter data type. .PARAMETER Entity The entity providing the parameter value. .OUTPUTS The parameter value corresponding to the specified column. #> [SuppressMessage("PSUseDeclaredVarsMoreThanAssignments", "discard")] hidden [object] GetParameterValue([DbColumnInfo] $Column, [object] $Entity) { $stringTypes = [DbType]::AnsiString, [DbType]::AnsiStringFixedLength, [DbType]::String, [DbType]::StringFixedLength $value = $Column.GetValue($Entity) return $discard = switch ($Column.DbType) { { $Column.PropertyType.IsEnum -and ($_ -in $stringTypes) } { ${value}?.ToString(); break } default { $value } } } <# .SYNOPSIS Returns the fully qualified name corresponding to the specified table. .PARAMETER Table The table. .OUTPUTS The fully qualified name corresponding to the specified table. #> hidden [string] GetTableName([DbTableInfo] $Table) { return $Table.Schema ? "$($this.QuoteIdentifier($Table.Schema))$($this.SchemaSeparator)$($this.QuoteIdentifier($Table.Name))" : $this.QuoteIdentifier($Table.Name) } } |