src/DbColumnInfo.psm1

using namespace System.ComponentModel.DataAnnotations.Schema
using namespace System.Data
using namespace System.Management.Automation
using namespace System.Reflection
using namespace System.Threading

<#
.SYNOPSIS
    Provides information about a database column.
#>

class DbColumnInfo {

    <#
    .SYNOPSIS
        The nullability context.
    #>

    hidden static [ThreadLocal[NullabilityInfoContext]] $NullabilityContext = [ThreadLocal[NullabilityInfoContext]]::new([Func[NullabilityInfoContext]] { [NullabilityInfoContext]::new() })

    <#
    .SYNOPSIS
        The mapping between common .NET types and data types.
    #>

    hidden static [hashtable] $TypeMap = @{
        [bool] = [DbType]::Boolean
        [byte[]] = [DbType]::Binary
        [byte] = [DbType]::Byte
        [char] = [DbType]::StringFixedLength
        [DateOnly] = [DbType]::Date
        [datetime] = [DbType]::DateTime
        [DateTimeOffset] = [DbType]::DateTimeOffset
        [decimal] = [DbType]::Decimal
        [double] = [DbType]::Double
        [float] = [DbType]::Single
        [guid] = [DbType]::Guid
        [int] = [DbType]::Int32
        [long] = [DbType]::Int64
        [Nullable[bool]] = [DbType]::Boolean
        [Nullable[byte]] = [DbType]::Byte
        [Nullable[char]] = [DbType]::StringFixedLength
        [Nullable[DateOnly]] = [DbType]::Date
        [Nullable[datetime]] = [DbType]::DateTime
        [Nullable[DateTimeOffset]] = [DbType]::DateTimeOffset
        [Nullable[decimal]] = [DbType]::Decimal
        [Nullable[double]] = [DbType]::Double
        [Nullable[float]] = [DbType]::Single
        [Nullable[guid]] = [DbType]::Guid
        [Nullable[int]] = [DbType]::Int32
        [Nullable[long]] = [DbType]::Int64
        [Nullable[sbyte]] = [DbType]::SByte
        [Nullable[short]] = [DbType]::Int16
        [Nullable[TimeOnly]] = [DbType]::Time
        [Nullable[uint]] = [DbType]::UInt32
        [Nullable[ulong]] = [DbType]::UInt64
        [Nullable[ushort]] = [DbType]::UInt16
        [sbyte] = [DbType]::SByte
        [short] = [DbType]::Int16
        [string] = [DbType]::String
        [TimeOnly] = [DbType]::Time
        [uint] = [DbType]::UInt32
        [ulong] = [DbType]::UInt64
        [ushort] = [DbType]::UInt16
    }

    <#
    .SYNOPSIS
        Value indicating whether the column can be read.
    #>

    [bool] $CanRead

    <#
    .SYNOPSIS
        Value indicating whether the column can be written to.
    #>

    [bool] $CanWrite

    <#
    .SYNOPSIS
        The SQL data type of the column.
    #>

    [DbType] $DbType

    <#
    .SYNOPSIS
        Value indicating whether the column value is generated by the database.
    #>

    [bool] $IsComputed

    <#
    .SYNOPSIS
        Value indicating whether the column value is generated by the database when a row is inserted.
    #>

    [bool] $IsIdentity

    <#
    .SYNOPSIS
        Value indicating whether the column value is nullable.
    #>

    [bool] $IsNullable

    <#
    .SYNOPSIS
        The column name.
    #>

    [string] $Name

    <#
    .SYNOPSIS
        The type of the column value.
    #>

    [Type] $PropertyType

    <#
    .SYNOPSIS
        The property information providing the column metadata.
    #>

    hidden [PropertyInfo] $Property

    <#
    .SYNOPSIS
        Creates new column information.
    .PARAMETER Property
        The property information providing the column metadata.
    #>

    DbColumnInfo([PropertyInfo] $Property) {
        $this.CanRead = $Property.CanRead
        $this.CanWrite = $Property.CanWrite
        $this.Property = $Property
        $this.PropertyType = $Property.PropertyType

        $column = [Attribute]::GetCustomAttribute($Property, [ColumnAttribute])
        $this.Name = ${column}?.Name ?? $Property.Name

        $dataType = [DbType]::Object
        $this.DbType = switch ($true) {
            ([string]::IsNullOrWhiteSpace(${column}?.TypeName)) { [DbColumnInfo]::TypeMap[$property.PropertyType] ?? [DbType]::Object; break }
            ([Enum]::TryParse([DbType], $column.TypeName, $true, [ref] $dataType)) { $dataType; break }
            default { [DbType]::Object }
        }

        $databaseGeneratedOption = [Attribute]::GetCustomAttribute($Property, [DatabaseGeneratedAttribute])?.DatabaseGeneratedOption ?? [DatabaseGeneratedOption]::None
        $this.IsComputed = $databaseGeneratedOption -ne [DatabaseGeneratedOption]::None
        $this.IsIdentity = $databaseGeneratedOption -eq [DatabaseGeneratedOption]::Identity
        $this.IsNullable = (-not [Attribute]::IsDefined($Property, [ValidateNotNullAttribute])) -and `
            (($null -ne [Nullable]::GetUnderlyingType($Property.PropertyType)) -or ([DbColumnInfo]::NullabilityContext.Value.Create($Property).WriteState -ne [NullabilityState]::NotNull))
    }

    <#
    .SYNOPSIS
        Gets the property value of a specified object.
    .PARAMETER Instance
        The object whose property value will be returned.
    .OUTPUTS
        The property value of the specified object.
    #>

    [object] GetValue([object] $Instance) {
        return $this.Property.GetValue($Instance)
    }

    <#
    .SYNOPSIS
        Sets the property value of a specified object.
    .PARAMETER Instance
        The object whose property value will be set.
    .PARAMETER Value
        The new property value.
    #>

    [void] SetValue([object] $Instance, [object] $Value) {
        $this.Property.SetValue($Instance, $Value)
    }
}