Get-SQL.ps1

if (-not $Global:DbSessions ) { $Global:DbSessions = @{}  }

function Get-SQL {
    <#
      .Synopsis
        Queries an ODBC, SQLite or SQL Server database
      .Description
        Get-SQL queries SQL databases using either ODBC, the ADO driver for SQLite or the native SQL-Server client.
        Connections to databases are kept open and reused to avoid the need to make connections for every query,
        but the first time the command is run it needs a connection string; this come from $DefaultDBConnection.
        (e.g. set in your Profile) rather than being passed as a parameter: if it is set you can run
        sql "Select * From Customers"
        without any other setup, PowerShell will assume "sql" means "GET-SQL" if there is no other command named SQL.
 
        Get-SQL -Connection allows a connection to be specified explicitly; -MsSQLserver forces the use of
        the native SQL Server driver, -lite allows the file name of a SQLite Database to be used
        and -Excel or -Access allow a file name to be used without converting it into an ODBC connection string.
 
        Multiple named sessions may be open concurrently, and the global variable $DbSessions holds objects
        for each until Get-SQL is run with -Close. Note that you can run a query and make and/or close
        the connection in a single command. However, if you pipe the output into a command like
        Select-Object -First 2 then when Get-SQL is stopped by the downstream command it is unable to
        close the connection.
 
        Get-Sql will also build simple queries, for example
        Get-SQL -Table Authors
        Will run the "Select * from Authors" and a condition can be specified with
        Get-SQL -Table Authors -Where Name -like "*Smith"
        Get-SQL -ShowTables will show the available tables, and Get-SQL -Describe Authors will show the design of the table.
 
        Argument completers fill in names of ODBC connections, databases, tables, and columns where needed.
      .Parameter SQL
        A SQL statement. If other parameters (such as -Table, or -Where) are provided, it becomes the end of the SQL statement.
        If no statement is provided, or none can be built from the other parameters, Get-SQL returns information about the connection.
      .Parameter Connection
        An ODBC connection string or an Access, Excel, or SQLite file name or the name of a SQL Server
        It can be in the form "DSN=LocallyDefinedDSNName;" or
        "Driver={MySQL ODBC 5.1 Driver};SERVER=192.168.1.234;PORT=3306;DATABASE=xstreamline;UID=johnDoe;PWD=password;"
        A default connection string can be set in in $DBConnection so that you can just run "Get-SQL " «SQL Statement» ".
      .Parameter Excel
        Specifies that the string in -Connection is an Excel file path to be converted into a connection string.
      .Parameter Access
        Specifies that the string in -Connection is an Access file path to be converted into a connection string.
      .Parameter Lite
        Specifies the SQLite driver should be used and the string in -Connection may be the path to a SQLite file.
      .Parameter MsSQLserver
        Specifies the SQL Native client should be used and string in -Connection may be the name of a SQL Server.
      .Parameter Session
        Allows a database connection to be Identified by name: this sets the name used in the global variable $DBSessions.
        In addition, an alias is added: for example, if the session is named "F1" you can use the command F1 in place of Get-SQL -Session F1
      .Parameter ForceNew
        If specified, makes a new connection for the default or named session.
        If a connection is already established, -ForceNew is required to change the connection string.
      .Parameter ChangeDB
        For SQL server and ODBC sources which support it (like MySQL) switches to a different database at the same server.
      .Parameter Close
        Closes a database connection. Note this is run in the "end" phase of the command. If Get-SQL is stopped by another command
        in the pipeline (for example Select-object -first ) then it may not close the connection, so although this command can be
        combined with a select query, care is needed to ensure it is not defeated by another command in the same pipeline.
      .Parameter Table
        Specifies a table to select or delete from or to update.
      .Parameter Where
        If specified, applies a SQL WHERE condition to the selected table. -Where specifies the field and the text in -SQL supplies the condition.
      .Parameter GT
        Used with -Where specifies the > operator should be used, with the operand for the condition found in -SQL.
      .Parameter GE
        Used with -Where specifies the >= operator should be used, with the operand for the condition found in -SQL.
      .Parameter EQ
        Used with -Where specifies the = operator should be used, with the operand for the condition found in -SQL.
      .Parameter NE
        Used with -Where specifies the <> operator should be used, with the operand for the condition found in -SQL.
      .Parameter LE
        Used with -Where specifies the <= operator should be used, with the operand for the condition found in -SQL.
      .Parameter LT
        Used with -Where specifies the < operator should be used, with the operand for the condition found in -SQL.
      .Parameter Like
        Used with -Where specifies the Like operator should be used, with the operand for the condition found in -SQL. "*" in -SQL will be replaced with "%".
      .Parameter NotLike
        Used with -Where specifies the Not Like operator should be used, with the operand for the condition found in -SQL. "*" in -SQL will be replaced with "%".
      .Parameter Select
        If Select is omitted, -Table TableName will result in "SELECT * FROM TableName".
        Select specifies field-names (or other text) to use in place of "*".
      .Parameter Distinct
        Specifies that "SELECT DISTINCT ..." should be used in place of "SELECT ...".
      .Parameter OrderBy
        Specifies fields to be used in a SQL ORDER BY clause added at the end of the query.
      .Parameter Delete
        If specified, changes the query from a SELECT to a DELETE. This allows a query to be tested as a SELECT before adding -Delete to the command.
        -Delete requires a WHERE clause and not all ODBC drivers support deletion.
      .Parameter Set
        If specified, changes the query from a Select to a Update -Set Specifies the field(s) to be updated.
        -Set requires a WHERE clause.
      .Parameter Values
        If -Set is specified, -Values contains the new value(s) for the fields being updated.
      .Parameter Insert
        Specifies a table to insert into. The SQL parameter should contain a hash table or PSObject which holding the data to be inserted.
      .Parameter DateFormat
        Allows the format applied to Dates to be inserted to be changed if a service requires does not follow standard conventions.
      .Parameter GridView
        If specified, sends the output to gridview instead of the PowerShell console.
      .Parameter GroupBy
        If specified, adds a group by clause to a select query; in this case the SELECT clause needs to contain fields suitable for grouping.
      .Parameter Describe
        Returns a description of the specified table - note that some ODBC providers don't support this.
      .Parameter ShowTables
        If specified, returns a list of tables in the current database - note that some ODBC providers don't support this.
      .Parameter Paste
        If specified, takes an SQL statement from the clipboard.
        Line breaks and any text before SELECT , UPDATE or DELETE will be removed.
      .Parameter Quiet
        If specified, suppresses printing of the console message saying how many rows were returned.
      .Parameter OutputVariable
         Behaves like the common parameters errorVariable, warningvariable etc.to pass back a table object instead of an array of data rows.
      .Example
        Get-SQL -MsSQLserver -Connection "server=lync3\rtclocal;database=rtcdyn; trusted_connection=true;" -Session Lync
        Creates a new session named "LYNC" to the rtcdyn database on the Rtclocal SQL instance on server Lync
      .Example
        Get-SQL -Session LR -Connection "DSN=LR" -Quiet -SQL $SQL
        Runs the SQL in $SQL - if the Session LR already exists it will be used, otherwise it will be created to the ODBC source "LR"
        Note that a script should always name a its session(s), something else may already have set the defualt session
      .Example
        Get-Sql -showtables *dataitem
        Gives a list of tables on the default connection that end with "dataitem"
      .Example
        Get-SQL -Session f1 -Excel -Connection C:\Users\James\OneDrive\Public\F1\f1Results.xlsx -showtables
        Creates a new connection named F1 to an Excel file, and shows the tables available.
      .Example
        f1 -Insert "[RACES]" @{RaceName = $raceName, RaceDate = $racedate.ToString("yyyy-MM-dd") }
        Uses the automatically created alias "f1" which was created in the previous example to insert a row of data into the "Races" Table
      .Example
        Get-SQL -Session F1 -Table "[races]" -Set "[poleDriver]" -Values $PoleDriver -SQL "WHERE RaceDate = $rd" -Confirm:$false
        Updates the races table in the "F1" session, setting the value in the column "PoleDriver" to the contents of
        the variable $PoleDriver, in those rows where the RaceDate = $RD. This time the session is explicitly specified
        (using aliases is OK at the command line but not in scripts especially if the alias is created by a command run in the script)
        Changes normally prompt the user to confirm but here -Confirm:$false prevents it
      .Example
        "CREATE USER 'johndoe' IDENTIFIED BY 'password'" , "GRANT ALL PRIVILEGES ON *.* TO 'johndoe'@'%' WITH grant option" | Get-SQL
        Pipes two commands into the default connection, giving a new mySql user full access to all tables in all databases
      .Example
        Get-Sql -paste -gridview
        Runs the query currently in the clipboard against the default existing and outputs to the Gridview
      .Example
        SQL -table catalog_dataitem -select dataStatus -distinct -orderBy dataStatus -gridView
        Builds the query " SELECT DISTINCT dataStatus FROM catalog_dataitem ORDER BY dataStatus",
        runs it against the default existing connection and displays the results in a grid.
      .Example
        [void](Get-sql $sql -OutputVariable Table)
        PowerShell unpacks Datatable objects into rows; so anything which needs a DataTable object cannot get it with
        $table = Get-Sql $sql
        because $table will contain an Array of DataRow objects, not a single DataTable.
        To get round this Get-SQL has -OutputVariable which behaves like the common parameters errorVariable, warningvariable etc.
        (using the Name of the variable 'Table' not its value '$table' as the parameter value)
        After running the command, the variable in the scope where the command is run contains the DataTable object.
        Usually the datarow objects will not be required, so the output can be cast to a void or piped to Out-Null.
    #>

    [CmdletBinding(DefaultParameterSetName='Describe',SupportsShouldProcess=$true,ConfirmImpact="High")]
    param   (
        [parameter(Position=0, ValueFromPipeLine=$true)]
        $SQL,
        [parameter(Position=1)][ValidateNotNullOrEmpty()]
        [string]$Connection  = $global:DefaultDBConnection ,
        [ValidateNotNullOrEmpty()]
        [string]$Session = "Default",
        [parameter(Position=2)]
        [alias('Use')]
        [string]$ChangeDB,
        [alias('Renew')]
        [switch]$ForceNew  ,
        [parameter(ParameterSetName="Paste")]
        [parameter(ParameterSetName="Describe")]
        [parameter(ParameterSetName="Select")]
        [parameter(ParameterSetName="SelectWhere")]
        [alias('g')][switch]$GridView,
        [parameter(ParameterSetName="Describe")]
        [alias('d')][string]$Describe,
        [parameter(ParameterSetName="ShowTables" , Mandatory=$true)]
        [switch]$ShowTables,
        [parameter(ParameterSetName="Paste"      , Mandatory=$true)]
        [switch]$Paste,
        [parameter(ParameterSetName="UpdateWhere", Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [parameter(ParameterSetName="DeleteWhere", Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [parameter(ParameterSetName="SelectWhere", Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [parameter(ParameterSetName="Update"     , Mandatory=$false)]
        [parameter(ParameterSetName="Delete"     , Mandatory=$false)]
        [parameter(ParameterSetName="Select"     , Mandatory=$false)]
        [alias('from','update')][string]$Table,
        #region Parameters for queries with a WHERE clause
        [parameter(ParameterSetName="UpdateWhere", Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [parameter(ParameterSetName="DeleteWhere", Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [parameter(ParameterSetName="SelectWhere", Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [string]$Where,
        [parameter(ParameterSetName="UpdateWhere")]
        [parameter(ParameterSetName="DeleteWhere")]
        [parameter(ParameterSetName="SelectWhere")]
        [switch]$GT,
        [parameter(ParameterSetName="UpdateWhere")]
        [parameter(ParameterSetName="DeleteWhere")]
        [parameter(ParameterSetName="SelectWhere")]
        [switch]$GE,
        [parameter(ParameterSetName="UpdateWhere")]
        [parameter(ParameterSetName="DeleteWhere")]
        [parameter(ParameterSetName="SelectWhere")]
        [switch]$EQ,
        [parameter(ParameterSetName="UpdateWhere")]
        [parameter(ParameterSetName="DeleteWhere")]
        [parameter(ParameterSetName="SelectWhere")]
        [switch]$NE,
        [parameter(ParameterSetName="UpdateWhere")]
        [parameter(ParameterSetName="DeleteWhere")]
        [parameter(ParameterSetName="SelectWhere")]
        [switch]$LE,
        [parameter(ParameterSetName="UpdateWhere")]
        [parameter(ParameterSetName="DeleteWhere")]
        [parameter(ParameterSetName="SelectWhere")]
        [switch]$LT,
        [parameter(ParameterSetName="UpdateWhere")]
        [parameter(ParameterSetName="DeleteWhere")]
        [parameter(ParameterSetName="SelectWhere")]
        [switch]$Like,
        [parameter(ParameterSetName="UpdateWhere")]
        [parameter(ParameterSetName="DeleteWhere")]
        [parameter(ParameterSetName="SelectWhere")]
        [switch]$NotLike,
        #endregion
        #Parameters for SELECT Queries
        [parameter(ParameterSetName="Select")]
        [parameter(ParameterSetName="SelectWhere")]
        [alias('Property')][string[]]$Select,
        [parameter(ParameterSetName="Select")]
        [parameter(ParameterSetName="SelectWhere")]
        [switch]$Distinct,
        [parameter(ParameterSetName="Select")]
        [parameter(ParameterSetName="SelectWhere")]
        [string[]]$OrderBy,
        [parameter(ParameterSetName="Select")]
        [parameter(ParameterSetName="SelectWhere")]
        [String[]]$GroupBy,
        #Parameters for Delete queries
        [parameter(ParameterSetName="DeleteWhere", Mandatory=$true)]
        [parameter(ParameterSetName="Delete"     , Mandatory=$true)]
        [switch]$Delete,
        #Parameters for Update queries
        [parameter(ParameterSetName="UpdateWhere", Mandatory=$true)]
        [parameter(ParameterSetName="Update"     , Mandatory=$true)]
        [string[]]$Set,
        [parameter(ParameterSetName="UpdateWhere", Mandatory=$true,Position=1)]
        [parameter(ParameterSetName="Update"     , Mandatory=$true,Position=1)]
        [Object[]]$Values,
        #Parameters for INSERT Queries
        [parameter(ParameterSetName="Insert"     , Mandatory=$true)]
        [alias('into')][string]$Insert,
        [parameter(ParameterSetName="Insert")]
        [parameter(ParameterSetName="Update")]
        [parameter(ParameterSetName="UpdateWhere")]
        [parameter(ParameterSetName="DeleteWhere")]
        [parameter(ParameterSetName="SelectWhere")]
        [String]$DateFormat   = "'\''yyyy'-'MM'-'dd HH':'mm':'ss'\''",
        [parameter(ParameterSetName="Paste")]
        [parameter(ParameterSetName="Describe")]
        [parameter(ParameterSetName="Select")]
        [parameter(ParameterSetName="SelectWhere")]
        [switch]$Quiet,
        [switch]$MsSQLserver,
        [switch]$Lite,
        [switch]$Access,
        [switch]$Excel,
        [String]$OutputVariable,
        [switch]$Close
    )
    begin   {
        #Prepare session, if needed, and leave it in the global variable DBSessions - a hash table with Name and connection object
        #If the function was invoked with an Alias of "DB" and there is session named "DB" switch to using that session
        if   (("Default" -eq $Session) -and $Global:DbSessions[$MyInvocation.InvocationName]) {$Session = $MyInvocation.InvocationName}

        #if the session doesn't exist or we're told to force a new session, then create and open a session
        if   (  ($ForceNew)  -or  (  -not   $Global:DbSessions[$session]) -and -not $Close) {
            if     ($Lite -and $PSVersionTable.PSVersion.Major -gt 5 -and $IsMacOS ) {
                Add-Type -path (Join-Path $PSScriptRoot "linux-x64\System.Data.SQLite.dll" )
            }
            elseif ($Lite -and $PSVersionTable.PSVersion.Major -gt 5 -and $linux )   {
                Add-Type -path (Join-Path $PSScriptRoot "linux-x64\System.Data.SQLite.dll" )
            }
            elseif ($lite -and -not [System.Environment]::Is64BitProcess) {
                Add-Type -path (Join-Path $PSScriptRoot "win-x86\System.Data.SQLite.dll" )
            }
            elseif ($lite ) {
                Add-Type -path (Join-Path $PSScriptRoot "win-x64\System.Data.SQLite.dll" )
            }
            #Catch -force to refresh instead of replace the current connection (e.g. Server has timed out )
            if (($ForceNew)      -and       $Global:DbSessions[$session] -and -not $PSBoundParameters.ContainsKey('Connection'))  {
                if     ($Global:DbSessions[$session].GetType().name -eq "SqlConnection"    ) {$MsSQLserver = $true}
                elseif ($Global:DbSessions[$session].GetType().name -eq "SQLiteConnection" ) {$Lite = $true}
            }
            #If -MSSQLServer switch is used assume connection is a server if there is no = sign in the connection string
            if  ($MsSQLserver    -and       $Connection                  -and  $connection -notmatch "=") {
                $Connection = "server=$Connection;trusted_connection=true;timeout=60"
            }
            #If -Lite switch is used assume connection is a file if there is no = sign in the connection string, check it exists and build the connection string
            if  ($Lite           -and       $Connection                  -and  $connection -notmatch "=") {
              if (Test-Path -Path  $Connection)  {
                     $Connection  = "Data Source="+
                                    (Resolve-Path -Path $Connection -ErrorAction SilentlyContinue).Path + ";"
              }
              else { Write-Warning -Message "Can't create database connection: could not find $Connection" ; return}
            }
            #If the -Excel or Access switches are used, then the connection parameter is the path to a file, so check it exists and build the connection string
            if  ($Excel)                  {
              if (Test-Path -Path  $Connection)  {
                  $Connection  = "Driver={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};DriverId=790;ReadOnly=0;Dbq=" +
                                 (Resolve-Path -Path $Connection -ErrorAction SilentlyContinue).Path + ";"
              }
              else { Write-Warning -Message "Can't create database connection: could not find $Connection" ; return}
            }
            if  ($Access)                 {
              if (Test-Path -Path  $Connection)  {
                     $Connection  = "Driver={Microsoft Access Driver (*.mdb, *.accdb)};Dbq="+
                                    (Resolve-Path -Path $Connection -ErrorAction SilentlyContinue).Path + ";"
              }
              else { Write-Warning -Message "Can't create database connection: could not find $Connection" ; return}
            }
            if (-not $Connection)         { Write-Warning -Message "A connection was needed but -Connection was not provided."; break}
            Write-Verbose -Message "Connection String is '$connection'"
            #Use different types for SQL server, SQLite and ODBC. They (and the logic) are almost interchangable.
            if  ($MsSQLserver)            { $Global:DbSessions[$Session] = New-Object -TypeName System.Data.SqlClient.SqlConnection -ArgumentList $Connection }
            elseif ($Lite)                { $Global:DbSessions[$Session] = New-Object -TypeName System.Data.SQLite.SQLiteConnection -ArgumentList $Connection }
            else                          { $Global:DbSessions[$Session] = New-Object -TypeName System.Data.Odbc.OdbcConnection     -ArgumentList $Connection
                                            $Global:DbSessions[$Session].ConnectionTimeout = 30
            }
            #Open our connection. NB, if 32 bit office is installed Excel, Access ETC have 32 bit ODBC drivers which need 32 bit Powershell not 64 bit.
            try                           { $Global:DbSessions[$Session].open() }
            catch                         { Write-Warning -Message "Error opening connection to '$Connection'"
                                            if (($Access -or $Excel) -and [System.Environment]::Is64BitProcess) {
                                                Write-Warning -Message  "This is 64-bit PowerShell, If Office is 32-bit you need to use 32 bit-PowerShell"}
                                            $Global:DbSessions[$Session] = $null
                                            break
            }
            #Create an alias which matches the connection name.
            if  ("Default" -eq $Session)  { $Global:DefaultDBConnection = $Connection }
            else                          { New-Alias -Name $Session -Value Get-SQL -Scope Global -Force}
        }
        if      ($ChangeDB)               { $Global:DbSessions[$Session].ChangeDatabase($ChangeDB) } #This method to change DB won't work with every provider
        if   (  ($Paste) -and (Get-Command -Name 'Get-Clipboard' -ErrorAction SilentlyContinue))  {
            #You could use [windows.clipboard]::GetText() - be warned this may not work in the older releases of the standard shell
            #For older versions of PowerShell I have a Get-Clipboard function which wraps this
            $SQL = (Get-Clipboard) -replace "^.*?(?=select|update|delete)","" -replace "[\n|\r]+"," "
        }
    }
    process {
        #If $table is specified make sure $SQL isn't empty otherwise we won't get to Select * from $Table; also make sure conditions allow for it to be zero!
        if   ($Table -and $null -eq $SQL) { $SQL = " "}
        if   ($SQL.SQL)                   { $SQL = $SQL.SQL}
        if                    ($Describe) { #support -Describe [tablename] to descibe a table
            if ($Global:DbSessions[$Session].driver -match "SQORA" ) { #Oracle is special ...
                Get-SQL -Session $Session -Quiet -SQL  "select COLUMN_NAME, data_type as TYPE_NAME, data_length AS COLUMN_SIZE " +
                                                        " from user_tab_cols where table_name = '$Describe' order by COLUMN_NAME"
            }
            else  { #Remove any [] around the table name - because that's how .GetSchema() works ...
                $Describe = $Describe -replace "\[(.*)\]",'$1'
                # For some drivers .GetSchema() can get the columns for a single table. But the Excel driver can't, so get all columns and filter.
                if     ($Global:DbSessions[$Session].Driver -match "ACEODBC.DLL" ) {
                        $columns = $Global:DbSessions[$Session].GetSchema("Columns") | Where-Object {$_.TABLE_NAME -eq $Describe }
                }
                elseif ($Global:DbSessions[$Session].gettype().name -match "SqlConnection" ) {#SQL server uses slightly differnet syntax
                        $columns = $Global:DbSessions[$Session].GetSchema("Columns", @("%","%",$Describe,"%"))
                }
                else {  $columns = $Global:DbSessions[$Session].GetSchema("Columns", @("","",$Describe)) }
                if ($GridView) {$columns | Out-GridView -Title "Table $Describe"}
                else           {$columns | Select-Object -Property @{n="COLUMN_NAME";e={if ($_.Column_Name -match "\W") {"[$($_.Column_Name)]"} else {$_.column_Name} }},
                                                                TYPE_NAME, COLUMN_SIZE, IS_NULLABLE
                }
            }
        }
        elseif              ($Showtables) { #ODBC method to get tables won't work with every provider, but nor will executing "show tables". $SQL param becomes a filter
            if   ($Global:DbSessions[$Session].driver -match "SQORA" ) {#Oracle is special ...
                (Get-SQL  -Session $Session -Quiet -SQL  "select OBJECT_NAME from user_objects where object_type IN ('VIEW','TABLE'); ").object_name |
                    Where-Object {$_ -like "$SQL*"}
            }
            else {$Global:DbSessions[$Session].GetSchema("Tables") | Where-Object {$Global:DbSessions[$Session].DataSource -ne "Access" -or $_.TABLE_TYPE -ne "SYSTEM TABLE"} |
                ForEach-Object {
                    if     ($_.TABLE_NAME -like "$SQL*" -and $_.TABLE_NAME -match "\W") {"[" + $_.TABLE_NAME + "]"}
                    elseif ($_.TABLE_NAME -like "$SQL*")                                {      $_.TABLE_NAME      }
                } | Sort-Object
            }
        }
        elseif           ($null -ne $SQL) { #$SQL holds any SQL which we can't (or don't want to( assemble from the cmdline, a whole statement or final clause
          ForEach        ($s     in $SQL) { #More than one statement/clause can be passed
            if ($Delete -or $Set -and -not $Table) { Write-Warning -Message "You must specifiy a table and where condition to use -Delete or -Set" ; return }
            if                            ($Table) { #If $Table was specified, build a Select, Delete or Update query
                #Support -table [tablename] -Where [ColumnName] -eq 99 and similar syntax.
                # -eq -ne and other operators are *switches*. The operand for = (etc.) is in $SQL so only Operator is allowed. Too complex to enforce this in Param() block!
                $opCount        =  (($Like, $EQ, $NE , $LT , $GT , $GE, $LE, $NotLike) -eq $true).Count
                #Can't have multiple operators, and operator requires -Where to be specified and a value in -SQL (-SQL usually implied in cmdline)
                if ((($opCount) -gt 1) -or  (($opCount -eq 1) -and -not $Where ) -or ($Where -and  " " -eq  $s )) {
                    Write-Warning -Message  "You can't specify a where condition like that"
                    return
                }
                if  (($opCount) -eq 1) { #If we have an operator, column and value in $s turn $s into the condition (add the column name after)
                    #if the operand for -eq etc is a date format it for SQL
                    if ($s -is [datetime])  {
                        $s = $s.tostring($DateFormat)  #Default format has "'" this works for Excel inserts and SQL server.
                        if ($Global:DbSessions[$Session].Driver -eq "ACEODBC.DLL") {       #For Excel where needs # not quotes as date markers
                            $s = $s -replace "'","#"
                        }
                    } #if the operand for -eq etc is not a number or isn't wrrapped in quotes. Wrap it in quotes and double up the ' character
                    elseif (($s -notmatch "^\d+\.?\d*$") -and ($s -notmatch "^'.*'$"))
                                      {$s = "'" + ($s -replace "(?<!')'(?!')","''") +"'"  }
                    if          ($EQ) {$s =   " = $s "            }
                    if          ($NE) {$s =   " <> $s "            }
                    if          ($GE) {$s =   " >= $s "            }
                    if          ($LE) {$s =   " <= $s "            }
                    if          ($GT) {$s =   " > $s "            }
                    if          ($LT) {$s =   " < $s "            }
                    if       (($Like)  -or ($NotLike) ) {   #for the like operators replace * wildcard with SQL % wildcard
                                       $s = $s -replace "\*","%"   }
                    if        ($Like) {$s =     " like $s "        }
                    if     ($NotLike) {$s = " not like $s "        }
                    #At the end of this $s holds the condition but not the column name
               }
                if           ($Delete) { #Support Delete queries -Table [tableName] -Delete -where [Column] -eq [Value]
                    #A careless -Delete could wipe out a table - so insist on either -where [columnName] and a condition, or "Where blah blah" in $SQL
                  if ((($Where) -and $s) -or ($s -match "where\s+\w+")) {
                    if ($Where)     {$s = "DELETE FROM $Table WHERE $Where " + $S }
                    else            {$s = "DELETE FROM $Table "              + $S }
                  }
                  else {Write-Warning -Message "You must specifiy a where condition to use -Delete"; return }
               }
                elseif          ($Set) {
                  #Support update ... set queries -Table [tableName] -Set [Columns] -Values [values] -Where [Column] -EQ [Value]
                  #Don't allow set to modify all the rows (same logic as Delete)
                  if ( (  $Where  -and $s) -or ($s -match "where\s+\w+")) {
                    #We have a list of columns in Set and values for them need the same number of each - then build the set clause, wrapping text values in ''
                    if ($Set.Count  -ne  $Values.Count)          {Write-Warning -Message "Must have the same number of columns to set as values to set them to"; return }
                    $setList = ""
                    for  ($i = 0; $i  -lt  $set.count; $i++) {
                        if  (   $Values[$i] -is [datetime])  {
                            if ($Global:DbSessions[$Session].gettype().name -match "SQLiteConnection") {
                                    $Vi = [int]($Values[$i].Subtract([datetime]::UnixEpoch).TotalSeconds)
                            }
                            else {  $Vi = $Values[$i].tostring($DateFormat)}     #Default format has "'" this works for Excel, Access and SQL server.
                            $SetList = $SetList + $Set[$i] + "= " + $vi +" ,"
                        }
                        # Wrap text in ' and escape ' char
                        elseif ($Values[$i] -notmatch "^[\d\.]*$") {$SetList = $SetList + $Set[$i] + "='" + ($Values[$i] -replace "'","''") +"' ," }
                        else                                       {$SetList = $SetList + $Set[$i] + "= " +  $Values[$i] +" ," }
                    }
                    #will have an extra "," at the end.
                    $setList = $setList -replace ",$",""
                    if   ($Where)   {$s = "UPDATE $Table SET $setList WHERE $Where " + $s }
                    else            {$s = "UPDATE $Table SET $setList "              + $s }
                }
                  else                {Write-Warning -Message "You must specifiy a where condition to use -Set"   ; return }
               }
                else                   {#If we're not updating or deleting and -Table was passed we must be selecting ....
                    if   (   $Select) {$SelectClause = ($Select -join ", ") + " FROM $Table " }
                    else              {$SelectClause =                      " * FROM $Table " }
                    if   (    $Where) {$SelectClause = $SelectClause   +      "WHERE $Where " } #note we need to have the "what" part of SQL. but SQL could be @("=10",">73") we'll run 2 queries
                    if   ( $Distinct) {$s = "SELECT DISTINCT "         +  $SelectClause + $s  }
                    else              {$s = "SELECT "                  +  $SelectClause + $s  }
                    if   (  $GroupBy) {$s = $s +    " GROUP BY "       + ($GroupBy -join ", ")}
                    if   (  $OrderBy) {$s = $s +    " ORDER BY "       + ($OrderBy -join ", ")}
                }
            }
            elseif                       ($Insert) {
            #Support -insert [IntoTableName] @{hashtable of fields and values}
                if     ($s -is [Hashtable]) {$index = $s.keys}
                elseif ($s -is [psobject] ) {$index = (Get-Member -InputObject $s -MemberType NoteProperty).Name }
                else                        { Write-Warning -Message "Can't build an Insert statement from $s. Pass a hashtable or a PSObject" ; return}
                $fieldsPart     = " "
                $valuesPart     = " "
                foreach ($name in $index) {
                    $fieldsPart = $fieldsPart  + $name + " , "
                    $v          = $s.$name
                    if     ($Global:DbSessions[$Session].gettype().name -match "SQLiteConnection") {
                        if ($v -is [datetime] )  {$v = [int]($v.Subtract([datetime]::UnixEpoch).TotalSeconds)}
                        if ($v -is [Boolean]  )  {$v = [int]$v}
                    }
                    #$DateFormat defaults to the standard date format which SQL dialects support, but it can be overridden for special cases
                    if     ($v -is [datetime] )  {$valuesPart = $valuesPart +         $v.tostring($DateFormat) + " , " }
                    elseif ($v -is [int]    -or
                            $v -is [float]  -or
                            $v -is [boolean]  )  {$valuesPart = $valuesPart +         $v.tostring()         +    " , " }
                    elseif ($v -match "^\d+$" )  {$valuesPart = $valuesPart +         $v.tostring()         +    " , " }
                    else                         {$valuesPart = $valuesPart +  "'" + ($v -replace "'","''") +   "' , " }
                }
                $s  = ("INSERT INTO {0} ({1}) VALUES ({2})" -f $Insert,($fieldsPart -replace ",\s*$",""),($valuesPart -replace ",\s*$",""))
                $s = $s -replace ",\s*,",", null ," -replace "(?<=[(,])\s*''\s*(?=[),])"," null " -replace ",\s*\)",", null)"
            }
            Write-Verbose -Message $s
            #Choose suitable data adapter object based on session type.
            if     ($Global:DbSessions[$Session].gettype().name -match "SqlConnection" )  {
               $da = New-Object    -TypeName System.Data.SqlClient.SqlDataAdapter -ArgumentList (
                        New-Object -TypeName System.Data.SqlClient.SqlCommand     -ArgumentList $s,$Global:DbSessions[$Session] )
            }
            elseif ($Global:DbSessions[$Session].gettype().name -match "SQLiteConnection" ) {
               $da = New-Object    -TypeName System.Data.SQLite.SQLiteDataAdapter -ArgumentList (
                        New-Object -TypeName System.Data.SQLite.SQLiteCommand     -ArgumentList $s,$Global:DbSessions[$Session] )
            }
            else  {
               $da = New-Object    -TypeName System.Data.Odbc.OdbcDataAdapter     -ArgumentList (
                        New-Object -TypeName System.Data.Odbc.OdbcCommand         -ArgumentList $s,$Global:DbSessions[$Session])

            }
            $dt       = New-Object -TypeName System.Data.DataTable
            #And finally we get to execute the SQL Statement.
            try  { if ((-not ($Set -or $Delete -or ($Insert -and $ConfirmPreference -ne "high"))) -or ($PSCmdlet.ShouldProcess("$Session database", $s)) ) {
                   $rows  = $da.fill($dt)
                   if (-not ($Quiet -or $Delete -or $Set -or $Insert)) {Write-host -Object ("" + [int]$rows + " row(s) returned")}
            }}
            catch {
               if($S)               { #if we get an error and -SQL was passed show the final SQL statement.
                  $e=$Global:error[0]
                  throw ( New-Object -TypeName "System.Management.Automation.ErrorRecord" `
                                     -ArgumentList (($e.exception.message -replace "^(.*])\s*","`$1`n") + "`n `n>>> $S `n `n" ), $e.FullyQualifiedErrorId ,"ParserError" ,$e.TargetObject)
               }
               else                   { throw }
              }
            if   (($GridView) -and (($PSVersionTable.PSVersion.Major -GE 3)-or ($host.name -match "ISE" )) ) {$dt | Out-GridView -Title $s}
            else  {$dt}
            if ($OutputVariable) {Set-Variable -Scope 2 -Name $OutputVariable -Value $dt -Visibility Public}
            }
        }
        elseif              (-not $Close -and -not $Quiet) { #If $SQL, $table, $describe or $showtimes weren't included either we're opening a new connection, or we're checking or closing an existing one.
            $Global:DbSessions[$Session]
        }
    }
    end     {
        if ($Close -and $Global:DbSessions[$Session]) {
            $Global:DbSessions[$Session].close()
            $Global:DbSessions[$Session].dispose()
            $Global:DbSessions.Remove($Session)
            Remove-Item -Path (Join-Path -Path "Alias:\" -ChildPath $Session) -ErrorAction SilentlyContinue
        }
    }
}

function Hide-GetSQL {
<#
    .Synopsis
        Allows a command line with quote marks to passed into Get-SQL, can be used simply as ¬
    .Example
        ¬ select host,user from mysql.user
        Sends the command "select host,user from mysql.user" to the default ODBC session
 
#>

  Get-Sql -sql ($MyInvocation.line.substring($MyInvocation.OffsetInLine)) | Format-Table -AutoSize
}
Set-Alias -Name ¬ -Value Hide-GetSQL