SqlQueryClass.psm1

Function Dismount-Database {
    Param (
        $connectionString = "Data Source=(localdb)\MSSQLLocalDB;Integrated Security=True",
        $Database = 'TestDatabase1',
        [Switch]$Quiet
    )
    Write-Warning "`nDetaching Database: ($Database)"
    $NonQuery = "EXEC sp_detach_db '$Database'"
    $Splat = @{connectionString = $connectionString; NonQuery = $NonQuery}
    # Remove any null value parameters to use called function's default value
    $Splat.Keys.Where({-not $Splat[$_]}).ForEach({$Splat.Remove($_)})
    Invoke-DatabaseNonQuery @Splat -Quiet:$Quiet
}
<# Usage Example: # >
$Error.Clear()
# Dismount-Database
# Dismount-Database -Database:$null
Dismount-Database -Database 'TestDataBase1'
Get-Database
#>


Function Get-Database {
    Param (
        $connectionString = "Data Source=(localdb)\MSSQLLocalDB;Integrated Security=True",
        $query = "SELECT name FROM sys.databases;",
        [Switch]$Quiet
    )
    $Splat = @{connectionString = $connectionString; query = $query}
    # Remove any null value parameters to use called function's default value
    $Splat.Keys.Where({-not $Splat[$_]}).ForEach({$Splat.Remove($_)})
    Invoke-DatabaseQuery @Splat -Quiet:$Quiet
}
<# Usage Examples # >
Get-Database
Get-Database -Quiet
Get-Database -connectionString:$null
#>

Function Get-DatabaseTable {
    Param (
        $connectionString,
        $query = "SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_SCHEMA, TABLE_NAME;",
        [Switch]$Quiet
    )
    $Splat = @{ connectionString = $connectionString; query = $query }
    # Remove any null value parameters to use called function's default value
    $Splat.Keys.Where({-not $Splat[$_]}).ForEach({$Splat.Remove($_)})
    Invoke-DatabaseQuery @Splat -Quiet:$Quiet
}
<# Usage Examples: # >
Get-DatabaseTable
#>


Function Invoke-DatabaseNonQuery {
    Param (
        $connectionString = "Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=C:\Git\SqlQueryClass\tests\TestDatabase1.mdf;Integrated Security=True",
        $NonQuery,
        [Switch]$Quiet
    )
    If ([String]::IsNullOrWhiteSpace($NonQuery)) {
        Throw "Parameter -NonQuery cannot be null or empty"
    }
    If (-Not $Quiet) {
        Write-Host "`nUsing ConnectionString: ($connectionString)"
        Write-Host ("Executing Database NonQuery: $($NonQuery | Out-String)").Trim()
    }
    Try {
        # Create and open the connection
        $connection = New-Object System.Data.SqlClient.SqlConnection($connectionString)
        $connection.Open()
        # Create a command to attach the database
        $attachCommand = $connection.CreateCommand()
        $attachCommand.CommandText = $NonQuery
        $attachCommand.ExecuteNonQuery()
    } Catch {
        Write-host ($_ | Out-String) -ForegroundColor Red
    } Finally {
        # Close connection
        $connection.Close()
    }
}
<# Usage Example: # >
$Error.Clear()
Invoke-DatabaseNonQuery -NonQuery "EXEC sp_detach_db 'NORTHWIND'"
#>


Function Invoke-DatabaseQuery {
    Param (
        $connectionString = "Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=C:\Git\SqlQueryClass\tests\TestDatabase1.mdf;Integrated Security=True",
        $query,
        [Switch]$Quiet
    )
    If ([String]::IsNullOrWhiteSpace($Query)) {
        Throw "Parameter -Query cannot be null or empty"
    }
    If (-Not $Quiet) {
        Write-Host "`nUsing ConnectionString: ($connectionString)"
        Write-Host ("Executing SQL Query: $($Query | Out-String)").Trim()
    }
    $SQLReader = $null
    $Result = $null
    Try {
        # Create and open the connection
        $connection = New-Object System.Data.SqlClient.SqlConnection($connectionString)
        $connection.Open()

        # Create the command
        $command = $connection.CreateCommand()
        $command.CommandText = $query
        $command.CommandTimeout = 600

        # Execute the command and read the results
        $SQLReader = $command.ExecuteReader()
        If ($SQLReader) {
            $Result = [System.Data.DataTable]::new()
            $Result.Load($SQLReader)
        }
        # Write-Host ($Result.Rows | Out-String)
        Return ,$Result
    } Catch {
        Write-host ($_ | Out-String) -ForegroundColor Red
    } Finally {
        # Close the reader and the connection
        If ($SQLReader) {
            $SQLReader.Close()
        }
        $connection.Close()
    }
}
<# Usage Example: # >
Invoke-DatabaseQuery -connectionString 'Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=C:\Git\SqlQueryClass\tests\TestDatabase1.mdf;Integrated Security=True' -query 'SELECT database_id, name, compatibility_level FROM sys.databases;'
Invoke-DatabaseQuery -query "SELECT TABLE_SCHEMA, TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_SCHEMA, TABLE_NAME;"
Invoke-DatabaseQuery -query "SELECT * FROM SqlQuery;"
Invoke-DatabaseQuery -query "SELECT * FROM SqlQueryParms;"
#>


Function Mount-Database {
    Param (
        $connectionString = "Data Source=(localdb)\MSSQLLocalDB;Integrated Security=True",
        $Database = 'TestDatabase1',
        $DatabaseFilePath = 'C:\Git\SqlQueryClass\tests\TestDatabase1.mdf',
        [Switch]$Quiet
    )
    Write-Warning "`nAttaching Database: ($Database) with Database File ($DatabaseFilePath)"
    $Query = "CREATE DATABASE $Database ON (FILENAME = '$DatabaseFilePath') FOR ATTACH"
    $Splat = @{ connectionString = $connectionString; query = $query }
    # Remove any null value parameters to use called function's default value
    $Splat.Keys.Where({-not $Splat[$_]}).ForEach({$Splat.Remove($_)})
    Invoke-DatabaseQuery @Splat -Quiet:$Quiet
}
<# Usage Example: # >
$Error.Clear()
Mount-Database
Get-Database
# Mount-Database -Database 'TestDataBase1' -DatabaseFilePath 'C:\Git\SqlQueryClass\tests\TestDatabase1.mdf'
#>


<#
.SYNOPSIS
    New-SqlQueryDataSet -- Creates and returns an Object instance of the [SqlQueryDataSet] class configured with or without the specified parameters.
.DESCRIPTION
    This function initializes a new instance of the [SqlQueryDataSet] class and the resulting object is configured is based which parameters were specified.
    
    All parameters are optional as the can be configured later using the [SqlQueryDataSet]$object returned when calling $object = New-SqlQueryDataSet
    When using $SQLServer and $Database, both must be specified together. The [SqlQueryDataSet] class will auto generate a SQL ConnectionString.
    Specifying $ConnectionString overrides auto generation even when $SQLServer and $Database are also specified.
    
    Based on which parameters are passed, this CmdLet will use one of the overloaded class constructors and configure instance settings with the other parameters:
    - [SqlQueryDataSet]::new()
    - [SqlQueryDataSet]::new(string SQLServer, string Database)
    - [SqlQueryDataSet]::new(string SQLServer, string Database, string Query)

    Explanation of Parameter Sets:
    - **`ServerDatabase`**: This parameter set allows the user to specify the SQL Server and Database separately without needing a full connection string.
    - **`ServerDatabaseWithConnectionString`**: This parameter set allows the user to provide both the SQL Server and Database separately, or use a connection string.
    - **`ConnectionString`**: This parameter set allows the user to provide a connection string directly.
.PARAMETER SQLServer
    The name or address of the SQL Server to connect to. This parameter is optional, but when used, must be specified with $Database.
.PARAMETER Database
    The name of the database to connect to on the specified SQL Server. This parameter is also optional, but when used, must be specified with $SQLServer.
.PARAMETER ConnectionString
    The full connection string to use for connecting to the SQL Server. This parameter is optional and provides an alternative to specifying $SQLServer and $Database separately.
.PARAMETER Query
    The SQL query to execute against the database. This parameter is optional, only configures the query settings, and does not trigger execution. Best when used with $TableName.
.PARAMETER TableName
    Unique identifier that names the configuration of this Query. This parameter is optional and a $TableName will be parsed from only simple queries which is why its best to specify with $Query.
.PARAMETER DisplayResults
    Boolean flag indicating whether to display the query results. The default value is $true.
    The [SqlQueryDataSet] class uses this flag to output content to standard out when executing content generating methods such as Execute().
.FUNCTIONALITY
    Creates and initializes an Instance of [SqlQueryDataSet] class
.OUTPUTS
    Object of [SqlQueryDataSet] class
.EXAMPLE
    $result = New-SqlQueryDataSet -SQLServer "myServer" -Database "myDB" -Query "SELECT * FROM myTable"
.EXAMPLE
    $result = New-SqlQueryDataSet -ConnectionString "Server=myServer;Database=myDB;User Id=myUser;Password=myPass;" -Query "SELECT * FROM myTable" -TableName myTable -DisplayResults $false
.NOTES
    Author: Brooks Vaughn
    Date: 2025-02-01
    Version: 0.1.0
#>


function New-SqlQueryDataSet {
    [CmdletBinding(DefaultParameterSetName = 'ServerDatabase')]
    # Suppress PSScriptAnalyzer rule about ShouldProcess
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]

    param (
        [Parameter(Mandatory = $false)]
        [string]$SQLServer,

        [Parameter(Mandatory = $false)]
        [ValidateScript({ if ($SQLServer -and -not $_) { throw "Database must be provided if SQLServer is specified." } else { $true } })]
        [string]$Database,

        [Parameter(Mandatory = $false)]
        [string]$ConnectionString,

        [Parameter(Mandatory = $false)]
        [ValidateScript({ if ($TableName -and -not $_) { throw "Query must be provided if TableName is specified." } else { $true } })]
        [string]$Query,

        [Parameter(Mandatory = $false)]
        [string]$TableName,

        [Parameter(Mandatory = $false)]
        [bool]$DisplayResults = $true
    )

    # Function logic here
    if ($SQLServer -and -not $Database) {
        Write-Warning "Database must be provided if SQLServer is specified."
    }

    if ($Database -and -not $SQLServer) {
        Write-Warning "SQLServer must be provided if Database is specified."
    }

    if ($TableName -and -not $Query) {
        Write-Warning "Query must be provided if TableName is specified."
    }

    $instance = $null
    if (-not [string]::IsNullOrEmpty($SQLServer) -and -not [string]::IsNullOrEmpty($Database)) {
        $instance = [SqlQueryDataSet]::new($SQLServer, $Database)
    } else {
        $instance = [SqlQueryDataSet]::new()
    }
    if (-not [string]::IsNullOrEmpty($Query)) {
        if (-not [string]::IsNullOrEmpty($TableName)) {
            [void]$instance.AddQuery($TableName, $Query)
        } else {
            [void]$instance.AddQuery($Query)
        }
    }
    if (-not [string]::IsNullOrEmpty($ConnectionString)) { $instance.ConnectionString = $ConnectionString }
    if (-not [string]::IsNullOrEmpty($SQLServer)) { $instance.SQLServer = $SQLServer }
    if (-not [string]::IsNullOrEmpty($Database)) { $instance.Database = $Database }
    $instance.DisplayResults = $DisplayResults
    return $instance
}

<#
.SYNOPSIS
. 'C:\Git\SqlQueryEditor\scripts\SqlQueryClass.ps1'
 
    SqlQueryClass.ps1 -- Dot Source file of SQL Query Class definitions for classes [SqlQueryDataSet] and [SqlQueryTable]
 
.DESCRIPTION
    This script defines two PowerShell classes [SqlQueryDataSet] and [SqlQueryTable] which are used to execute SQL Queries and return the results in a DataTable, DataAdapter, DataSet, DataRows ([Array]DataRow) or NonQuery.
 
    The parent class, [SqlQueryDataSet], is designed to manage SQL Server connections, execute queries, and methods to manage data.
    The child class, [SqlQueryTable], is designed to manage a table SQL query's configuration and query results.
 
    [SqlQueryDataSet] class property, [Tables], is a collection of child class [SqlQueryTable] objects.
    When a Query is added to the [SqlQueryDataSet] object, a new [SqlQueryTable] object is created and added to the [Tables] collection.
 
    The original design was a single class of [SqlQueryDataSet] with a single Query and Result. The SQL query could have several statements separated by semi-colons resulting in a DataSet result.
    The DataSet approach worked out well for binding with WPF components provided you know which tables[index] was associated with which query.
    When it came to how to edit and save changes for a specific DataTable in the DataSet, there was no easy way to only update a single table without looping through all the tables in the DataSet and checking for changes.
    Another issue was with the default values on columns like Id, CreatedOn, which were updated in the database but could not get the WPF Components to reflect the changes without executing a new query and setting the ItemsSources.
 
    The next approach was resorting to ObservableCollection and DataView objects. This involved creating Model-Views for each table, a corresponding class, and functions to Add, Update, Delete, and Save. This was a lot of hard coding and not very flexible and also suffered from the default column values issue.
 
    A working solution was not to use multiple queries in a single DataSet but to have separate instances of [SqlQueryDataSet] for each table query executed as a SQLDataAdapter.
    Using this approach, I was able to easily edit any table using the same DataGrid component and the application does not need to know the table schema of how to Select, Insert, Update, or Delete.
    The [SqlQueryDataSet] class handles all of that dynamically utilizing the features [System.Data.SqlClient.SqlConnection], [System.Data.SqlClient.SqlCommand], [System.Data.SqlClient.SqlDataAdapter], [System.Data.SqlClient.SqlCommandBuilder], and [System.Data.DataViewRowState] objects.
 
    This class was built for a document scanning application that has several comboboxes and DataGrids populated from different table queries. It used a collection of [SqlQueryDataSet] objects in the main program and an enum of the tableNames to assist with accessing the correct DataSet results.
I was uncomfortable with the idea of hardcoding the table names and having separate [SqlQueryDataSet] objects with the same repeating Database configurations and connections.
This lead to having the [SqlQueryDataSet] maintain a collection of table queries ([SqlQueryTable]) which is where we are at now.
 
Usage Documentation:
 
An instance of the [SqlQueryDataSet] can be created using any one of the following constructors:
    $TestQuery = [SqlQueryDataSet]::new()
    $TestQuery = [SqlQueryDataSet]::new($SQLServer)
    $TestQuery = [SqlQueryDataSet]::new($SQLServer, $Database)
    $TestQuery = [SqlQueryDataSet]::new($SQLServer, $Database, $Query)
 
When a Query is added to the [SqlQueryDataSet] object, a new [SqlQueryTable] object is created and added to [SqlQueryDataSet].Tables collection. The TableName and it's Index in the Tables collection is added a the TableNames [HashTable].
 
A Query can be added to the [SqlQueryDataSet] object in the following ways:
    $TestQuery = [SqlQueryDataSet]::new($SQLServer, $Database, $Query)
    $TestQuery.ExecuteQuery($Query)
    $TestQuery.ExecuteAsDataTable($Query)
    $TestQuery.ExecuteAsDataAdapter($Query)
    $TestQuery.ExecuteAsDataSet($Query)
    $TestQuery.ExecuteAsDataRows($Query)
    $TestQuery.AddQuery($Query)
    $TestQuery.LoadQueryFromFile($PathToSQLFile)
 
using the AddQuery() method or LoadQueryFromFile() method.
    $TestQuery.AddQuery($Query)
    $TestQuery.LoadQueryFromFile($PathToSQLFile)
 
 
 
It's only been test using SQL Express
 
Microsoft SQL Server 2014 (SP1-CU7) (KB3162659) - 12.0.4459.0 (X64)
                May 27 2016 15:33:17
                Copyright (c) Microsoft Corporation
                Express Edition (64-bit) on Windows NT 6.3 <X64> (Build 19045: ) (Hypervisor)
 
Example that produced the above
 
. "C:\Git\SqlQueryEditor\scripts\SqlQueryClass.ps1"
 
$SqlServer = '(localdb)\MSSQLLocalDB'
$DatabaseName = 'F:\DATA\BILLS\PSSCRIPTS\SCANMYBILLS\DATABASE1.MDF'
$ConnectionString = "Data Source={0};AttachDbFilename={1};Integrated Security=True" -f $SqlServer, $DatabaseName
 
$TestQuery = [SqlQueryDataSet]::new()
$TestQuery.Database = $DatabaseName
$TestQuery.ConnectionString = $ConnectionString
$TestQuery.DisplayResults = $true
 
$TestQuery.AddQuery('Version', "SELECT @@VERSION FROM INFORMATION_SCHEMA.TABLES")
$TestQuery.Tables[0].Query = "SELECT TABLE_NAME, @@VERSION FROM INFORMATION_SCHEMA.TABLES"
$TestQuery.Execute()
 
$TestQuery.ExecuteQuery('SELECT @@VERSION FROM INFORMATION_SCHEMA.TABLES')
$TestQuery.Tables[0].Result
 
$TestQuery.ExecuteAsDataSet('SELECT @@VERSION FROM INFORMATION_SCHEMA.TABLES')
 
$TestQuery.AddQuery("SELECT * FROM [dbo].[SqlQuery] ORDER BY Id DESC")
 
Setting the TableIndex controls which table query will be selected when performing Executions and Saves. These are a few ways to set the TableIndex:
$TestQuery.TableIndex = 2
$TestQuery.TableIndex = $TestQuery.Tables.Where({$_.TableName -eq 'SqlQuery'})[0].TableIndex
$TestQuery.TableIndex = $TestQuery.TableNames['SqlQuery']
 
 
Helper Methods
$TestQuery.BuildOleDbConnectionString() -- Builds a connection string for OleDb
$TestQuery.BuildConnectionString() -- [Hidden] Builds a connection string for SqlClient used as a fallback when $TestQuery.ConnectionString is missing or fails
$TestQuery.Clear() -- Closes the connection and clears all the properties and collections of the [SqlQueryDataSet] object
$TestQuery.CloseConnection() -- Closes the SQL Connection
$TestQuery.GetCreateBasicDLL($TableName) -- Returns a DataTable with the basic structure of a table with the following columns: Id, CreatedOn, CreatedBy, UpdatedOn, UpdatedBy
$TestQuery
$TestQuery
$TestQuery
$TestQuery
$TestQuery
 
 
AddQuery Method int AddQuery(string Query), int AddQuery(string TableName, string Query)
BuildOleDbConnectionString Method string BuildOleDbConnectionString()
Clear Method void Clear()
CloseConnection Method void CloseConnection()
Equals Method bool Equals(System.Object obj)
Execute Method System.Object Execute(), System.Object Execute(SqlQueryTable table), System.Object Execute(int TableIndex), System.Object Execute(stri…
ExecuteAsDataAdapter Method System.Object ExecuteAsDataAdapter(string SqlQuery)
ExecuteAsDataSet Method System.Object ExecuteAsDataSet(string SqlQuery)
ExecuteAsDataTable Method System.Object ExecuteAsDataTable(string SqlQuery)
ExecuteAsDataRows Method System.Object ExecuteAsDataRows(string SqlQuery)
ExecuteNonQuery Method System.Object ExecuteNonQuery(string SqlQuery)
ExecuteQuery Method System.Object ExecuteQuery(string SqlQuery), System.Object ExecuteQuery(string TableName, string SqlQuery)
GetCreateBasicDLL Method System.Object GetCreateBasicDLL(string TableName)
GetCreateDDL Method System.Object GetCreateDDL(string TableName)
GetDBTableIndexes Method System.Object GetDBTableIndexes(string TableName)
GetDBTableIndexesV17 Method System.Object GetDBTableIndexesV17(string TableName)
GetDBTableSchema Method System.Object GetDBTableSchema(string TableName)
GetHashCode Method int GetHashCode()
GetTableFromQuery Method System.Object GetTableFromQuery(string Query)
GetTableFromTableName Method System.Object GetTableFromTableName(string TableName)
GetType Method type GetType()
LoadQueryFromFile Method void LoadQueryFromFile(string Path)
OpenConnection Method void OpenConnection()
ParseSQLQuery Method System.Object ParseSQLQuery(string Query)
SaveChanges Method System.Object SaveChanges()
ToString Method string ToString()
CommandTimeout Property int CommandTimeout {get;set;}
ConnectionString Property string ConnectionString {get;set;}
ConnectionTimeout Property int ConnectionTimeout {get;set;}
Database Property string Database {get;set;}
DisplayResults Property bool DisplayResults {get;set;}
KeepAlive Property bool KeepAlive {get;set;}
SQLConnection Property System.Object SQLConnection {get;set;}
SQLServer Property string SQLServer {get;set;}
TableIndex Property int TableIndex {get;set;}
TableNames Property hashtable TableNames {get;set;}
Tables Property System.Collections.Generic.List[SqlQueryTable] Tables {get;set;}
 
$TestQuery.Tables | GM
 
 
 
Helpful commands to check and manage SQL Express:
 
# Check environment PATH for Sql Related Paths
ForEach ($path in (([environment]::GetEnvironmentVariables()).Path -split ';').Where({$_ -like '*SQL*'})) {(Get-ChildItem -Path $path -Filter *.exe -Recurse).FullName}
 
# Check environment PATH for Sql Express Paths
ForEach ($path in (([environment]::GetEnvironmentVariables()).Path -split ';').Where({$_ -like '*SQL*'})) {(Get-ChildItem -Path $path -Filter sqllocaldb.exe -Recurse).FullName}
C:\Program Files\Microsoft SQL Server\110\Tools\Binn\SqlLocalDB.exe
C:\Program Files\Microsoft SQL Server\120\Tools\Binn\SqlLocalDB.exe
C:\Program Files\Microsoft SQL Server\130\Tools\Binn\SqlLocalDB.exe
 
# Display Help
SqlLocalDB.exe -?
 
# Show Version
SqlLocalDB.exe v
Microsoft SQL Server 2012 (11.0.3000.0)
Microsoft SQL Server 2014 (12.0.4459.0)
Microsoft SQL Server 2016 (13.0.7037.1)
 
# List Databases
SqlLocalDB.exe info
MSSQLLocalDB
ProjectsV13
v11.0
 
# Info on specific Database
SqlLocalDB.exe info MSSQLLocalDB
Name: MSSQLLocalDB
Version: 12.0.4459.0
Shared name:
Owner: Desktop\Brooks
Auto-create: Yes
State: Running
Last start time: 1/31/2025 9:51:16 AM
Instance pipe name:
 
# Get current Database Status
$status = ((SqlLocalDB.exe info MSSQLLocalDB) -split [Environment]::NewLine).Where({$_}).Where({$_.StartsWith('State:')})[0].Split(' ')[-1]
$status
 
# Start the Database
SqlLocalDB.exe Start MSSQLLocalDB
 
 
.AUTHOR
    Brooks Vaughn
 
.COMPONENT
    VERSION 1.0.0
 
.NOTES
When using the debugger, I always get this error and not sure why or how to correct it as I have no idea where it is coming from.
ParserError:
Line |
   1 | [System.Diagnostics.DebuggerHidden()]param() ,eval $stdout.sync=true
     | ~
     | Missing expression after unary operator ','.
it was suggested to use this at top of my scripts:
[System.Diagnostics.DebuggerHidden()]
param()
It doesn't seem to help. Strange think is it happens every other execution
 
.EXAMPLE
 
# Configure Database settings for connection
$SqlServer = '(localdb)\MSSQLLocalDB'
$DatabaseName = 'F:\DATA\BILLS\PSSCRIPTS\SCANMYBILLS\DATABASE1.MDF'
$ConnectionString = "Data Source={0};AttachDbFilename={1};Integrated Security=True" -f $SqlServer, $DatabaseName
 
 
# Create a new instance of SqlQueryDataSet
$TestQuery = New-SqlQueryDataSet -SQLServer $SqlServer -Database $DatabaseName -ConnectionString $ConnectionString -DisplayResults $true
# There are at least 12 different overloaded Execute methods used execute queries and return results as different object types.
 
# Example usage of the class
$TestQuery.ExecuteQuery('DBTables', "SELECT TABLE_NAME, @@VERSION FROM INFORMATION_SCHEMA.TABLES")
 
# Displaying the Results of the Query
$TestQuery.Tables[0].Result
 
# Changing an existing Table Query and then executing it.
$TestQuery.Tables[0].Query = "SELECT * FROM INFORMATION_SCHEMA.TABLES"
$TestQuery.Execute($TestQuery.Tables[0])
 
$TestQuery = New-SqlQueryDataSet -SQLServer $SqlServer -Database $DatabaseName -ConnectionString $ConnectionString -DisplayResults $true -TableName 'Category' -Query 'SELECT * FROM [dbo].Category;'
$TestQuery = New-SqlQueryDataSet -SQLServer $SqlServer -Database $DatabaseName -ConnectionString $ConnectionString -DisplayResults $false
$TestQuery.ExecuteQuery('Category', 'SELECT * FROM [dbo].Category;')
$TestQuery.Tables[0].Result[0]
$TestQuery.DisplayResults = $false
$TestQuery.Execute($TestQuery.Tables[0])
 
.NOTES
C:\Git\SqlQueryEditor> $TestQuery = [SqlQueryDataSet]::new()
PS C:\Git\SqlQueryEditor> $TestQuery | GM
 
   TypeName: SqlQueryDataSet
 
Name MemberType Definition
---- ---------- ----------
AddQuery Method int AddQuery(string Query), int AddQuery(string TableName, string Query)
BuildOleDbConnectionString Method string BuildOleDbConnectionString()
Clear Method void Clear()
CloseConnection Method void CloseConnection()
Equals Method bool Equals(System.Object obj)
Execute Method System.Object Execute(), System.Object Execute(SqlQueryTable table), System.Object Execute(int TableIndex), System.Object Execute(stri…
ExecuteAsDataAdapter Method System.Object ExecuteAsDataAdapter(string SqlQuery)
ExecuteAsDataSet Method System.Object ExecuteAsDataSet(string SqlQuery)
ExecuteAsDataTable Method System.Object ExecuteAsDataTable(string SqlQuery)
ExecuteAsDataRows Method System.Object ExecuteAsDataRows(string SqlQuery)
ExecuteNonQuery Method System.Object ExecuteNonQuery(string SqlQuery)
ExecuteQuery Method System.Object ExecuteQuery(string SqlQuery), System.Object ExecuteQuery(string TableName, string SqlQuery)
GetCreateBasicDLL Method System.Object GetCreateBasicDLL(string TableName)
GetCreateDDL Method System.Object GetCreateDDL(string TableName)
GetDBTableIndexes Method System.Object GetDBTableIndexes(string TableName)
GetDBTableIndexesV17 Method System.Object GetDBTableIndexesV17(string TableName)
GetDBTableSchema Method System.Object GetDBTableSchema(string TableName)
GetHashCode Method int GetHashCode()
GetTableFromQuery Method System.Object GetTableFromQuery(string Query)
GetTableFromTableName Method System.Object GetTableFromTableName(string TableName)
GetType Method type GetType()
LoadQueryFromFile Method void LoadQueryFromFile(string Path)
OpenConnection Method void OpenConnection()
ParseSQLQuery Method System.Object ParseSQLQuery(string Query)
SaveChanges Method System.Object SaveChanges()
ToString Method string ToString()
CommandTimeout Property int CommandTimeout {get;set;}
ConnectionString Property string ConnectionString {get;set;}
ConnectionTimeout Property int ConnectionTimeout {get;set;}
Database Property string Database {get;set;}
DisplayResults Property bool DisplayResults {get;set;}
KeepAlive Property bool KeepAlive {get;set;}
SQLConnection Property System.Object SQLConnection {get;set;}
SQLServer Property string SQLServer {get;set;}
TableIndex Property int TableIndex {get;set;}
TableNames Property hashtable TableNames {get;set;}
Tables Property System.Collections.Generic.List[SqlQueryTable] Tables {get;set;}
 
PS C:\Git\SqlQueryEditor> $TestQuery.AddQuery("SELECT TABLE_NAME, @@VERSION FROM INFORMATION_SCHEMA.TABLES")
 
dataBase schemaTable Schema TableName
-------- ----------- ------ ---------
         INFORMATION_SCHEMA.TABLES INFORMATION_SCHEMA TABLES
 
GetTableFromQuery(): $schemaTable.tableName=TABLES, TableName=TABLES, TableIndex=0
 
PS C:\Git\SqlQueryEditor> $TestQuery.Tables | GM
 
   TypeName: SqlQueryTable
 
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
isDirty Property bool isDirty {get;set;}
Parent Property SqlQueryDataSet Parent {get;set;}
Query Property string Query {get;set;}
QueryFile Property string QueryFile {get;set;}
Result Property System.Object Result {get;set;}
ResultType Property ResultType ResultType {get;set;}
SQLCommand Property System.Object SQLCommand {get;set;}
SqlDataAdapter Property System.Object SqlDataAdapter {get;set;}
TableIndex Property int TableIndex {get;set;}
TableName Property string TableName {get;set;}
 
#>


enum ResultType { DataTable; DataRows; DataAdapter; DataSet; NonQuery; }

class SqlQueryTable {
    [int]$TableIndex = 0
    [string]$TableName = [string]::Empty
    [string]$Query = [string]::Empty
    [object]$SQLCommand = $null
    [object]$SqlDataAdapter = $null
    [ResultType]$ResultType = [ResultType]::DataTable
    [object]$Result = $null
    [bool]$isDirty = $false
    [string]$QueryFile = [string]::Empty
    [SqlQueryDataSet]$Parent = $null

     # Constructor -empty object
    SqlQueryTable ()
    { 
        Return
    }
}

class SqlQueryDataSet {
    [string]$SQLServer
    [string]$Database
    [int]$ConnectionTimeout = 5
    [int]$CommandTimeout = 600
    # Connection string keywords: https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnection.connectionstring(v=vs.110).aspx
    [string]$ConnectionString
    [object]$SQLConnection
    [int]$TableIndex = 0
    [System.Collections.Generic.List[SqlQueryTable]]$Tables
    [System.Collections.Hashtable]$TableNames = @{}
    [bool]$DisplayResults = $True
    [bool]$KeepAlive = $False

     # Constructor -empty object
    SqlQueryDataSet () { Return }
    
    # Constructor - sql server and database
    SqlQueryDataSet ([String]$SQLServer, [String]$Database) { 
        $This.SQLServer = $SQLServer
        $This.Database = $Database
    }

    # Constructor - sql server, database and query
    SqlQueryDataSet ([String]$SQLServer, [String]$Database, [string]$Query) { 
        $This.SQLServer = $SQLServer
        $This.Database = $Database
        $This.TableIndex = $This.AddQuery($Query)
    }

    # Methods to Add New Table for Query and Results
    [int] AddQuery([String]$Query) {
        Return $This.AddQuery([String]::Empty, $Query)
    }
    [int] AddQuery([String]$TableName, [String]$Query) {
        If (-not $this.Tables) {
            $this.Tables = [System.Collections.Generic.List[SqlQueryTable]]::new()
        }
        $table = $null
        $schemaTable = $This.ParseSQLQuery($Query)
        If ([string]::IsNullOrEmpty($TableName)) {
            $table = $This.GetTableFromQuery([String]$Query)
            Write-Host ($schemaTable | Out-String)
            $tableName = $schemaTable.tableName
        } Else {
            $table = $This.GetTableFromTableName([String]$TableName)
        }
        If ([String]::IsNullOrEmpty($table)) {
            $index = $This.TableNames[$TableName]
            If ($index) {
                $table = $This.Tables[$index]
            }
            If ([String]::IsNullOrEmpty($table)) {
                $table = [SqlQueryTable]::new()
                $table.Parent = $This
                $table.Query = $Query
                $table.TableIndex = $This.Tables.Count
                $table.TableName = $TableName
                $This.TableNames.Add($TableName,$table.TableIndex)
                [void]$This.Tables.Add($table)
            } Else {
                $This.TableIndex = $table.TableIndex
            }
        } Else {
            $This.TableIndex = $table.TableIndex
            If ($schemaTable.tableName -ne $table.TableName) {
                Write-Warning "GetTableFromQuery() TableName from Query is different then TableName in Tables: `$schemaTable.tableName=$($schemaTable.tableName), TableName=$($table.TableName), TableIndex=$($table.TableIndex)"
            }
        }
        Write-Host "GetTableFromQuery(): `$schemaTable.tableName=$($schemaTable.tableName), TableName=$($table.TableName), TableIndex=$($table.TableIndex)" -ForegroundColor Green
        Return ($table.TableIndex)
    }

    # Method to Find existing Table based on if there is an existing SQL Query
    [Object] GetTableFromQuery([String]$Query) {
        $table = $null
        If ($this.Tables) {
            $table = $This.Tables.Where({$_.Query -eq $Query})
            If ($table) {
                Return ($table[0])
            }
        }
        Return ($table)
    }

   # Method to Find existing Table based on TableName
    [Object] GetTableFromTableName([String]$TableName) {
        $table = $null
        If ($this.Tables) {
            $table = $This.Tables.Where({$_.TableName -eq $TableName})
            If ($table) {
                Return ($table[0])
            }
        }
        Return ($table)
    }

    # Method
    hidden [String]BuildConnectionString() {
        Return ("Server=$($This.SQLServer);Database=$($This.Database);Integrated Security=SSPI;Connection Timeout=$($This.ConnectionTimeout)")
        # Return ("Data Source=$($This.SQLServer);Initial Catalog=$($This.Database);Integrated Security=True;Connect Timeout=$($This.ConnectionTimeout);Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False")
    }

    [String]BuildOleDbConnectionString() {
        # Return ("Provider=MSOLEDBSQL;Data Source=$($This.SQLServer);Initial Catalog=$($This.Database);Integrated Security=SSPI;Connection Timeout=$($This.ConnectionTimeout)")
        # Return ("Provider=MSOLEDBSQL;Server=$($This.SQLServer);Initial Catalog=$($This.Database);Integrated Security=SSPI;Connection Timeout=$($This.ConnectionTimeout)")
        # Return ("Provider=MSOLEDBSQL;Server=$($This.SQLServer);Database=$($This.Database);Trusted_Connection=yes")
        # Return ("Provider=MSOLEDBSQL;DataTypeCompatibility=80;Server=$($This.SQLServer);Database=$($This.Database);Trusted_Connection=yes")
        # Return ("Provider=MSOLEDBSQL;Driver={ODBC Driver 17 for SQL Server};Server=$($This.SQLServer);Database=$($This.Database);Trusted_Connection=yes;Connection Timeout=$($This.ConnectionTimeout)")
        Return ("Provider=MSOLEDBSQL;Server=$($This.SQLServer);Database=$($This.Database);Trusted_Connection=yes;Connection Timeout=$($This.ConnectionTimeout)")
        <#
        All the above: "Could not find server 'System' in sys.servers.
          Verify that the correct server name was specified.
          If necessary, execute the stored procedure sp_addlinkedserver to add the server to sys.servers."
        #>

    }

    # Method
    LoadQueryFromFile([String]$Path) {
        If (Test-Path $Path) {
            If ([IO.Path]::GetExtension($Path) -ne ".sql") {
                throw [System.IO.FileFormatException] "'$Path' does not have an '.sql' extension'"
            } Else {
                Try {
                    [String]$QueryStatement = Get-Content -Path $Path -Raw -ErrorAction Stop
                    $This.TableIndex = $This.AddQuery($QueryStatement)
                    $This.Tables[$This.TableIndex].QueryFile = $Path
                } Catch {
                    Write-host ($_ | Out-String) -ForegroundColor Red 
                }
            }
       } Else {
         throw [System.IO.FileNotFoundException] "'$Path' not found"
       }
    }

    # Method
    [void] OpenConnection() {
        #If localDB, Get Instance Name
        $localdb = ($This.SQLConnection.DataSource -split '\\')[-1]
        If (-not $localdb) {
            $localdb = ($This.ConnectionString -split ';').Where({$_.StartsWith('Data Source')}).ForEach({$_ -split '\\'})[-1]
        }
        $status = ((SqlLocalDB.exe info "$localdb") -split [environment]::NewLine).Where({$_})
        Switch ($status.Where({$_.StartsWith('State:')}).ForEach({($_ -split ' ')[-1]})) {
            'Stopped' {SqlLocalDB.exe Start "$localdb" ; break}
        }
        If ($This.SQLConnection -and $This.SQLConnection.State -eq 'Open' -and $This.KeepAlive) {
            Return
        } ElseIf ($This.SQLConnection -and $This.SQLConnection.State -ne 'Open') {
            $This.SQLConnection.Open()
        } Else {
            If ($This.SQLConnection -and -not $This.KeepAlive) {
                If ($This.SQLConnection.State -eq 'Open') {
                    $This.SQLConnection.Close()
                }
                $This.SQLConnection.Dispose()
            }

            If (-not $This.ConnectionString) {
                $This.ConnectionString = $This.BuildConnectionString()
            }

            If ($This.SQLConnection.State -ne 'Open') {
                $This.SQLConnection = [System.Data.SqlClient.SqlConnection]::new()
                $This.SQLConnection.ConnectionString = $This.ConnectionString
                Try {
                    $This.SQLConnection.Open()
                } Catch {
                    $This.CloseConnection()
                    Write-host ($_ | Out-String) -ForegroundColor Red
                }
            }
        }
    }

    # Method
    [void] CloseConnection() {
        If ($This.SQLConnection) {
            $This.SQLConnection.Close()
            $This.SQLConnection.Dispose()
            $This.SQLConnection = $null
        }
    }

    # Method
    [System.Data.SqlClient.SqlCommand] GetSqlCommand([string]$query) {
        $This.OpenConnection()
        $SQLCommand = $This.SQLConnection.CreateCommand()
        $SQLCommand.CommandText = $query
        $SQLCommand.CommandTimeout = $This.CommandTimeout
        $SQLCommand.Connection = $This.SQLConnection
        Return $SQLCommand
    }

    [void] Clear() {
        $This.CloseConnection()
        $This.SQLServer = $null
        $This.Database = $null
        $This.ConnectionTimeout = 5
        $This.CommandTimeout = 600
        $This.ConnectionString = $null
        $This.SQLConnection = $null
        $This.TableIndex = 0
        $This.Tables.Clear()
        $This.TableNames.Clear()
        $This.DisplayResults = $True
        $This.KeepAlive = $False
    }

    # Method
    [Object] Execute() {
        $table = $This.Tables[$This.TableIndex]
        Return $This.Execute($table)
    }
    # Method
    [Object] Execute([SqlQueryTable]$table) {
        $SQLReader = $null
        $table.SQLCommand = $This.GetSqlCommand($table.Query)
        Try {
            If ($table.ResultType -in @([ResultType]::DataTable, [ResultType]::DataRows)) {
                $SQLReader = $table.SQLCommand.ExecuteReader()
                If ($SQLReader) {
                    $table.Result = [System.Data.DataTable]::new()
                    $table.Result.Load($SQLReader)
                    If ($This.DisplayResults) {
                        If ($table.ResultType -eq [ResultType]::DataTable) {
                            Return ,$table.Result
                        } Else {
                            Return $table.Result
                        }
                    }
                }
            } ElseIf ($table.ResultType -eq [ResultType]::DataAdapter) {
                $table.Result = [System.Data.DataSet]::new()
                $table.SqlDataAdapter = [System.Data.SqlClient.SqlDataAdapter]::new($table.SQLCommand)
                [void]$table.SqlDataAdapter.Fill($table.Result)

                # $SqlCommandBuilder = [System.Data.SqlClient.SqlCommandBuilder]::new($table.SqlDataAdapter)
                # $SqlCommandBuilder.DataAdapter = $table.SqlDataAdapter
                # $SqlCommandBuilder.QuotePrefix = "["
                # $SqlCommandBuilder.QuoteSuffix = "]"
                # Try { $table.SqlDataAdapter.DeleteCommand = $SqlCommandBuilder.GetDeleteCommand()
                # } Catch {
                # Write-Warning "Failed to get DeleteCommand: $_"
                # }
                # Try { $table.SqlDataAdapter.UpdateCommand = $SqlCommandBuilder.GetUpdateCommand()
                # } Catch {
                # Write-Warning "Failed to get UpdateCommand: $_"
                # }
                # Try { $table.SqlDataAdapter.InsertCommand = $SqlCommandBuilder.GetInsertCommand()
                # } Catch {
                # Write-Warning "Failed to get InsertCommand: $_"
                # }

                If ($This.DisplayResults) {
                    Return $table.Result.Tables[0]
                }
            } ElseIf ($table.ResultType -eq [ResultType]::DataSet) {
                $table.Result = [System.Data.DataSet]::new()
                $table.SqlDataAdapter = [System.Data.SqlClient.SqlDataAdapter]::new($table.SQLCommand)
                [void]$table.SqlDataAdapter.Fill($table.Result)
                If ($This.DisplayResults) {
                    Return $table.Result.Tables[0]
                }
            } ElseIf ($table.ResultType -eq [ResultType]::NonQuery) {
                $table.Result = $table.SQLCommand.ExecuteNonQuery()
                Return $table.Result
            }
        } Catch {
            $This.SQLConnection.Close()
            Return $(Write-host ($_ | Out-String) -ForegroundColor Red)           
        } Finally {
            If (-not $this.KeepAlive) {
                $This.CloseConnection()
            }
            If ($SQLReader -and -not $SQLReader.IsClosed) {
                $SQLReader.Close()
                $SQLReader.Dispose()
            }
        }
        Return $null
    }

    # Method
    [Object] Execute([Int]$TableIndex) {
        If ($TableIndex -ge 0) {
            $This.TableIndex = $TableIndex
        }
        $table = $This.Tables[$TableIndex]
        Return ($This.Execute($table))
    }

    # Method
    [Object] Execute([String]$SqlQuery) {
        If ($SqlQuery) {
            $This.TableIndex = $This.AddQuery($SqlQuery)
        }
        $table = $This.Tables[$This.TableIndex]
        Return ($This.Execute($table))
    }

    # Method
    [Object] Execute([ResultType]$ResultType) {
        $table = $This.Tables[$This.TableIndex]
        $table.ResultType = $ResultType
        Return ($This.Execute($table))
    }

    # Method
    [Object] ExecuteNonQuery([String]$SqlQuery) {
        If ($SqlQuery) {
            $This.TableIndex = $This.AddQuery($SqlQuery)
        }
        $table = $This.Tables[$This.TableIndex]
        $table.ResultType = [ResultType]::NonQuery
        Return ($This.Execute($table))
    }

    # Method
    [Object] ExecuteQuery([String]$SqlQuery) {
        If ($SqlQuery) {
            $This.TableIndex = $This.AddQuery($SqlQuery)
        }
        $table = $This.Tables[$This.TableIndex]
        $table.ResultType = [ResultType]::DataTable
        Return ($This.Execute($table))
    }

    [Object] ExecuteQuery([String]$TableName, [String]$SqlQuery) {
        If ($SqlQuery) {
            $This.TableIndex = $This.AddQuery($TableName, $SqlQuery)
        }
        $table = $This.Tables[$This.TableIndex]
        $table.ResultType = [ResultType]::DataTable
        Return ($This.Execute($table))
    }

    # Method
    [Object] ExecuteAsDataTable([String]$SqlQuery) {
        If ($SqlQuery) {
            $This.TableIndex = $This.AddQuery($SqlQuery)
            $table = $This.Tables[$This.TableIndex]
        }
        $table = $This.Tables[$This.TableIndex]
        $table.ResultType = [ResultType]::DataTable
        Return ($This.Execute($table))
    }

    # Method
    [Object] ExecuteAsDataAdapter([String]$SqlQuery) {
        If ($SqlQuery) {
            $This.TableIndex = $This.AddQuery($SqlQuery)
        }
        $table = $This.Tables[$This.TableIndex]
        $table.ResultType = [ResultType]::DataAdapter
        Return ($This.Execute($table))
    }

    # Method
    [Object] ExecuteAsDataSet([String]$SqlQuery) {
        If ($SqlQuery) {
            $This.TableIndex = $This.AddQuery($SqlQuery)
        }
        $table = $This.Tables[$This.TableIndex]
        $table.ResultType = [ResultType]::DataSet
        Return ($This.Execute($table))
    }

    # Method
    [Object] ExecuteAsDataRows([String]$SqlQuery) {
        If ($SqlQuery) {
            $This.TableIndex = $This.AddQuery($SqlQuery)
        }
        $table = $This.Tables[$This.TableIndex]
        $table.ResultType = [ResultType]::DataRows
        Return ($This.Execute($table))
    }

    # Method
    [Object] SaveChanges() {
        $table = $This.Tables[$This.TableIndex]
        If ($This.ConnectionString) {
            $SaveChangesConnectionString = $This.ConnectionString
        } Else {
            $SaveChangesConnectionString = $This.BuildConnectionString()
        }

        $SaveChangesConnection = [System.Data.SqlClient.SqlConnection]::new()
        $SaveChangesConnection.ConnectionString = $SaveChangesConnectionString

        Try {
            $This.KeepAlive = $false
            $This.OpenConnection()

            #--------------------------------------------------
            # Create a DataView to examine the changes
            #--------------------------------------------------
            # $dataView = [System.Data.DataView]::new($This.Tables[0].Result.Tables[0])
            # # Set the RowStateFilter to display only added and modified rows.
            # $dataView.RowStateFilter = ([System.Data.DataViewRowState]::Deleted -bor [System.Data.DataViewRowState]::Added -bor [System.Data.DataViewRowState]::ModifiedCurrent)
            # ForEach ($row in $dataView) {Write-Host ($row | FT -AutoSize | Out-String).Trim()}
            # Write-Host ($dataView | FT -AutoSize | Out-String).Trim()

            $commandBuilder = [System.Data.SqlClient.SqlCommandBuilder]::new($table.SqlDataAdapter)
            $table.SqlDataAdapter.UpdateCommand = $commandBuilder.GetUpdateCommand()
            $table.SqlDataAdapter.InsertCommand = $commandBuilder.GetInsertCommand()
            $table.SqlDataAdapter.DeleteCommand = $commandBuilder.GetDeleteCommand()

            If ($table.ResultType -in @([ResultType]::DataAdapter,[ResultType]::DataSet)) {
                $table.SqlDataAdapter.Update($table.Result.Tables[0])
            } ElseIf ($table.ResultType -eq [ResultType]::DataTable) {
                $table.SqlDataAdapter.Update($table.Result)
            }

            If (-not [String]::IsNullOrEmpty($table.SqlDataAdapter.DeleteCommand)) {
                $table.SqlDataAdapter.DeleteCommand.Connection = $This.SQLConnection
            }
            If (-not [String]::IsNullOrEmpty($table.SqlDataAdapter.UpdateCommand)) {
                $table.SqlDataAdapter.UpdateCommand.Connection = $This.SQLConnection
            }
            If (-not [String]::IsNullOrEmpty($table.SqlDataAdapter.InsertCommand)) {
                $table.SqlDataAdapter.InsertCommand.Connection = $This.SQLConnection
            }
            
            Try { # First process deletes.
                $table.SqlDataAdapter.Update($table.Result.Tables[0].Select($null, $null, [System.Data.DataViewRowState]::Deleted))
            } Catch {
                Write-Warning "Handled an exception in the delete process: $($_.Exception.Message)"
            }
            
            Try { # Next process updates.
                $table.SqlDataAdapter.Update($table.Result.Tables[0].Select($null, $null, [System.Data.DataViewRowState]::ModifiedCurrent))
            } Catch {
                Write-Warning "Handled an exception in the delete process: $($_.Exception.Message)"
            }
            
            Try { # Finally, process inserts.
                $table.SqlDataAdapter.Update($table.Result.Tables[0].Select($null, $null, [System.Data.DataViewRowState]::Added))
                $table.Result.Tables[0].AcceptChanges()
            } Catch {
                Write-Warning "Handled an exception in the delete process: $($_.Exception.Message)"
            }
        } Catch {
            return $(Write-host ($_ | Out-String) -ForegroundColor Red) 
        } Finally {
            $table.Result.AcceptChanges()
            $This.CloseConnection()
        }

        If ($This.DisplayResults) {
            Return $table.Result.Tables[0]
        } Else {
            Return $null
        }
    }

    # Method to Retrieve Table Schema
    [Object] GetDBTableSchema([String]$TableName) {
        $SqlQuery = "SELECT * FROM [$($This.Database)].INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$TableName';"
        # $SqlQuery = "SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE FROM [$($This.Database)].INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '$TableName';"
        Return ($This.ExecuteQuery($SqlQuery))
    }

    # Method to Retrieve Table Indexes from SQL v17 or higher using STRING_AGG
    [Object] GetDBTableIndexesV17([String]$TableName) {
        $query = "SELECT i.name AS INDEX_NAME,
                        STRING_AGG(c.name, ', ') AS COLUMN_NAMES,
                        i.is_unique AS IS_UNIQUE
                FROM sys.indexes AS i
                JOIN sys.index_columns AS ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
                JOIN sys.columns AS c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
                WHERE i.object_id = OBJECT_ID('dbo.$TableName')
                GROUP BY i.name, i.is_unique"

        $adapter = New-Object System.Data.SqlClient.SqlDataAdapter($query, $This.ConnectionString)
        $indexTable = New-Object System.Data.DataTable
        $adapter.Fill($indexTable)
        return $indexTable
    }

    # Method to Retrieve Table Indexes Without STRING_AGG
    [Object] GetDBTableIndexes([String]$TableName) {
        $SqlQuery = "SELECT i.name AS INDEX_NAME,
                        STUFF((SELECT ', ' + c.name
                                FROM [$($This.Database)].sys.index_columns AS ic
                                JOIN [$($This.Database)].sys.columns AS c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
                                WHERE ic.object_id = i.object_id AND ic.index_id = i.index_id
                                FOR XML PATH('')), 1, 2, '') AS COLUMN_NAMES,
                        i.is_unique AS IS_UNIQUE
                FROM [$($This.Database)].sys.indexes AS i
                WHERE i.object_id = OBJECT_ID('dbo.$TableName')
                GROUP BY i.name, i.is_unique, i.object_id, i.index_id"

        Return ($This.ExecuteQuery($SqlQuery))
    }

    # Method to Generate the CREATE TABLE statement
    [Object] GetCreateBasicDLL([String]$TableName) {
        $schemaTable = $This.GetDBTableSchema($TableName)
        $createTableStatement = "USE [$($This.Database)]" + [Environment]::NewLine +
            "GO" + [Environment]::NewLine + [Environment]::NewLine +
            "CREATE TABLE [$TableName] (" + [Environment]::NewLine
        foreach ($column in $schemaTable.Rows) {
            $columnName = $column.COLUMN_NAME
            $dataType = $column.DATA_TYPE
            $maxLength = if ($column.CHARACTER_MAXIMUM_LENGTH -ne -1) { "($($column.CHARACTER_MAXIMUM_LENGTH))" } else { "" }
            $nullable = if ($column.IS_NULLABLE -eq "YES") { "NULL" } else { "NOT NULL" }
            $createTableStatement += " [$columnName] $dataType$maxLength $nullable," + [Environment]::NewLine
        }
        # Remove the last comma and add closing parenthesis
        $createTableStatement = $createTableStatement.TrimEnd(",`r`n".ToCharArray()) + [Environment]::NewLine + ");"
        Return ($createTableStatement)
    }

    # Method to Generate the CREATE TABLE statement
    [Object] GetCreateDDL([String]$TableName) {
        $schemaTable = $This.GetDBTableSchema($TableName)
        $indexTable = $This.GetDBTableIndexes($TableName)

        $createTableStatement = "USE [$($This.Database)]" + [Environment]::NewLine +
                                "GO" + [Environment]::NewLine + [Environment]::NewLine +
                                "/****** Object: Table [dbo].[$TableName] Script Date: $(Get-Date -Format 'MM/dd/yyyy hh:mm:ss tt') ******/" + [Environment]::NewLine +
                                "SET ANSI_NULLS ON" + [Environment]::NewLine +
                                "GO" + [Environment]::NewLine + [Environment]::NewLine +
                                "SET QUOTED_IDENTIFIER ON" + [Environment]::NewLine +
                                "GO" + [Environment]::NewLine + [Environment]::NewLine +
                                "CREATE TABLE [dbo].[$TableName] (" + [Environment]::NewLine

        foreach ($column in $schemaTable.Rows) {
            $columnName = $column.COLUMN_NAME
            $dataType = $column.DATA_TYPE
            $maxLength = if ($column.CHARACTER_MAXIMUM_LENGTH -ne -1) { "($($column.CHARACTER_MAXIMUM_LENGTH))" } else { "" }
            $nullable = if ($column.IS_NULLABLE -eq "YES") { "NULL" } else { "NOT NULL" }
            $identity = if ($column.COLUMN_NAME -eq "Id") { "IDENTITY (1, 1)" } else { "" }
            $createTableStatement += " [$columnName] $dataType$maxLength $identity $nullable," + [Environment]::NewLine
        }

        # Remove the last comma and add closing parenthesis
        $createTableStatement = $createTableStatement.TrimEnd(",`r`n".ToCharArray()) + [Environment]::NewLine + ");" + [Environment]::NewLine + [Environment]::NewLine
        # $createTableStatement = $createTableStatement.TrimEnd(",`r`n".ToCharArray()) + [Environment]::NewLine + "), PRIMARY KEY CLUSTERED ([Id] ASC);" + [Environment]::NewLine + [Environment]::NewLine

        # Add index creation statements, excluding the primary key index
        foreach ($index in $indexTable.Rows) {
            if ($index.INDEX_NAME -notlike 'PK__*') {
                $indexName = $index.INDEX_NAME
                $columnNames = $index.COLUMN_NAMES
                $unique = if ($index.IS_UNIQUE -eq 1) { "UNIQUE " } else { "" }
                $createTableStatement += "GO" + [Environment]::NewLine +
                                        "CREATE $($unique)NONCLUSTERED INDEX [$indexName]" + [Environment]::NewLine +
                                        " ON [dbo].[$TableName]([$columnNames] ASC);" + [Environment]::NewLine
            }
        }
        return $createTableStatement
    }

    # Method to Parse SQL Statements to extract the Schema and TableName. Support Multi-line Select From statements
    [Object] ParseSQLQuery([String]$Query) {
        $queryPattern = '([\s\t]*FROM[\s\t\r\n](?<schemaTable>[A-Za-z0-9_.\[\]]+).*$)|((?s)[\s\t]*FROM[\s\t\r\n]+(?<schemaTable>[A-Za-z0-9_. \[\] ]+).*$)'

        If ($Query -like '*\*' -and $Query -like '*.MDF*') {
            $queryPattern = '([\s\t]*FROM[\s\t\r\n](?<databaseName>[\[]?[A-Za-z0-9_\:\\]+[\.MDF]{4}[\]]?)[.]{1}(?<schemaTable>[A-Za-z0-9_.\[\]]+).*$)'
        } Else {
            $queryPattern = '([\s\t]*FROM[\s\t\r\n](?<schemaTable>[A-Za-z0-9_.\[\]]+).*$)|((?s)[\s\t]*FROM[\s\t\r\n]+(?<schemaTable>[A-Za-z0-9_. \[\] ]+).*$)'
        }
        $schemaTablePattern = '([\[]?(?<tableName>[A-Za-z0-9_]+)[\.\[\] ]?$)|([\[]?(?<schema>[A-Za-z0-9_]+)[\.\[\]]*(?<tableName>[A-Za-z0-9_]+)[\.\[\] ]?$)'
        $retResult = ([PSCustomObject]@{
            dataBase = [string]::Empty
            schemaTable = [string]::Empty
            Schema = [string]::Empty
            TableName = [string]::Empty
        })
        If ($Query -match $queryPattern) {
            $retResult.schemaTable = $Matches.schemaTable
            $retResult.dataBase = $Matches.databaseName
            If ($retResult.schemaTable -match $schemaTablePattern) {
                $retResult.schema = $Matches.schema
                $retResult.TableName = $Matches.tableName
            }
        }
        Return ($retResult)
    }

}