Function SQL_Services(@Service, @Params)
/***********************************************************************************************************************

    This program is proprietary and is not to be used by or disclosed to others, nor is it to be copied without written
    permission from SRP Computer Solutions, Inc..

    Name        :   SQL_Services

    Description :   Handler program for all module related services.

    Notes       :   The generic parameters should contain all the necessary information to process the services. Often
                    this will be information like the data Record and Key ID.

    Parameters  :
        Service         [in] -- Name of the service being requested
        Param1-10   [in/out] -- Additional request parameter holders
        Response       [out] -- Response to be sent back to the Controller (MCP) or requesting procedure

    History     :   (Date, Initials, Notes)
        10/19/17    dmb     v1.0.0 - Original programmer.
        05/06/20    dmb     v2.0.0 - Added GetDatabases, GetSchemas, and GetTables services.
        05/09/20    dmb     v2.0.0 - Changed GetSQLServer service to GetMappedSQLServer to use the same naming
                                     convention as other similar services.
        05/09/20    dmb     v2.0.0 - Decprecate the GetTableNames service. Use GetTables instead.
        05/09/20    dmb     v2.0.0 - Update GetTableColumns service to support Database and Schema arguments. Add logic
                                     to check the Table argument for other SQL objects.
        05/09/20    dmb     v2.0.0 - Remove all case conversions for SQL table names in all services.
        05/11/20    dmb     v2.0.0 - Update the GetMappedSQLColumnValues and GetMappedOIColumnValues services to support
                                     additional SQL data types and to use SRP_Date/SRP_DateTime/SRP_Time functions
                                     instead of Iconv/Oconv.
        05/11/20    dmb     v2.0.0 - Update GetSQLUpdateStatements service so secondary tables use the correct SQL
                                     syntax.
        05/12/20    djs     v2.0.0 - Update GetServers service to use new SERVERS table. Add CreateNewConnection,
                                     EditConnection, DeleteConnection, and SetConnectionDesc services.
        05/12/20    dmb     v2.0.0 - More updates to the GetSQLInsertStatements and GetSQLUpdateStatements services to
                                     improve handling of AMV mapping.
        05/12/20    dmb     v2.0.0 - Move transaction control out of the WriteDataRowToSQL and DeleteDataRowFromSQL
                                     services into the ExecuteQueries service so error trapping can determine if a
                                     COMMIT or ROLLBACK should be made.
        05/13/20    dmb     v2.0.0 - Update GetConnectionString service to use the SERVERS table.
        05/13/20    dmb     v2.0.0 - Remove deprecated LogConnectionActivity service.
        05/14/20    dmb     v2.0.0 - Update the ExecuteQueries service to ignore query statements that are empty. This
                                     will allow other query statements to be executed in the event of an accidental
                                     empty entry.
        05/14/20    dmb     v2.0.0 - Update the GetMappedSQLColumnValues service return the OI Key value correctly when
                                     working with AMV values greater than 1.
        05/14/20    dmb     v2.0.0 - Update the GetSQLUpdateStatements service to DELETE all secondary SQL table rows
                                     first and then INSERT rows as needed. This is necessary to keep the data in sync
                                     since AMV columns likely will not have a column that can be used as a secondary Key
                                     ID within the secondary SQL table.
        06/03/20    dmb     v2.0.0 - Update the GetSQLInsertStatements and GetSQLUpdateStatements services so that AMV
                                     master columns with actual data (rather than empty values) will generate a SQL
                                     statement. This fixes a problem where NULLs were attempted to be stored in a
                                     primary key column.
        06/04/20    dmb     v2.0.0 - Update the GetStaticSQLColumnNames and GetStaticSQLColumnValues services to support
                                     the new JSON structure.
        06/05/20    dmb     v2.0.0 - Update the GetDatabases service so system schemas are excluded.
        06/06/20    dmb     v2.0.0 - Improve error handling in the GetAllQueryDataRows service.
        06/06/20    dmb     v2.0.0 - Update the GetAllQueryDataRows service to check the EOF property of the RecordSet
                                     object before calling the GetString method.
        06/06/20    dmb     v2.0.0 - Update the GetSchemas service to see if the error returned from the ExecuteQuery
                                     service reports a permissions error. If so, ignore and clear the error.
        06/08/20    dmb     v2.0.0 - Change all references to information_schema to all caps to avoid case sensitivity
                                     issues.
        06/08/20    dmb     v2.0.0 - Update the GetDatabases service to see if the response from the ExecuteQuery
                                     service is "0". If so, then clear the response and set an error.
        06/10/20    dmb     v2.0.1 - Update the GetJoinedOIColumns service to assign to the ValuesReturned passed by
                                     reference argument whether an associated column should return its column data
                                     (default) or its index position.
        06/10/20    dmb     v2.0.1 - Update the GetMappedOIColumnNames to use the ValuesReturned argument that is
                                     assigned by the GetJoinedOIColumns service. This service will also assigned its own
                                     ValuesReturned passed by preference argument.
        06/10/20    dmb     v2.0.1 - Update the GetMappedSQLColumnValues to use the ValuesReturned argument that is
                                     assigned by the GetMappedOIColumnNames service.
        06/10/20    dmb     v2.0.1 - Change the Errors argument to ErrorList in the AddConnectionErrors service to avoid
                                     suspected VNAV warnings.
        06/10/20    dmb     v2.0.1 - Change the Desc argument to Description in the SetConnectionDesc service to avoid
                                     suspected VNAV warnings.
        06/10/20    dmb     v2.0.1 - Update the GetMappedSQLColumnNames service to better exclude SQL columns that are
                                     not mapped.
        06/10/20    dmb     v2.0.1 - Update the GetMappedOIColumnNames service to better exclude OI columns that are not
                                     mapped.
        06/12/20    dmb     v2.0.1 - Update the GetAMVMaster service to locate the AMV Master using the attributes of
                                     the join clause rather than the dedicated key/value pair.
        06/12/20    dmb     v2.0.1 - Update the GetSQLInsertStatements and GetSQLUpdateStatements services to use the
                                     updated GetAMVMaster service.
        06/14/20    dmb     v2.0.1 - Update the GetSQLInsertStatements and GetSQLUpdateStatements services to verify if
                                     any columns used in the join clause are not defined in the map. If so, then include
                                     these columns and their corresponding values in the Insert or Update statements.
        06/14/20    dmb     v2.0.1 - Fix bug in the GetMappedSQLColumnValues service to properly return the AMV index
                                     position if the value returned requested it.
        06/14/20    dmb     v2.0.1 - Added the GetJoinedSQLColumns service.
        06/15/20    dmb     v2.0.1 - Update the GetMappedSQLColumnValues service to support the bit SQL data type.
        06/15/20    dmb     v2.0.1 - Update the GetTableColumns service to use a SQL statement that also returns PK and
                                     Null constraints.
        06/22/20    dmb     v2.0.1 - Update the GetTableColumns service so the results are sorted by PK and then
                                     alphabetically.
        06/22/20    dmb     v2.0.1 - Update the GetTableColumns service to make a USE <database> query if the database
                                     is known.
        06/23/20    dmb     v2.0.1 - Rename the GetJoinedSQLColumns service to GetMappedJoinedSQLColumns and update all
                                     calls to this service.
        06/23/20    dmb     v2.0.1 - Rename the GetJoinedSQLColumns service to GetMappedJoinedSQLColumnNames and update
                                     all calls to this service.
        06/23/20    dmb     v2.0.1 - Rename the GetJoinedOIColumns service to GetMappedJoinedOIColumnNames and update
                                     all calls to this service.
        06/23/20    dmb     v2.0.1 - Added the GetMappedJoinedSQLColumnValues service.
        06/23/20    dmb     v2.0.1 - Added the GetMappedJoinedOperators service.
        06/23/20    dmb     v2.0.1 - Major refactoring of all services that create SQL statements to better handle edge
                                     cases and joined columns.
        06/24/20    dmb     v2.0.1 - Fix memory leak in the GetMappedOIColumnNames service by releasing the MapObj
                                     object.
        06/29/20    dmb     v2.0.1 - Update the GetMappedSQLColumnValues service so that all errors are cleared once
                                     the column value is being calculated because any errors that are created must have
                                     come from a calculated column. This will avoid returning a number of values that
                                     differs from the number of column names.
        07/10/20    dmb     v2.0.1 - Update the GetMappedSQLColumnValues service to support the uniqueidentifier SQL
                                     data type.
        07/10/20    dmb     v2.0.1 - Udpate the GetMappedOIColumnNames and GetMappedSQLColumnNames services to support
                                     a ExcludeJoinedColumns flag rather than assume joined columns should always be
                                     excluded.
        07/10/20    dmb     v2.0.1 - Update the GetMappedOIColumnValues and GetMappedSQLColumnValues services to pass
                                     the ExcludeJoinedColumns flag to the GetMappedOIColumnNames and
                                     GetMappedSQLColumnNames services.
        07/20/20    dmb     v2.0.1 - Fix bug in the GetMappedJoinedSQLColumnValues service that attempted to convert
                                     an index value based on the data type.
        07/20/20    dmb     v2.0.1 - Fix bug in the WriteDataRowToSQL service to avoid VNAV error with the
                                     TransactionQueries variable.
        07/31/20    dmb     v2.0.2 - Fix bug in the GetSQLInsertStatements and GetSQLUpdateStatements services where
                                     maps to tables with no data columns would attempt to insert/update empty values
                                     in empty columns.
        07/31/20    dmb     v2.0.2 - Update the GetSQLUpdateStatements service to avoid creating an update state for
                                     primary tables that only have a join but no column maps.
        08/13/20    djs     v2.0.3 - Replaced CreateNewConnection service with CreateNewConnectionString and
                                     SaveConnectionString services. These new services allow multiple connections to
                                     the same SQL server (i.e. same SQL source, different user credentials and/or base
                                     catalog) to be stored under user defined Server ID key IDs.
        08/14/20    djs            - Created a new copy of SQL_Services (LSL2) from SQL_Services (SYSPROG).
                                     Added PostSQLRequest and ProcessSQLRequests services back into SQL_Services (LSL2),
                                     which are services that only exist in Infineon's copy of SQL_Services. These
                                     services add the ability to query SQL servers outside of the originating server's
                                     domain. Created SERVERS entries for all SQL Server connections currently in the
                                     APP_INFO table.
        08/17/20    djs            - Replaced all calls to SQL_Services_DJS (a temporary stored procedure that
                                     was utilized during the SQL_Services (SYSPROG) upgrade) throughout the LSL2
                                     application with calls to SQL_Services.
        08/17/20    dmb     v2.0.7 - Update the GetMappedJoinedSQLColumnValues and GetMappedSQLColumnValues services
                                     to safety check integer and floating point numbers for empty and non-numeric
                                     values and convert to something other than an empty or unquoted string.
        09/18/20    dmb     v2.0.8 - Update the GetTables service to sort the results rather than rely upon SQL to
                                     return sorted results.
        06/28/21    dmb     v2.0.9 - Update the GetMappedSQLColumnNames and GetMappedSQLColumnValues services to compare
                                     the passed in SQL Table against the mapped SQL table in the JSON definition before
                                     accepting the current SQL Column. This resolves issues where one OI table with only
                                     MultiValue columns is mapped to two SQL tables.
        02/12/2024  djm     v2.0.10- Move Free_Class.Net(DotNetObj) in ExecuteQuery to avoid memory leak.

***********************************************************************************************************************/

#pragma precomp SRP_PreCompiler

$insert LOGICAL
$insert SERVICE_SETUP
$insert SERVERS_EQUATES
$insert MICROSOFT_ADO_EQUATES
$insert DICT_EQUATES
$insert SQL_REQUESTS_EQUATES
$insert RLIST_EQUATES
$insert REVDOTNETEQUATES

Equ CRLF$  to \0D0A\
Equ COMMA$ to ','

Common /SQLServices/ ConnectionObjects@, Unused2@, Unused3@, Unused4@, Unused5@, Unused6@, Unused7@, Unused8@

Declare function    Memory_Services, SQL_Services, Database_Services, SRP_JSON, SRP_Array, SRP_Hash, SRP_COM, SRP_Encode
Declare function    SRP_Date, SRP_DateTime, SRP_Time, Utility, Unicode_UTF8, Str_Unicode, GetTickCount, SRP_Decode
Declare function    SRP_List, Environment_Services, Logging_Services, Active_Directory_Services
Declare subroutine  Memory_Services, SQL_Services, Database_Services, SRP_JSON, SRP_Stopwatch, SRP_COM, SRP_List
Declare subroutine  GetTempPath, GetTempFileName, Sleepery, WinYield, Yield, RList, Set_Status, Logging_Services
Declare subroutine  Update_Index, obj_Notes

UseRevDotNet = Database_Services('ReadDataRow', 'APP_INFO', 'USE_REVDOTNET')
If UseRevDotNet EQ '' then UseRevDotNet = False$

LogPath     = Environment_Services('GetApplicationRootPath') : '\LogFiles\SQL_Services'
LogDate     = Oconv(Date(), 'D4/')
LogTime     = Oconv(Time(), 'MTS')
LogFileName = LogDate[7, 4] : '-' : LogDate[1, 2] : '-' : LogDate[4, 2] : ' SQL Services Log.csv'
Headers     = 'Logging DTM' : @FM : 'Error'
objLog      = Logging_Services('NewLog', LogPath, LogFileName, CRLF$, ',', Headers, '', False$, False$)

LogPath     = Environment_Services('GetApplicationRootPath') : '\LogFiles\Replication'
LogFileName = LogDate[7, 4] : '-' : LogDate[1, 2] : '-' : LogDate[4, 2] : ' Null Record Log.csv'
Headers     = 'Logging DTM' : @FM : 'Error'
objNullLog  = Logging_Services('NewLog', LogPath, LogFileName, CRLF$, ',', Headers, '', False$, False$)

LoggingDTM  = LogDate : ' ' : LogTime   ; // Logging DTM

If ConnectionObjects@ EQ '' then ConnectionObjects@   = SRP_List('Create')

GoToService else
    Error_Services('Add', Service : ' is not a valid service request within the ' : ServiceModule : ' module.')
end

Return Response OR ''


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Service Parameter Options
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Options BOOLEAN         = True$, False$


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Services
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

//----------------------------------------------------------------------------------------------------------------------
// WriteDataRowToSQL
//
// OITableName  - The name of the OpenInsight table that is mapped to the SQL table being written to. - [Required]
// OIKeyID      - The Key ID to the OpenInsight row being written to SQL. - [Required]
// OIRow        - The OpenInsight row being written to SQL. - [Required]
// OIColumns    - An @FM delimited list of OpenInsight table columns that should only be used to write to SQL. If empty,
//                all columns that have a map defintion will be used. - [Optional]
// 
// High level services that writes OpenInsight data into a designated SQL table. This services calls other lower
// level services as needed and assumes an OpenInsight to SQL map has already been created.
//----------------------------------------------------------------------------------------------------------------------
Service WriteDataRowToSQL(OITableName, OIKeyID, OIRow, OIColumns)

    If (OITableName NE '') AND (OIKeyID NE '') AND (OIRow NE '') then
        // Create a new or get an existing Connection Object.
        SQLServer          = SQL_Services('GetMappedSQLServer', OITableName)
        objConnection      = SQL_Services('GetConnectionObject', SQLServer, 2)
        TransactionQueries = ''

        If Error_Services('NoError') then
            // Confirm if row already exists. This will determine if an INSERT or UPDATE should be made.
            PrimarySQLTableName = SQL_Services('GetPrimarySQLTable', OITableName)                
            TransactionQueries  = SQL_Services('GetSQLMergeStatements', OITableName, OIKeyID, OIRow, OIColumns)              
        end

        If Error_Services('NoError') AND (TransactionQueries NE '') then
            // Process the tranaction queries.
            SQL_Services('ExecuteQueries', objConnection, TransactionQueries, True$, OITableName, OIKeyID)
            If Error_Services('HasError') then
                ErrorMessage = Error_Services('GetMessage')
                Error_Services('Clear')
                InsertError  = IndexC(ErrorMessage, 'FOREIGN', 1)
                If InsertError then
                    // Record may have been inserted in between the time this routine began and by the time the SQL
                    // statement executed. Try an UPDATE statement instead.
                    TransactionQueries  = SQL_Services('GetSQLUpdateStatements', OITableName, OIKeyID, OIRow, OIColumns)
                    If Error_Services('NoError') AND (TransactionQueries NE '') then
                        SQL_Services('ExecuteQueries', objConnection, TransactionQueries, True$, OITableName, OIKeyID)
                    end
                end else
                    Error_Services('Add', ErrorMessage)
                end
            end
        end
    end else
       Error_Services('Add', 'The OITableName, OIKeyID, or OIRow argument was missing in the ' : Service : ' service.')
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// DeleteDataRowFromSQL
//
// High level services that deletes data from a designated SQL table. This services calls other lower level services as
// needed and assumes an OpenInsight to SQL map has already been created.
//----------------------------------------------------------------------------------------------------------------------
Service DeleteDataRowFromSQL(OITableName, OIKeyID)

    If (OITableName NE '') AND (OIKeyID NE '') then
        // Create a new or get an existing Connection Object.
        SQLServer           = SQL_Services('GetMappedSQLServer', OITableName)
        objConnection       = SQL_Services('GetConnectionObject', SQLServer, 2)
        TransactionQueries  = ''

        If Error_Services('NoError') then
            // Confirm if row already exists. This will determine if an INSERT or UPDATE should be made.
            PrimarySQLTableName     = SQL_Services('GetPrimarySQLTable', OITableName)
            JoinClause              = SQL_Services('GetJoinClause', OITableName, PrimarySQLTableName, OIKeyID, OIRow)
            SQLStatement            = 'SELECT TOP 1 * FROM ' : PrimarySQLTablename : ' WHERE ' : JoinClause
            RowData                 = SQL_Services('ExecuteQuery', objConnection, SQLStatement, True$)

            If RowData NE '' then
                // Create the DELETE statements so the data in SQL is cleared out first.
                TransactionQueries  = SQL_Services('GetSQLDeleteStatements', OITableName, OIKeyID)
            end
        end

        If Error_Services('NoError') AND (TransactionQueries NE '') then
            // Process the tranaction queries.
            SQL_Services('ExecuteQueries', objConnection, TransactionQueries, True$)
        end
    end else
        Error_Services('Add', 'The OITableName or OIKeyID argument was missing in the ' : Service : ' service.')
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// ReadDataRowFromSQL
//
// High level services that reads data from a designated SQL table. This services calls other lower level services as
// needed and assumes an OpenInsight to SQL map has already been created.
//----------------------------------------------------------------------------------------------------------------------
Service ReadDataRowFromSQL(OITableName, OIKeyID)

    OIRow   = ''

    If (OITableName NE '') AND (OIKeyID NE '') then
        // Create a new or get an existing Connection Object.
        SQLServer       = SQL_Services('GetMappedSQLServer', OITableName)
        objConnection   = SQL_Services('GetConnectionObject', SQLServer, 2)

        If Error_Services('NoError') then
            // Select the SQL data for the primary SQL table associated to the OpenInsight table.
            SQLTableName        = SQL_Services('GetPrimarySQLTable', OITableName)
            SQLSelectStatement  = SQL_Services('GetSQLSelectStatement', OITableName, SQLTableName, OIKeyID)
            SQLData             = SQL_Services('ExecuteQuery', objConnection, SQLSelectStatement, True$)
            If (SQLData NE '') AND (Error_Services('NoError')) then
                OIRow   = SQL_Services('GetMappedOIColumnValues', OITableName, SQLTableName, SQLData)
            end
        end

        If Error_Services('NoError') then
            // Select the SQL data for all secondary SQL tables associated to the OpenInsight table.
            SQLTableNames   = SQL_Services('GetSecondarySQLTables', OITableName)
            If SQLTableNames NE '' then
                For Each SQLTableName in SQLTableNames using @FM
                    SQLSelectStatement  = SQL_Services('GetSQLSelectStatement', OITableName, SQLTableName, OIKeyID)
                    // Create a new or get an existing Connection Object.
                    SQLServer           = SQL_Services('GetMappedSQLServer', OITableName)
                    objConnection       = SQL_Services('GetConnectionObject', SQLServer, 2)

                    If Error_Services('NoError') AND (SQLSelectStatement NE '') then
                        SQLData = SQL_Services('ExecuteQuery', objConnection, SQLSelectStatement, True$)
                        If (SQLData NE '') AND (Error_Services('NoError')) then
                            OIRow   = SQL_Services('GetMappedOIColumnValues', OITableName, SQLTableName, SQLData, OIRow)
                        end
                    end
                While (Error_Services('NoError'))
                Next SQLTable
            end
        end
    end else
        Error_Services('Add', 'The OITableName or OIKeyID argument was missing in the ' : Service : ' service.')
    end

    Response    = OIRow

end service


//----------------------------------------------------------------------------------------------------------------------
// GetRowCount
//
// Server   - Name of the server the connection object is bound to. This should be the same name that will be used in
//            the Data Source connection string.
// Table    - SQL table whose row count is being requested.
//
// High level services that counts the number of rows in the designated table. This services calls other lower level
// services.
//----------------------------------------------------------------------------------------------------------------------
Service GetRowCount(Server, TableName)

    RowCount    = ''

    If (Server NE '') AND (TableName NE '') then
        // Create a new or get an existing Connection Object.
        objConnection   = SQL_Services('GetConnectionObject', Server, 2)

        If Error_Services('NoError') then
            // Select the SQL data for the primary SQL table associated to the OpenInsight table.
            ErrorMessage        = ''
            SQLCountStatement   = 'SELECT COUNT(*) FROM ' : TableName
            RowCount            = SQL_Services('ExecuteQuery', objConnection, SQLCountStatement, True$)
            If Error_Services('HasError') then
                ErrorMessage    = Error_Services('GetMessage')
            end
            SQL_Services('DestroyConnectionObject', objConnection)
            If ErrorMessage NE '' then Error_Services('Add', ErrorMessage)
        end
    end else
        Error_Services('Add', 'The Server or TableName argument was missing in the ' : Service : ' service.')
    end

    Response    = RowCount

end service


//----------------------------------------------------------------------------------------------------------------------
// GetDataRows
//
// Server           - Name of the server the connection object is bound to. This should be the same name that
//                    will be used in the Data Source connection string.
// QueryStatement   - Query command. This should be a properly syntaxed SQL statement.
//
// High level service that gets database rows from the indicated database using the indicated query statement. This
// service will automatically get the connection and query object needed to perform the query statement.
//----------------------------------------------------------------------------------------------------------------------
Service GetDataRows(Server, QueryStatement, AttemptsBeforeFailure)
    
    If AttemptsBeforeFailure EQ '' then AttemptsBeforeFailure = 1
    DataRows    = ''

    If ( (Server NE '') AND (QueryStatement NE '') ) then
        ErrorMessage    = ''
        objConnection   = SQL_Services('GetConnectionObject', Server, 2)
        If Error_Services('NoError') then
            If Error_Services('NoError') then
                DataRows    = SQL_Services('ExecuteQuery', objConnection, QueryStatement, True$, AttemptsBeforeFailure)
            end
            // Get any errors from the GetConnectionObject service or the ExecuteQuery service.
            ErrorMessage    = Error_Services('GetMessage')
        end
        SQL_Services('DestroyConnectionObject', objConnection)
        If ErrorMessage then Error_Services('Set', ErrorMessage)
    end else
        Error_Services('Add', 'Server or QueryStatement argument was missing in the ' : Service : 'service.')
    end

    Response    = DataRows

end service


//----------------------------------------------------------------------------------------------------------------------
// GetConnectionObject
//
// Server                   - Name of the server the connection object is bound to. This should be the same name that
//                            will be used in the Data Source connection string.
// AttemptsBeforeFailure    - Number of attempts to make the connect before erroring out. Default is 1.
//
// Returns a Connection Object based on the server. A returned Connection Object means that the server has been
// successfully connected to with the provided credentials. This service uses caching, so until an existing Connection
// Object has been destroyed (see the DestroyConnectionObject service), it will continue to use the one already in
// existence.
//----------------------------------------------------------------------------------------------------------------------
Service GetConnectionObject(Server, AttemptsBeforeFailure)

    If UseRevDotNet EQ False$ then
        If AttemptsBeforeFailure EQ '' then AttemptsBeforeFailure = 1

        objConnection   = ''

        // Check to see if the Connection Object already exists.
        ServiceKeyID   := '*' : Server
        objConnection   = Memory_Services('GetValue', ServiceKeyID)

        If objConnection EQ '' then
            If Server NE '' then
                Attempts    = 0
                Error       = ''
                Loop
                    If SRP_COM(objConnection, 'CREATE', 'ADODB.Connection') then
                        Memory_Services('SetValue', objConnection : '*Server', Server)
                        If SRP_COM('', 'ERROR') then
                            Error           = SRP_COM('', 'ERROR')
                            SQL_Services('DestroyConnectionObject', objConnection)
                            objConnection   = ''
                        end else
                            ConnectionString    = SQL_Services('GetConnectionString', Server)
                            If Error_Services('NoError') then
                                SRP_COM(objConnection, 'CALL', 'Open', ConnectionString)
                                If SRP_COM('', 'ERROR') then
                                    Error           = SRP_COM('', 'ERROR')
                                    SQL_Services('DestroyConnectionObject', objConnection)
                                    objConnection   = ''
                                end
                            end else
                                objConnection   = ''
                            end
                        end
                    end
                    Attempts   += 1
                Until (objConnection NE '') OR (Attempts GE AttemptsBeforeFailure)
                Repeat
                If objConnection then
                    Memory_Services('SetValue', ServiceKeyID, objConnection)
                end else
                    If Error_Services('NoError') then
                        // If no error message has been set then create a generic one.
                        Error_Services('Add', 'Unable to create the connection object to ' : Server : ' in the ' : Service : ' service: ' : Error)
                    end
                end
            end else
                Error_Services('Add', 'Server argument was missing from the ' : Service : ' service.')
            end
        end

        Response = objConnection
        
    end else

        If AttemptsBeforeFailure EQ '' then AttemptsBeforeFailure = 1

        objConnection   = ''

        // Check to see if the Connection Object already exists.
        ServiceKeyID  := '*' : Server : '*objConnection'
        objConnection  = Memory_Services('GetValue', ServiceKeyID)

        If objConnection EQ '' then
            If Server NE '' then
                Attempts    = 0
                Error       = ''
                Loop
                    // Define the .NET version
                    DotNetHandle     = StartDotNet("","4.0")
                    DotNetDir        = CheckDotNet('4.0'):'\'
                    SqlClientDLLPath = DotNetDir:'System.Data.dll'
                    ConnectionString = Sql_Services('GetConnectionString', Server)
                    // Specify the DLL
                    rv = Set_Property.NET(DotNetHandle, "AssemblyName", SqlClientDLLPath)
                    // Define the classes
                    objConnection = Create_Class.NET(DotNetHandle, "System.Data.SqlClient.SqlConnection", 0, ConnectionString, 'System.String')
                    If Not(Get_Status(errCode)) then
                        rv = Set_Property.NET(objConnection, 'Pooling', 'False')
                        If Not(Get_Status(errCode)) then
                            rv = Send_Message.Net(objConnection, "Open")
                            If Not(Get_Status(errCode)) then
                                ConnectionState = Get_Property.Net(objConnection, "State")
                                If ConnectionState EQ 'Open' then
                                    ClientConnectionID = Get_Property.NET(objConnection, 'ClientConnectionId', False$)
                                    Memory_Services('SetValue', ClientConnectionID : '*Server', Server)
                                end else
                                    Sql_Services('DestroyConnectionObject', objConnection)
                                    objConnection   = ''
                                end 
                            end else
                                Sql_Services('DestroyConnectionObject', objConnection)
                                objConnection   = ''
                            end
                        end else
                            Sql_Services('DestroyConnectionObject', objConnection)
                            objConnection   = ''
                        end
                    end
                    Attempts += 1
                Until (objConnection NE '') OR (Attempts GE AttemptsBeforeFailure)
                Repeat
                If objConnection and Error_Services('NoError') then
                    Memory_Services('SetValue', ServiceKeyID, objConnection)
                    ConnObjIndex = SRP_List('Add', ConnectionObjects@, objConnection)
                end else
                    If Assigned(objConnection) then Free_Class.Net(objConnection)
                    If Error_Services('NoError') then
                        // If no error message has been set then create a generic one.
                        Error_Services('Add', 'Unable to create the connection object to ' : Server : ' in the ' : Service : ' service: ' : Error)
                    end
                end
            end else
                Error_Services('Add', 'Server argument was missing from the ' : Service : ' service.')
            end
        end
        
        Response = objConnection
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// DestroyConnectionObject
//
// objConnection    - Connection Object (see GetConnectionObject service.)
//
// Destroys the Connection Object. This returns True$ if successful, False$ if not.
//----------------------------------------------------------------------------------------------------------------------
Service DestroyConnectionObject(objConnection)

    Destroyed   = False$    ; // Assume object is not destroyed for now.
    
    If UseRevDotNet EQ False$ then

        If objConnection NE '' then
            Error   = ''
            // Close the connection object, but don't worry about errors. It might have already been closed.
            SRP_COM(objConnection, 'CALL', 'Close')
            SRP_COM(objConnection, 'RELEASE')
            If SRP_COM('', 'ERROR') then
                Error       = SRP_COM('', 'ERROR')
            end else
                Destroyed   = True$
            end
            If Destroyed then
                SQL_Services('ClearConnectionErrors', objConnection)
                Server  = Memory_Services('GetValue', objConnection : '*Server')
                Memory_Services('SetValue', ServiceModule : '*GetConnectionObject*' : Server, '')
            end else
                SQL_Services('AddConnectionErrors', objConnection, Error)
                Error_Services('Add', 'Service : ' : Service : ', Error : ' : Error)            
            end
        end else
            Error_Services('Add', 'The objConnection argument was missing in the ' : Service : ' service.')
        end

    end else
        
        If objConnection NE '' then
            ConnObjIndex = SRP_List('Locate', ConnectionObjects@, objConnection)
            If ConnObjIndex GT 0 then
                Error              = ''
                ClientConnectionID = Get_Property.NET(objConnection, 'ClientConnectionId', False$)
                // Close the connection object, but don't worry about errors. It might have already been closed.
                Set_Status(0)
                Sql_Services('ClearConnectionErrors', objConnection)
                rv                 = Send_Message.Net(objConnection, 'Close')
                errCode            = ''
                If Not(Get_Status(errCode)) then
                    Server        = Memory_Services('GetValue', ClientConnectionID : '*Server')
                    MemKey        = ServiceModule : '*GetConnectionObject*' : Server : '*objConnection'
                    Memory_Services('SetValue', MemKey, '')
                    SRP_List('RemoveAt', ConnectionObjects@, ConnObjIndex)
                    Free_Class.Net(objConnection)
                end else
                    Error = 'Error in service ':Service:'. Failed to close SQL connection. Error code: ':errCode
                    Sql_Services('AddConnectionErrors', objConnection, Error)
                    Error_Services('Add', 'Service : ' : Service : ', Error : ' : Error)            
                end
            end
            
        end else
            Error_Services('Add', 'The objConnection argument was missing in the ' : Service : ' service.')
        end

    end
    Response = Destroyed

end service


//----------------------------------------------------------------------------------------------------------------------
// AddConnectionErrors
//
// objConnection    - Connection Object (see GetConnectionObject service.)
// ErrorList        - @FM delimited list of errors.
//
// Adds a list of errors to the indicated Connection Object so the GetConnectionErrors service can retrieve them at
// will. This list will accumulate until the ClearConnectionErrors service is called.
//----------------------------------------------------------------------------------------------------------------------
Service AddConnectionErrors(objConnection, ErrorList)

    If UseRevDotNet EQ False$ then
        objConnection       = objConnection[1, '*']
        ServiceKeyID        = ServiceModule : '*' : 'GetConnectionErrors' : '*' : objConnection
        ConnectionErrors    = Memory_Services('GetValue', ServiceKeyID)

        If (objConnection NE '') AND (ErrorList NE '') then
            If ConnectionErrors NE '' then
                ConnectionErrors    := @FM : ErrorList
            end else
                ConnectionErrors     = ErrorList
            end
            Memory_Services('SetValue', ServiceKeyID, ConnectionErrors)
        end else
            Error_Services('Add', 'The objConnection or ErrorList argument was missing in the ' : Service : ' service.')
        end
    end else
        objConnection      = objConnection[1, '*']
        ClientConnectionID = Get_Property.NET(objConnection, 'ClientConnectionId', False$)
        ServiceKeyID       = ServiceModule : '*' : 'GetConnectionErrors' : '*' : ClientConnectionID
        ConnectionErrors   = Memory_Services('GetValue', ServiceKeyID)

        If (objConnection NE '') AND (ErrorList NE '') then
            If ConnectionErrors NE '' then
                ConnectionErrors    := @FM : ErrorList
            end else
                ConnectionErrors     = ErrorList
            end
            Memory_Services('SetValue', ServiceKeyID, ConnectionErrors)
        end else
            Error_Services('Add', 'The objConnection or ErrorList argument was missing in the ' : Service : ' service.')
        end
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// GetConnectionErrors
//
// objConnection    - Connection Object (see GetConnectionObject service.)
//
// Returns an @FM list of all connection errors associated with the indicated Connection Object.
//----------------------------------------------------------------------------------------------------------------------
Service GetConnectionErrors(objConnection)

    If UseRevDotNet EQ False$ then
        objConnection       = objConnection[1, '*']
        ConnectionErrors    = ''

        If objConnection NE '' then
            ServiceKeyID        = ServiceModule : '*' : 'GetConnectionErrors' : '*' : objConnection
            ConnectionErrors    = Memory_Services('GetValue', ServiceKeyID)
        end else
            Error_Services('Add', 'The objConnection argument was missing in the ' : Service : ' service.')
        end

        Response    = ConnectionErrors
    end else
        objConnection    = objConnection[1, '*']
        ConnectionErrors = ''

        If objConnection NE '' then
            ClientConnectionID = Get_Property.NET(objConnection, 'ClientConnectionId', False$)
            ServiceKeyID       = ServiceModule : '*' : 'GetConnectionErrors' : '*' : ClientConnectionID
            ConnectionErrors   = Memory_Services('GetValue', ServiceKeyID)
        end else
            Error_Services('Add', 'The objConnection argument was missing in the ' : Service : ' service.')
        end

        Response    = ConnectionErrors
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// ClearConnectionErrors
//
// objConnection    - Connection Object (see GetConnectionObject service.)
//
// Clears all connection errors associated with the indicated Connection Object.
//----------------------------------------------------------------------------------------------------------------------
Service ClearConnectionErrors(objConnection)

    If UseRevDotNet EQ False$ then
        objConnection   = objConnection[1, '*']

        If objConnection NE '' then
            ServiceKeyID    = ServiceModule : '*' : 'GetConnectionErrors' : '*' : objConnection
            Memory_Services('SetValue', ServiceKeyID, '')
        end else
            Error_Services('Add', 'The objConnection argument was missing in the ' : Service : ' service.')
        end
    end else
        objConnection   = objConnection[1, '*']

        If objConnection NE '' then
            ClientConnectionID = Get_Property.NET(objConnection, 'ClientConnectionId', False$)
            ServiceKeyID       = ServiceModule : '*' : 'GetConnectionErrors' : '*' : ClientConnectionID
            Memory_Services('SetValue', ServiceKeyID, '')
        end else
            Error_Services('Add', 'The objConnection argument was missing in the ' : Service : ' service.')
        end
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// HasConnectionErrors
//
// objConnection    - Connection Object (see GetConnectionObject service.)
//
// Returns a Boolean flag indicating of the Connection Object has any connection errors.
//----------------------------------------------------------------------------------------------------------------------
Service HasConnectionErrors(objConnection)

    objConnection   = objConnection[1, '*']
    HasErrors       = False$    ; // Assume no errors for now.

    If objConnection NE '' then
        ConnectionErrors = SQL_Services('GetConnectionErrors', objConnection)
        If ConnectionErrors NE '' then
            HasErrors   = True$
        end else
            HasErrors   = False$
        end
    end else
        Error_Services('Add', 'The objConnection argument was missing in the ' : Service : ' service.')
    end

    Response = HasErrors

end service


//----------------------------------------------------------------------------------------------------------------------
// GetConnectionString
//
// Returns a well formed Connection String for the current server. This is used for ADO connections.
//----------------------------------------------------------------------------------------------------------------------
Service GetConnectionString(Server)

    If UseRevDotNet EQ False$ then
        ServiceKeyID       := '*' : Server
        ConnectionString    = Memory_Services('GetValue', ServiceKeyID)

        If ConnectionString EQ '' then
            If (Server NE '') then
                ConnectionInfo  = Database_Services('ReadDataRow', 'SERVERS', Server)
                If ConnectionInfo NE '' then
                    Swap \0D0A\ with @FM in ConnectionInfo
                    ConnectionString    = ConnectionInfo<3>
                    ConnectionString   := ';'
                    Memory_Services('SetValue', ServiceKeyID, ConnectionString)
                end else
                    Error_Services('Add', Server : ' is not a valid server. [' : Service : ']')
                end
            end else
                Error_Services('Add', 'The Server argument was missing in the ' : Service : ' service.')
            end
        end

        Response    = ConnectionString
    end else

        If (Server NE '') then
			// Prod Stealth
			ConnectionString = Database_Services('ReadDataRow', 'SERVERS', Server)

            If ConnectionString NE '' then
                ConnectionString = Delete(ConnectionString, 2, 0, 0)
                ProviderIdx = IndexC(ConnectionString, 'Provider', 1)
                If ProviderIdx then
                    ConnectionString = ConnectionString[(ProviderIdx), 999]
                    ProviderEndIdx   = Index(ConnectionString, ';', 1)
                    ConnectionString = ConnectionString[(ProviderEndIdx + 1), 999]
                end
                InitialIdx = IndexC(ConnectionString, 'Initial File Name', 1)
                If InitialIdx then
                    InitialLen = Len(ConnectionString[InitialIdx, 'F;'])
                    ConnectionString[InitialIdx, InitialLen + 1] = ''
                end
                DataSourceIdx = IndexC(ConnectionString, 'Data Source', 1)
                If DataSourceIdx then
                    ConnectionString[DataSourceIdx + 12, 0] = 'tcp:'
                end
                TrustIdx = IndexC(ConnectionString, 'Trust Server Certificate', 1)
                If TrustIdx then
                	TrustLen                                 = Len(ConnectionString[TrustIdx, 'F;'])
                	ConnectionString[TrustIdx, TrustLen + 1] = ''
                end
                
                AccessIdx = IndexC(ConnectionString, 'Access Token', 1)
                If AccessIdx then
                	AccessLen                                  = Len(ConnectionString[AccessIdx, 'F;'])
                	ConnectionString[AccessIdx, AccessLen + 1] = ''
                end
                
                AuthIdx = IndexC(ConnectionString, 'Authentication', 1)
                If AuthIdx then
                	AuthLen                                = Len(ConnectionString[AuthIdx, 'F;'])
                	ConnectionString[AuthIdx, AuthLen + 1] = ''
                end                
                
                SpnIdx = IndexC(ConnectionString, 'Server SPN', 1)
                If SpnIdx then
                	SpnLen                               = Len(ConnectionString[SpnIdx, 'F;'])
                	ConnectionString[SpnIdx, SpnLen + 1] = ''
                end
                If ConnectionString[-1, 1] EQ ';' then ConnectionString[-1, 1] = ''
            end else
                Error_Services('Add', Server : ' is not a valid server. [' : Service : ']')
            end
        end else
            Error_Services('Add', 'The Server argument was missing in the ' : Service : ' service.')
        end

        Response    = ConnectionString
        
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// ExecuteQuery
//
// objConnection    - Connection Object (see GetConnectionObject service.)
// QueryStatement   - Query command. This should be a properly syntaxed SQL statement.
// ReturnDataRows   - Boolean flag to determine if all data rows resulting from the query should automatically be
//                    returned. This is the same as if the caller used the GetAllQueryDataRows service, but this allows
//                    everything to be done in one service rather than two. Default is False$
//
// Executes the query against the Connection Object. If the ReturnDataRows argument is False$, then this returns True$
// if there are results, False$ if there are no results or if there is a problem running the services. Otherwise, the
// datarows from the query will be returned unless there is an error.
//----------------------------------------------------------------------------------------------------------------------
Service ExecuteQuery(objConnection, QueryStatement, ReturnDataRows=BOOLEAN, AttemptsBeforeFailure)

    If AttemptsBeforeFailure EQ '' then AttemptsBeforeFailure = 1
    Results = False$    ; // Assume no results from this query for now.
    If ReturnDataRows NE True$ then ReturnDataRows = False$
    ErrorMessage = ''
    If UseRevDotNet EQ False$ then
        
        If (objConnection NE '') AND (QueryStatement NE '') then        
            Attempts     = 0
            ErrorMessage = ''
            Loop
                objRecordSet    = SRP_COM(objConnection, 'CALL', 'Execute', QueryStatement)
                If SRP_COM('', 'ERROR') OR (objRecordSet EQ 0) then
                    ErrorMessage = 'ADO error in the ' : Service : ' service: ' : SRP_COM('', 'ERROR')
                    Results      = ''    ; // Since there is an error, no result should be returned.
                end else
                    If ReturnDataRows then
                        Results = SQL_Services('GetAllQueryDataRows', objRecordSet)
                    end else
                        Results = True$
                    end
                    SRP_COM(objRecordSet, 'CALL', 'Cancel')
                    SRP_COM(objRecordSet, 'RELEASE')
                end
                Attempts   += 1
            Until ( (ErrorMessage EQ '') OR (Attempts GE AttemptsBeforeFailure) )
            Repeat
        end else
            Error_Services('Add', 'The objConnection or QueryStatement argument was missing in the ' : Service : ' service.')
        end
    end else
        If (objConnection NE '') AND (QueryStatement NE '') then
            Attempts     = 0
            ConnObjIndex = SRP_List('Locate', ConnectionObjects@, objConnection)
            If ConnObjIndex GT 0 then
                Loop
                    objCommand = Send_Message.Net(objConnection, 'CreateCommand', '', '', 1)
                    If Not(Get_Status(errCode)) then
                        rv = Set_Property.NET(objCommand, 'CommandText', QueryStatement)
                        If Not(Get_Status(errCode)) then
                            If ReturnDataRows then
                                objReader = Send_Message.Net(objCommand, 'ExecuteReader', '', '', 1)
                                If Not(Get_Status(errCode)) then
                                    Reading  = ''
                                    RowIndex = 0
                                    Loop
                                        RowData   = ''
                                        RowIndex += 1 
                                        Reading   = Send_Message.Net(objReader, 'Read')
                                    Until ( (Reading EQ 'False') or (ErrorMessage NE '') ) 
                                        If Not(Get_Status(errCode)) then
                                            NumCols        = Get_Property.Net(objReader, 'FieldCount')
                                            If Not(Get_Status(errCode)) then
                                                For Col = 1 to NumCols
                                                    DotNetObj = Send_Message.Net(objReader, 'GetValue', (Col - 1), 'System.Int32', 1)
                                                    If Not(Get_Status(errCode)) then
                                                        Result = Send_Message.Net(DotNetObj, 'ToString')
                                                        If (Result EQ 'True'  or Result EQ 'False') then
                                                            DotNetObjType   = Send_Message.Net(DotNetObj, 'GetType')
                                                            If DotNetObjType EQ 'System.Boolean' then
                                                                Swap 'True' with 1 in Result
                                                                Swap 'False' with 0 in Result
                                                            end
                                                        end
                                                        RowData<0, Col> = Result
                                                        If Get_Status(errCode) then
                                                            ErrorMessage = 'Error calling ToString method in the Object object in the ' : Service : ' service.' : errCode
                                                        end
                                                    end else
                                                        ErrorMessage = 'Error calling GetValue method in the SqlDataReader object in the ' : Service : ' service.' : errCode
                                                    end
                                                    If Assigned(DotNetObj) then Free_Class.NET(DotNetObj)
                                                Until Error EQ True$
                                                Next Col
                                                
                                            end else
                                                ErrorMessage = 'Error getting FieldCount property of the SqlDataReader in the ' : Service : ' service.' : errCode
                                            end
                                        end else
                                            ErrorMessage = 'Error calling Read method in the SqlDataReader object in the ' : Service : ' service.' : errCode
                                        end
                                        Results<RowIndex> = RowData
                                    Repeat
                                    If Reading EQ 'False' and RowIndex EQ 1 then Results = '' ; // No results
                                end else
                                    AccessError = (IndexC(errCode, 'not able to access the database', 1) GT 0)
                                    If Not(AccessError) then ErrorMessage = 'Error creating SqlDataReader object in the ' : Service : ' service.' : errCode
                                end
                                Swap @FM with @RM in Results
                                Swap @VM with @FM in Results
                                If Assigned(objReader) and (objReader NE '') then
                                    rv = Send_Message.Net(objReader, 'Close')
                                    Free_Class.NET(objReader)
                                end
                            end else
                                objTransaction = Send_Message.Net(objConnection, 'BeginTransaction', '', '', 1)
                                If Not(Get_Status(errCode)) then
                                    rv = Set_Property.NET(objCommand, 'Transaction', objTransaction)
                                    If Not(Get_Status(errCode)) then
                                        Results = Send_Message.Net(objCommand, "ExecuteScalar")
                                        If Not(Get_Status(errCode)) then
                                            rv = Send_Message.Net(objTransaction, 'Commit')
                                            If Get_Status(errCode) then
                                                AllAccepted = False$
                                                ErrorMessage = 'Error committing transaction in the ' : Service : ' service.' : errCode
                                            end else
                                                Results = True$
                                            end
                                        end else
                                            ErrorMessage = 'Failed to execute SQL command in the ' :Service : 'service.' : errCode
                                        end
                                    end else
                                        ErrorMessage = 'Unable to set Transaction property of the SqlCommand object in the ' : Service : ' service: ' : errCode
                                    end
                                end else
                                    ErrorMessage = 'Unable to create the SQLTransaction object in the ' : Service : ' service: ' : errCode
                                end
                            end
                        end else
                            ErrorMessage = 'Unable to set CommandText property of the SQLCommand object in the ' : Service : ' service: ' : errCode
                        end
                        If Assigned(objTransaction) then Free_Class.NET(objTransaction)
                        If Assigned(objCommand) then Free_Class.NET(objCommand)
                    end else
                        ErrorMessage = 'Unable to create the SQLCommand object in the ' : Service : ' service: ' : errCode
                    end
                    Attempts   += 1
                Until ( (ErrorMessage EQ '') or (Attempts GE AttemptsBeforeFailure) )
                Repeat
            end else
                ErrorMessage = 'objCommand could not be found in the ' : Service : ' service.'
            end
        end else
            ErrorMessage = 'The objConnection or QueryStatement argument was missing in the ' : Service : ' service.'
        end
        If ErrorMessage NE '' then
            Results    = ''
            LogData    = ''
            LogData<1> = LoggingDTM
            LogData<2> = QueryStatement
            LogData<3> = ErrorMessage
            Logging_Services('AppendLog', objLog, LogData, @RM, @FM)
            Swap @FM with @VM in ErrorMessage
            Error_Services('Add', ErrorMessage)
        end
    end
    Response = Results

end service


//----------------------------------------------------------------------------------------------------------------------
// ExecuteQueries
//
// objConnection    - Connection Object (see GetConnectionObject service.)
// QueryStatements  - @FM delimited list of Query commands. These should be a properly syntaxed SQL statement.
//
// Executes one or more queries against the Connection Object. This returns True$ if all queries processed without any
// errors, False$ if there is a problem running any of the queries.
//----------------------------------------------------------------------------------------------------------------------
Service ExecuteQueries(objConnection, QueryStatements, UseTransactionControl=BOOLEAN, OITableName, OIKeyID)

    If UseTransactionControl NE True$ then UseTransactionControl = False$

    AllAccepted  = True$    ; // Assume all queries were accepted for now unless otherwise proven.
    ErrorMessage = ''
    
    If UseRevDotNet EQ False$ then

        If (objConnection NE '') AND (QueryStatements NE '') then
            *Message = '1. Begin transaction processing.' ; GoSub WriteLog
            If UseTransactionControl EQ True$ then
                // Begin transaction processing.
                objRecordSet    = SRP_COM(objConnection, 'CALL', 'Execute', 'BEGIN TRANSACTION')
                *Message = '2. Begin transaction processing.' ; GoSub WriteLog
                If SRP_COM('', 'ERROR') OR (objRecordSet EQ 0) then
                    *Message = '3a. Begin transaction processing.' ; GoSub WriteLog
                    Error_Services('Add', 'ADO error in the ' : Service : ' service: ' : SRP_COM('', 'ERROR'))
                end else
                    *Message = '3b. Begin transaction processing.' ; GoSub WriteLog
                    SRP_COM(objRecordSet, 'RELEASE')
                end
            end
            *Message = '4. Begin transaction processing.' ; GoSub WriteLog
            If Error_Services('NoError') then
                *Message = '5. Begin execute call.' ; GoSub WriteLog
                For Each QueryStatement in QueryStatements using @FM
                    If QueryStatement NE '' then
                        *Message = '6. Begin execute call.' ; GoSub WriteLog
                        objRecordSet    = SRP_COM(objConnection, 'CALL', 'Execute', QueryStatement)
                        If SRP_COM('', 'ERROR') OR (objRecordSet EQ 0) then
                            *Message = '7a. Begin execute call.' ; GoSub WriteLog
                            Error_Services('Add', 'ADO error in the ' : Service : ' service: ' : SRP_COM('', 'ERROR'))
                            AllAccepted = False$
                        end else
                            *Message = '7b. Begin execute call.' ; GoSub WriteLog
                            SRP_COM(objRecordSet, 'RELEASE')
                        end
                    end
                Until Error_Services('HasError')
                Next QueryStatement
            end
            *Message = '8. Begin execute call.' ; GoSub WriteLog
            If UseTransactionControl EQ True$ then
                // COMMIT or ROLLBACK the transaction.
                *Message = '9. Begin execute call.' ; GoSub WriteLog
                If Error_Services('NoError') then
                    *Message = '10a. Begin execute call.' ; GoSub WriteLog
                    objRecordSet    = SRP_COM(objConnection, 'CALL', 'Execute', 'COMMIT TRANSACTION')
                end else
                    *Message = '10b. Begin execute call.' ; GoSub WriteLog
                    AllAccepted     = False$
                    objRecordSet    = SRP_COM(objConnection, 'CALL', 'Execute', 'ROLLBACK TRANSACTION')
                    SRP_COM(objRecordSet, 'RELEASE')
                end
                *Message = '11. Begin execute call.' ; GoSub WriteLog
                SRP_COM(objRecordSet, 'RELEASE')
            end
        end else
            *Message = '12. All accepted not true.' ; GoSub WriteLog
            AllAccepted = False$
            Error_Services('Add', 'The objConnection or QueryStatement argument was missing in the ' : Service : ' service.')
        end
    end else
        If (objConnection NE '') AND (QueryStatements NE '') then
            ConnObjIndex = SRP_List('Locate', ConnectionObjects@, objConnection)
            If ConnObjIndex GT 0 then
                objCommand = Send_Message.Net(objConnection, 'CreateCommand', '', '', 1)
                If Not(Get_Status(errCode)) then
                    objTransaction = Send_Message.Net(objConnection, 'BeginTransaction', 'Test', 'System.String', 1)
                    If Not(Get_Status(errCode)) then
                        rv = Set_Property.NET(objCommand, 'Transaction', objTransaction)
                        If Not(Get_Status(errCode)) then
                            For Each QueryStatement in QueryStatements using @FM
                                If QueryStatement NE '' then
                                    rv = Set_Property.Net(objCommand, 'CommandText', QueryStatement)
                                    If Not(Get_Status(errCode)) then
                                        rv = Send_Message.Net(objCommand, 'ExecuteNonQuery')
                                        If Get_Status(errCode) then
                                            ErrorMessage = 'Error executing command ':Quote(QueryStatement):' in the ' : Service : ' service.' : errCode
                                        end
                                    end else
                                        ErrorMessage = 'Error setting CommandText property of the SqlCommand object in the ' : Service : ' service.' : errCode
                                    end
                                end
                            Until ErrorMessage NE ''
                            Next QueryStatement
                        end else
                            ErrorMessage = 'Unable to set Transaction property of the SqlCommand object in the ' : Service : ' service. ' : errCode
                        end
                    end else
                        ErrorMessage = 'Unable to create the SQLTransaction object in the ' : Service : ' service. ' : errCode
                    end
                end else
                    ErrorMessage = 'Unable to create the SQLCommand object in the ' : Service : ' service. ' : errCode
                end
                If UseTransactionControl EQ True$ then
                    // COMMIT or ROLLBACK the transaction.
                    If ErrorMessage EQ '' then
                        rv = Send_Message.Net(objTransaction, 'Commit')
                        If Get_Status(errCode) then
                            AllAccepted = False$
                            ErrorMessage = 'Error committing transaction in the ' : Service : ' service.' : errCode
                        end
                    end else
                        AllAccepted     = False$
                        rv = Send_Message.Net(objTransaction, 'Rollback')
                    end
                end
                If Assigned(objTransaction) then Free_Class.NET(objTransaction)
                If Assigned(objCommand) then Free_Class.NET(objCommand)
            end
        end else
            AllAccepted = False$
            ErrorMessage = 'The objConnection or QueryStatement argument was missing in the ' : Service : ' service.'
        end
        If ErrorMessage NE '' then
            LogData    = ''
            LogData<1> = LoggingDTM
            LogData<2> = OITableName
            LogData<3> = OIKeyID
            LogData<4> = ErrorMessage
            Logging_Services('AppendLog', objLog, LogData, @RM, @FM)
            Swap @FM with ',' in ErrorMessage
            Error_Services('Add', ErrorMessage)
        end
    end

    Response = AllAccepted

end service


//----------------------------------------------------------------------------------------------------------------------
// GetAllQueryDataRows
//
// objRecordSet - RecordSet Object (see ExecuteQuery service.)
//
// Gets all of the data rows from the RecordSet Object. This returns an @RM array of data rows.
//----------------------------------------------------------------------------------------------------------------------
Service GetAllQueryDataRows(objRecordSet)

    DataRows    = ''

    If Len(objRecordSet) then
        BOF         = SRP_COM(objRecordSet, 'GET', 'BOF')
        EOF         = SRP_COM(objRecordSet, 'GET', 'EOF')
        If (EOF EQ False$) then
            DataRows    = SRP_COM(objRecordSet, 'CALL', 'GetString', 2, -1, @FM, @RM)
            If SRP_COM('', 'ERROR') then
                Error   = SRP_COM('', 'ERROR')
                Error_Services('Add', 'ADO error in the ' : Service : ' service: ' : SRP_COM('', 'ERROR'))
            end
            If Error_Services('NoError') then
                If DataRows[-1, 1] EQ @RM then DataRows[-1, 1] = ''
                If DataRows EQ objRecordSet then
                    DataRows    = ''
                    Error_Services('Add', 'Unknown ADO error in the ' : Service : ' service.')
                end else
                    If SRP_COM('', 'ERROR') then
                        Error   = SRP_COM('', 'ERROR')
                        If IndexC(Error, 'current record has been deleted', 1) then
                            DataRows    = ''
                        end else
                            Error_Services('Add', 'ADO error in the ' : Service : ' service: ' : SRP_COM('', 'ERROR'))
                        end
                    end
                end
            end
        end
    end else
        Error_Services('Add', 'The objRecordSet argument was missing in the ' : Service : ' service.')
    end

    Response = DataRows

end service


//----------------------------------------------------------------------------------------------------------------------
// GetServers
//
// Returns an @FM list of database servers.
//----------------------------------------------------------------------------------------------------------------------
Service GetServers()

    Servers  = ''
    hServers = Database_Services('GetTableHandle', 'SERVERS')
    If Error_Services('NoError') then
        Select hServers
        EOF = False$
        Loop
            Readnext ServerID else EOF = True$            
        Until EOF
            Servers    := ServerID : @FM
        Repeat
        Servers[-1, 1]  = ''
        Servers = SRP_Array('SortRows', Servers, 'AL1', 'LIST', @FM, @VM)
    end

    Response = Servers

end service


//----------------------------------------------------------------------------------------------------------------------
// SaveConnectionString
//
// ServerID         - Key ID to a row in the SERVERS database table.
// ConnectionString - SQL server connection string
//
// If a row in the SERVERS table with the key ServerID exists, updates the connection string, otherwise a new row
// within the SERVERS database table containing ConnectionString.
//----------------------------------------------------------------------------------------------------------------------
Service SaveConnectionString(ServerName, ConnectionString)

    If ServerName NE '' then
        ServerRec = Database_Services('ReadDataRow', 'SERVERS', ServerName)
        ServerRec<SERVERS.CONNECTION_STRING$> = ConnectionString
        Database_Services('WriteDataRow', 'SERVERS', ServerName, ServerRec, True$, False$, True$)
    end else
        ErrorMsg = 'Error saving connection. Null server name passed in to the ' : Service : ' service.'
        Error_Services('Add', ErrorMsg)
    end
    
end service


//----------------------------------------------------------------------------------------------------------------------
// CreateNewConnectionString
//
// Utilizes the Windows Data Link Properties utility to create a new connection string.
// Returns a field mark delimited array containing a suggested server name (the data source) and the connection string
// generated by the Windows Data Link Properties utility.
//----------------------------------------------------------------------------------------------------------------------
Service CreateNewConnectionString()

    Response         = ''
    ServerName       = ''
    ConnectionString = ''
    OleDBString      = ''
    Filename         = 'temp.udl'
    TempPath         = Str(\00\, 1024)
    GetTempPath(Len(TempPath), TempPath)
    Convert \00\ to '' in TempPath
    TempFilename = Str(\00\, 1024)
    GetTempFileName(TempPath, \00\, 0, TempFilename)
    OSWrite OleDBString to TempFilename
    UDLFile     = 'C:\Windows\syswow64\rundll32.exe "C:\Program Files (x86)\Common Files\System\Ole DB\oledb32.dll",OpenDSLFile ' : TempFilename
    Mode        = '-1'  ;* modal
    rv          = Utility('RUNWIN', UDLFile, Mode)       
    OSRead ConnectionString from TempFilename then
        If ConnectionString NE '' then
            Convert \FFFE\ to '' in ConnectionString
            ConnectionString = Unicode_UTF8(ConnectionString)
            DataSourcePos = Index(ConnectionString, 'Data Source', 1)
            ServerPos     = Index(ConnectionString, 'Server', 1)
            Begin Case
                Case DataSourcePos
                    ServerName = ConnectionString[(DataSourcePos + Len('Data Source') + 1), 'F;'] 
                Case ServerPos
                    ServerName = ConnectionString[(ServerPos + Len('Server') + 1), 'F;']                 
            End Case
        end
    end
    Response = ServerName : @FM : ConnectionString
    
end service


//----------------------------------------------------------------------------------------------------------------------
// EditConnection
//
// ServerID - Key ID for the SERVERS database table.
//
// Utilizes the Windows Data Link Properties utility to edit the connection string found in the SERVERS record
// associated with the ServerID key. 
//----------------------------------------------------------------------------------------------------------------------
Service EditConnection(ServerID)
    
    If ServerID NE '' then
        ServerRec = Database_Services('ReadDataRow', 'SERVERS', ServerID)
        If Error_Services('NoError') then
            ConnectionString = ServerRec<SERVERS.CONNECTION_STRING$>
            TempPath      = Str(\00\, 1024)
            GetTempPath(Len(TempPath), TempPath)
            TempFilename  = Str(\00\, 1024)
            GetTempFileName(TempPath, \00\, 0, TempFilename)
            If ConnectionString NE '' then
                UnicodeString = \FFFE\ : Str_Unicode(ConnectionString)
                OSWrite UnicodeString To TempFilename
            end else
                OSWrite '' to TempFileName
            end
            Program = 'C:\Windows\syswow64\rundll32.exe "C:\Program Files (x86)\Common Files\System\Ole DB\oledb32.dll",OpenDSLFile ':TempFilename
            Mode    = '-1'  ;* modal
            rv      = Utility("RUNWIN", Program, Mode)
            NewFile = ''
            OSRead NewConnectionString from TempFilename then           
                Convert \FFFE\ to '' in NewConnectionString
                NewConnectionString = Unicode_UTF8(NewConnectionString)
                ServerRec<SERVERS.CONNECTION_STRING$> = NewConnectionString
                Database_Services('WriteDataRow', 'SERVERS', ServerID, ServerRec, True$, False$, True$)
            end
        end
    end
    
end service


//----------------------------------------------------------------------------------------------------------------------
// DeleteConnection
//
// ServerID - Key ID for the SERVERS database table.
//
// Deletes the SERVERS record associated with the ServerID key.
//----------------------------------------------------------------------------------------------------------------------
Service DeleteConnection(ServerID)
    
    If ServerID NE '' then
        Database_Services('DeleteDataRow', 'SERVERS', ServerID, True$)
    end
    
end service


//----------------------------------------------------------------------------------------------------------------------
// SetConnectionDesc
//
// ServerID     - Key ID for the SERVERS database table.
// Description  - Description of the server connection
//
// Sets the DESCRIPTION column of the SERVERS record associated with the ServerID key.
//----------------------------------------------------------------------------------------------------------------------
Service SetConnectionDesc(ServerID, Description)
    
    If ServerID NE '' then
        ServerRec = Database_Services('ReadDataRow', 'SERVERS', ServerID)
        ServerRec<SERVERS.DESCRIPTION$> = Description
        Database_Services('WriteDataRow', 'SERVERS', ServerID, ServerRec)
    end
    
end service


//----------------------------------------------------------------------------------------------------------------------
// SetOITableToSQLMap
//
// TableName    - OpenInsight table being mapped. - [Required]
// SQLMap       - JSON object containing the OI Table to SQL Map information. - [Required]
//
// Updates the stored JSON object that defines the mapping between the indicated OpenInsight table and the SQL table.
//----------------------------------------------------------------------------------------------------------------------
Service SetOITableToSQLMap(TableName, SQLMap)

    Convert @Lower_Case to @Upper_Case in TableName
    ServiceKeyID    = ServiceModule : '*' : 'GetOITableToSQLMap' : '*' : TableName

    If (TableName NE '') AND (SQLMap NE '') then
        Convert \0D0A\ to @FM in SQLMap ; // Makes it easier to inspect. The GetOITableToSQLMap service removes the @FMs.
        Database_Services('WriteDataRow', 'DICT.' : TableName, '%SQLMap%', SQLMap, True$)
        If Error_Services('NoError') then
            Convert @FM to '' in SQLMap
            Memory_Services('SetValue', ServiceKeyID, SQLMap)
        end
    end else
        Error_Services('Add', 'The TableName or SQLMap argument was missing in the ' : Service : ' service.')
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// GetOITableToSQLMap
//
// TableName    - OpenInsight table being mapped. - [Required]
//
// Returns JSON object that defines the mapping between the indicated OpenInsight table and the SQL table.
//----------------------------------------------------------------------------------------------------------------------
Service GetOITableToSQLMap(TableName)

    Convert @Lower_Case to @Upper_Case in TableName

    ServiceKeyID   := '*' : TableName
    OITableToSQLMap = Memory_Services('GetValue', ServiceKeyID)

    If OITableToSQLMap EQ '' then
        If TableName NE '' then
            OITableToSQLMap = Database_Services('ReadDataRow', 'DICT.' : TableName, '%SQLMap%')
            Convert @FM to '' in OITableToSQLMap
            If OITableToSQLMap NE '' then
                Memory_Services('SetValue', ServiceKeyID, OITableToSQLMap)
            end
        end else
            Error_Services('Add', 'The TableName argument was missing in the ' : Service : ' service.')
        end
    end

    Response = OITableToSQLMap

end service


//----------------------------------------------------------------------------------------------------------------------
// SetOIColumnToSQLMap
//
// TableName    - OpenInsight table being mapped. - [Required]
// ColumnName   - Database column from the OpenInsight table. - [Required]
// SQLMap       - JSON object containing the OI Column to SQL Map information. - [Required]
//
// Updates the stored JSON object that defines the mapping between the indicated OpenInsight column and the SQL table.
//----------------------------------------------------------------------------------------------------------------------
Service SetOIColumnToSQLMap(TableName, ColumnName, SQLMap)

    Convert @Lower_Case to @Upper_Case in TableName
    Convert @Lower_Case to @Upper_Case in ColumnName
    ServiceKeyID    = ServiceModule : '*' : 'GetOIColumnToSQLMap' : '*' : TableName : '*' : ColumnName

    If (TableName NE '') AND (ColumnName NE '') AND (SQLMap NE '') then
        Convert @FM to '' in SQLMap
        Convert \0D0A\ to '' in SQLMap
        ColumnData              = Database_Services('ReadDataRow', 'DICT.' : TableName, ColumnName)
        ColumnData<DICT_USER1$> = SQLMap
        Database_Services('WriteDataRow', 'DICT.' : TableName, ColumnName, ColumnData, True$)
        If Error_Services('NoError') then
            Memory_Services('SetValue', ServiceKeyID, SQLMap)
        end
    end else
        Error_Services('Add', 'The TableName, ColumnName, or SQLMap argument was missing in the ' : Service : ' service.')
    end

end service


//----------------------------------------------------------------------------------------------------------------------
// GetAllOIColumnToSQLMaps
//
// TableName    - OpenInsight table being mapped. - [Required]
//
// Returns an @FM/@VM delimited array of all JSON objects that define the mapping being the table columns and their
// respective SQL table columns. Columns with no maps will return an empty string for the JSON object.
//----------------------------------------------------------------------------------------------------------------------
Service GetAllOIColumnToSQLMaps(TableName)

    Convert @Lower_Case to @Upper_Case in TableName
    ColumnSQLMaps   = ''

    If TableName NE '' then
        FieldArray      = Database_Services('ReadDataRow', 'DICT.' : TableName, '%FIELDS%')
        ColumnNames     = FieldArray<FIELDS_NAME$>
        For Each ColumnName in ColumnNames using @VM
            SQLMap      = SQL_Services('GetOIColumnToSQLMap', TableName, ColumnName)
            ColumnSQLMaps  := ColumnName : @VM
            If SQLMap NE '' then
                ColumnSQLMaps  := SQLMap
            end
            ColumnSQLMaps  := @FM
        Next ColumnName
        ColumnSQLMaps[-1, 1]    = ''
    end else
        Error_Services('Add', 'The TableName argument was missing in the ' : Service : ' service.')
    end

    Response = ColumnSQLMaps

end service


//----------------------------------------------------------------------------------------------------------------------
// GetOIColumnToSQLMap
//
// TableName    - OpenInsight table being mapped. - [Required]
// ColumnName   - Database column from the OpenInsight table. - [Required]
//
// Returns JSON object that defines the mapping between the indicated OpenInsight table column and the SQL table column.
//----------------------------------------------------------------------------------------------------------------------
Service GetOIColumnToSQLMap(TableName, ColumnName)

    Convert @Lower_Case to @Upper_Case in TableName
    Convert @Lower_Case to @Upper_Case in ColumnName
    ServiceKeyID       := '*' : TableName : '*' : ColumnName
    OIColumnToSQLMap    = Memory_Services('GetValue', ServiceKeyID)

    If OIColumnToSQLMap EQ '' then
        If (TableName NE '') AND (ColumnName NE '') then
            ColumnData          = Database_Services('ReadDataRow', 'DICT.' : TableName, ColumnName)
            OIColumnToSQLMap    = ColumnData<DICT_USER1$>
            If OIColumnToSQLMap NE '' then
                Memory_Services('SetValue', ServiceKeyID, OIColumnToSQLMap)
            end
        end else
            Error_Services('Add', 'The TableName or ColumnName argument was missing in the ' : Service : ' service.')
        end
    end

    Response = OIColumnToSQLMap

end service


//----------------------------------------------------------------------------------------------------------------------
// GetMappedSQLServer
//
// Returns the name of the SQL Server that the indicated OpenInsight table is mapped to.
//----------------------------------------------------------------------------------------------------------------------
Service GetMappedSQLServer(OITableName)

    Convert @Lower_Case to @Upper_Case in OITableName
    ServiceKeyID   := '*' : OITableName
    SQLServer       = Memory_Services('GetValue', ServiceKeyID)

    If SQLServer EQ '' then
        If OITableName NE '' then
            OITableToSQLMap = SQL_Services('GetOITableToSQLMap', OITableName)
            If SRP_JSON(MapObj, 'PARSE', OITableToSQLMap) EQ '' then
                SQLServer   = SRP_JSON(MapObj, 'GETVALUE', 'SQLServer.Name')
                SRP_JSON(MapObj, 'RELEASE')
            end
            If SQLServer NE '' then Memory_Services('SetValue', ServiceKeyID, SQLServer)
        end else
            Error_Services('Add', 'OITableName argument was missing in the ' : Service : ' service.')
        end
    end

    Response    = SQLServer

end service


//----------------------------------------------------------------------------------------------------------------------
// GetDatabases
//
// objConnection    - Connection Object (see GetConnectionObject service.) - [Optional, but Server is then Required]
// Server           - Name of a defined server. - [Optional, but objConnection is then Required]
//
// Returns an @FM list of databases available through the indicated connection object. If the connection object is
// empty, then a new one will be created (and then subsequently destroyed) for the indicated server.
//----------------------------------------------------------------------------------------------------------------------
Service GetDatabases(objConnection, Server)

    Databases   = ''

    If (objConnection NE '') OR (Server NE '') then
        If objConnection EQ '' then
            objConnection   = SQL_Services('GetConnectionObject', Server, 3)
            NewObject       = True$
        end else
            NewObject   = False$
        end

        If Error_Services('NoError') then
            SQLQuery    = "SELECT name FROM sys.databases WHERE name NOT IN ('master', 'tempdb', 'model', 'msdb')"
            Databases   = SQL_Services('ExecuteQuery', objConnection, SQLQuery, True$)
            If Databases EQ 0 then
                Databases = ''
                Error_Services('Add', 'Unknown error in the ' : Service : ' service for server ' : Server : '.')
            end else
                Convert @RM to @FM in Databases
            end
        end

        If NewObject EQ True$ then
            SQL_Services('DestroyConnectionObject', objConnection)
        end
    end else
        Error_Services('Add', 'objConnection and Server arguments were both missing in the ' : Service : ' service.')
    end

    Response = Databases

end service


//----------------------------------------------------------------------------------------------------------------------
// GetSchemas
//
// objConnection    - Connection Object (see GetConnectionObject service.) - [Optional, but Server is then Required]
// Server           - Name of a defined server. - [Optional, but objConnection is then Required]
// Database         - Name of a specific database to use. If empty, then all databases will be used. - [Required]
//
// Returns an @FM list of schemas available for the indicated database. If the connection object is empty, then a new
// one will be created (and then subsequently destroyed) for the indicated server.
//----------------------------------------------------------------------------------------------------------------------
Service GetSchemas(objConnection, Server, Database)

    Schemas     = ''

    If Database NE '' then
        If (objConnection NE '') OR (Server NE '') then
            If objConnection EQ '' then
                objConnection   = SQL_Services('GetConnectionObject', Server, 3)
                NewObject       = True$
            end else
                NewObject   = False$
            end

            If Error_Services('NoError') then
                SQLQuery    = "SELECT DISTINCT TABLE_SCHEMA FROM [" : Database : "].INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE'"
                Schemas     = SQL_Services('ExecuteQuery', objConnection, SQLQuery, True$)
                If Error_Services('NoError') then
                    Convert @RM to @FM in Schemas
                end else
                    Message     = Error_Services('GetMessage')
                    If IndexC(Message, 'not able to access the database', 1) AND IndexC(Message, 'under the current security context', 1) then
                        // Users does not have permission. Clear the error rather than report it.
                        Error_Services('Clear')
                    end
                end
            end

            If NewObject EQ True$ then
                ErrorMessage    = Error_Services('GetMessage')
                SQL_Services('DestroyConnectionObject', objConnection)
                If ErrorMessage NE '' then Error_Services('Add', ErrorMessage)
            end
        end else
            Error_Services('Add', 'objConnection and Server arguments were both missing in the ' : Service : ' service.')
        end
    end else
        Error_Services('Add', 'Database argument was missing in the ' : Service : ' service.')
    end

    Response    = Schemas

end service


//----------------------------------------------------------------------------------------------------------------------
// GetTables
//
// objConnection        - Connection Object (see GetConnectionObject service.) - [Optional, but Server is then Required]
// Server               - Name of a defined server. - [Optional, but objConnection is then Required]
// Databases            - @FM delimited list of databases to use. If empty, then all databases will be used.
//                        - [Optional]
// Schemas              - @FM delimited list of schemas to use. If empty, then all schemas will be used. If Databases is
//                        empty, then Schemas will be ignored. - [Optional]
// ReturnFullObjects    - Boolean flag to indicate if full object notation (e.g., Server.Database.Schema.Table) should
//                        be returned or just the tables. Default is False$ - [Optional]
// BracketObjects       - Boolean flag to indicate if the objects being returned should be bracketed in case some of the
//                        object names are reserved words. Default is False$ - [Optional]
//
// Returns an @FM list of tables using full object name formatting (i.e., Database.Schema.Table) available for the
// indicated connection object (or server). If the connection object is empty, then a new one will be created (and then
// subsequently destroyed) for the indicated server.
//----------------------------------------------------------------------------------------------------------------------
Service GetTables(objConnection, Server, Databases, Schemas, IncludeServerObject=BOOLEAN, IncludeDatabaseObject=BOOLEAN, IncludeSchemaObject=BOOLEAN, BracketObjects=BOOLEAN)

    Tables  = ''

    If IncludeServerObject NE True$     then IncludeServerObject    = False$
    If IncludeDatabaseObject NE True$   then IncludeDatabaseObject  = False$
    If IncludeSchemaObject NE True$     then IncludeSchemaObject    = False$
    If BracketObjects NE True$          then BracketObjects         = False$
    If Databases EQ ''                  then Schemas                = ''

    If (objConnection NE '') OR (Server NE '') then
        If objConnection EQ '' then
            objConnection   = SQL_Services('GetConnectionObject', Server, 3)
            NewObject       = True$
        end else
            NewObject   = False$
        end

        If Error_Services('NoError') then
            If Databases EQ '' then Databases = SQL_Services('GetDatabases', objConnection, Server)
*            If @USER4 EQ 'DANIEL_ST' then
*                debug
*                ConnectionState = Get_Property.Net(objConnection, "State")
*            end
            If Error_Services('NoError') AND (Databases NE '') then
                For Each Database in Databases using @FM
                    If Schemas EQ '' then Schemas = SQL_Services('GetSchemas', objConnection, Server, Database)
*                    If @USER4 EQ 'DANIEL_ST' then
*                        debug
*                        ConnectionState = Get_Property.Net(objConnection, "State")
*                    end
                    If UseRevDotNet EQ True$ and Schemas EQ 0 then Schemas = ''
                    If Error_Services('NoError') AND (Schemas NE '') then
                        For Each Schema in Schemas using @FM
                            SQLQuery    = "SELECT TABLE_NAME FROM [" : Database : "].INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='" : Schema : "'"
                            TableNames  = SQL_Services('ExecuteQuery', objConnection, SQLQuery, True$)
                            Convert @RM to @FM in TableNames
                            If Error_Services('NoError') AND (TableNames NE '') then
                                If (IncludeServerObject EQ True$) OR (IncludeDatabaseObject EQ True$) OR (IncludeSchemaObject EQ True$) then
                                    For Each Table in TableNames using @FM
                                        TableObject = Table
                                        If IncludeSchemaObject EQ True$ then
                                            TableObject = Schema : '.' : TableObject
                                            If IncludeDatabaseObject EQ True$ then
                                                TableObject = Database : '.' : TableObject
                                                If IncludeServerObject EQ True$ then
                                                    TableObject = Server : '.' : TableObject
                                                end
                                            end
                                        end
                                        Tables := TableObject : @FM
                                    Next Table
                                end else
                                    Tables := TableNames : @FM
                                end
                            end
                        Next Schema
                        Schemas = ''
                    end
                Next Database
            end
            Tables[-1, 1]  = ''
            If (Tables NE '') AND (BracketObjects EQ True$) then
                Swap '.' with '].[' in Tables
                Swap @FM with ']' : @FM : '[' in Tables
                Tables = '[' : Tables : ']'
                Tables = SRP_Array('SortRows', Tables, 'AL1', 'LIST', @FM, @VM)
            end
        end

        If NewObject EQ True$ then
            ErrorMessage    = Error_Services('GetMessage')
            SQL_Services('DestroyConnectionObject', objConnection)
            If ErrorMessage NE '' then Error_Services('Add', ErrorMessage)
        end
    end else
        Error_Services('Add', 'objConnection and Server arguments were both missing in the ' : Service : ' service.')
    end

    Response    = Tables

end service


//----------------------------------------------------------------------------------------------------------------------
// GetTableColumns
//
// objConnection    - Connection Object (see GetConnectionObject service.) - [Optional, but Server is then Required]
// Server           - Name of a defined server. - [Optional, but objConnection is then Required]
// Database         - Database object for the table. - [Optional]
// Schema           - Schema object for the table. - [Optional]
// Table            - Database Table whose column data is being requested. The Table argument might have the Server,
//                    Database, and/or Schema objects embedded. If so, then these will override any values specified
//                    in the Server, Database, or Schema arguments. - [Required]
//
// Returns an @FM/@VM array of database column information.
//----------------------------------------------------------------------------------------------------------------------
Service GetTableColumns(objConnection, Server, Database, Schema, Table)

    TableColumns    = ''

    If Table NE '' then
        Convert '[]' to '' in Table ; // Remove any brackets so the query won't fail.
        If Index(Table, '.', 1) then
            // Table came formatted with other objects. Parse these out.
            NumObjects  = DCount(Table, '.')
            Begin Case
                Case NumObjects EQ 4
                    Server      = Table[1, '.']
                    Database    = Table[Col2() + 1, '.']
                    Schema      = Table[Col2() + 1, '.']
                    Table       = Table[Col2() + 1, '.']
                Case NumObjects EQ 3
                    Database    = Table[1, '.']
                    Schema      = Table[Col2() + 1, '.']
                    Table       = Table[Col2() + 1, '.']
                Case NumObjects EQ 2
                    Schema      = Table[1, '.']
                    Table       = Table[Col2() + 1, '.']
            End Case
        end

        If (objConnection NE '') OR (Server NE '') then
            If objConnection EQ '' then
                objConnection   = SQL_Services('GetConnectionObject', Server, 3)
                NewObject       = True$
            end else
                NewObject   = False$
            end

            If Error_Services('NoError') then
                ErrorMessage    = ''

                // This statement allows us to return PK and Null constraints.
                
                If Database NE '' then
                    SQL_Services('ExecuteQuery', objConnection, "USE " : Database, False$)
                end
                If Error_Services('NoError') then
                    QueryStatement  = ""
                    QueryStatement := "SELECT "
                    QueryStatement := "    c.name 'Column Name', "
                    QueryStatement := "    t.Name 'Data type', "
                    QueryStatement := "    c.max_length 'Max Length', "
                    QueryStatement := "    c.is_nullable, "
                    QueryStatement := "    ISNULL(i.is_primary_key, 0) 'Primary Key' "
                    QueryStatement := "FROM    "
                    QueryStatement := "    sys.columns c "
                    QueryStatement := "INNER JOIN "
                    QueryStatement := "    sys.types t ON c.user_type_id = t.user_type_id "
                    QueryStatement := "LEFT OUTER JOIN "
                    QueryStatement := "    sys.index_columns ic ON ic.object_id = c.object_id AND ic.column_id = c.column_id "
                    QueryStatement := "LEFT OUTER JOIN "
                    QueryStatement := "    sys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id "
                    QueryStatement := "WHERE"
                    QueryStatement := "    c.object_id = OBJECT_ID('"
                    If Database NE '' then QueryStatement := "[" : Database : "]."
                    If Schema NE '' then QueryStatement := "[" : Schema : "]."
                    QueryStatement := "[" : Table : "]')"

                    TableColumns    = SQL_Services('ExecuteQuery', objConnection, QueryStatement, True$)
                    If Error_Services('NoError') then
                        Convert @FM to @VM in TableColumns
                        Convert @RM to @FM in TableColumns
                        TableColumns    = SRP_Array('SortRows', TableColumns, 'DR5' : @FM : 'AL1', 'LIST', @FM, @VM)
                    end else
                        ErrorMessage    = Error_Services('GetMessage')
                    end
                    SQL_Services('DestroyConnectionObject', objConnection)
                    If ErrorMessage NE '' then Error_Services('Add', ErrorMessage)
                end
            end

            If NewObject EQ True$ then
                ErrorMessage    = Error_Services('GetMessage')
                SQL_Services('DestroyConnectionObject', objConnection)
                If ErrorMessage NE '' then Error_Services('Add', ErrorMessage)
            end
        end else
            Error_Services('Add', 'The Server or Table argument was missing in the ' : Service : ' service.')
        end
    end else
        Error_Services('Add', 'Table argument was missing in the ' : Service : ' service.')
    end
    Response = TableColumns

end service


//----------------------------------------------------------------------------------------------------------------------
// GetPrimarySQLTable
//
// OITableName  - The name of the OpenInsight table that is mapped to the primary SQL table. - [Required]
//
// Returns the name of the primary SQL table that the indicated OpenInsight table is mapped to.
//----------------------------------------------------------------------------------------------------------------------
Service GetPrimarySQLTable(OITableName)

    Convert @Lower_Case to @Upper_Case in OITableName
    ServiceKeyID   := '*' : OITableName
    PrimarySQLTable = Memory_Services('GetValue', ServiceKeyID)

    If PrimarySQLTable EQ '' then
        If OITableName NE '' then
            OITableToSQLMap = SQL_Services('GetOITableToSQLMap', OITableName)
            If SRP_JSON(MapObj, 'PARSE', OITableToSQLMap) EQ '' then
                PrimarySQLTable = SRP_JSON(MapObj, 'GETVALUE', 'PrimarySQLTable.Name')
                SRP_JSON(MapObj, 'RELEASE')
            end
            If PrimarySQLTable NE '' then Memory_Services('SetValue', ServiceKeyID, PrimarySQLTable)
        end else
            Error_Services('Add', 'The OITableName argument was missing in the ' : Service : ' service.')
        end
    end

    Response    = PrimarySQLTable

end service


//----------------------------------------------------------------------------------------------------------------------
// GetSecondarySQLTables
//
// OITableName  - The name of the OpenInsight table that is mapped to secondary SQL tables. - [Required]
//
// Returns an @FM delimited list of the secondary SQL tables that the indicated OpenInsight table is mapped to.
//----------------------------------------------------------------------------------------------------------------------
Service GetSecondarySQLTables(OITableName)

    Convert @Lower_Case to @Upper_Case in OITableName
    ServiceKeyID       := '*' : OITableName
    SecondarySQLTables  = Memory_Services('GetValue', ServiceKeyID)

    If SecondarySQLTables EQ '' then
        If OITableName NE '' then
            OITableToSQLMap = SQL_Services('GetOITableToSQLMap', OITableName)
            If SRP_JSON(MapObj, 'PARSE', OITableToSQLMap) EQ '' then
                TablesArray = SRP_JSON(MapObj, 'GET', 'SecondarySQLTables')
                NumTables   = SRP_JSON(TablesArray, 'GETCOUNT')
                MatchFound  = False$
                For TableNum = 1 to NumTables
                    SecondarySQLTables := SRP_JSON(TablesArray, 'GETVALUE', '[' : TableNum : '].Name', '') : @FM
                Until MatchFound
                Next TableNum
                SRP_JSON(TablesArray, 'RELEASE')
                SRP_JSON(MapObj, 'RELEASE')
            end
            SecondarySQLTables[-1, 1]   = ''
            If SecondarySQLTables NE '' then Memory_Services('SetValue', ServiceKeyID, SecondarySQLTables)
        end else
            Error_Services('Add', 'The OITableName argument was missing in the ' : Service : ' service.')
        end
    end

    Response    = SecondarySQLTables

end service


//----------------------------------------------------------------------------------------------------------------------
// GetStaticSQLColumnNames
//
// OITableName  - The name of the OpenInsight table that columns mapped to a SQL table with static values. - [Required]
//
// Returns an @FM delimited list of SQL column names that have a static value. These are usually used in a SQL insert
// statement along with the mapped columns.
//----------------------------------------------------------------------------------------------------------------------
Service GetStaticSQLColumnNames(OITableName)

    Convert @Lower_Case to @Upper_Case in OITableName
    ServiceKeyID           := '*' : OITableName
    StaticSQLColumnNames    = Memory_Services('GetValue', ServiceKeyID)

    If StaticSQLColumnNames EQ '' then
        If (OITableName NE '') then
            // Add all OpenInsight columns defined in the SQL Join Clause.
            OITableToSQLMap = SQL_Services('GetOITableToSQLMap', OITableName)
            If SRP_JSON(MapObj, 'PARSE', OITableToSQLMap) EQ '' then
                StaticColumnsArray  = SRP_JSON(MapObj, 'GET', 'StaticColumns')
                NumColumns          = SRP_JSON(StaticColumnsArray, 'GETCOUNT')
                For ColumnNum = 1 to NumColumns
                    objStaticColumn         = SRP_JSON(StaticColumnsArray, 'GET', '[' : ColumnNum : ']')
                    StaticSQLColumnNames   := SRP_JSON(objStaticColumn, 'GETVALUE', 'SQLColumnName') : @FM
                    SRP_JSON(objStaticColumn, 'RELEASE')
                Next NumColumns
                StaticSQLColumnNames[-1, 1] = ''
                SRP_JSON(StaticColumnsArray, 'RELEASE')
                SRP_JSON(MapObj, 'RELEASE')
            end
            If StaticSQLColumnNames NE '' then Memory_Services('SetValue', ServiceKeyID, StaticSQLColumnNames)
        end else
            Error_Services('Add', 'The OITableName argument was missing in the ' : Service : ' service.')
        end
    end

    Response    = StaticSQLColumnNames

end service


//----------------------------------------------------------------------------------------------------------------------
// GetStaticSQLColumnValues
//
// OITableName  - The name of the OpenInsight table that columns mapped to a SQL table with static values. - [Required]
//
// Returns an @FM delimited list of values for SQL column names that have a static value. These are usually used in a
// SQL insert statement along with the mapped columns.
//----------------------------------------------------------------------------------------------------------------------
Service GetStaticSQLColumnValues(OITableName)

    Convert @Lower_Case to @Upper_Case in OITableName
    ServiceKeyID           := '*' : OITableName
    StaticSQLColumnValues   = Memory_Services('GetValue', ServiceKeyID)

    If StaticSQLColumnValues EQ '' then
        If (OITableName NE '') then
            // Add all OpenInsight columns defined in the SQL Join Clause.
            OITableToSQLMap = SQL_Services('GetOITableToSQLMap', OITableName)
            If SRP_JSON(MapObj, 'PARSE', OITableToSQLMap) EQ '' then
                StaticColumnsArray  = SRP_JSON(MapObj, 'GET', 'StaticColumns')
                NumColumns          = SRP_JSON(StaticColumnsArray, 'GETCOUNT')
                For ColumnNum = 1 to NumColumns
                    objStaticColumn         = SRP_JSON(StaticColumnsArray, 'GET', '[' : ColumnNum : ']')
                    StaticValue             = SRP_JSON(objStaticColumn, 'GETVALUE', 'StaticValue')
                    StaticValue             = "'" : StaticValue : "'"
                    StaticSQLColumnValues  := StaticValue : @FM
                    SRP_JSON(objStaticColumn, 'RELEASE')
                Next NumColumns
                StaticSQLColumnValues[-1, 1]    = ''
                SRP_JSON(StaticColumnsArray, 'RELEASE')
                SRP_JSON(MapObj, 'RELEASE')
            end
            If StaticSQLColumnValues NE '' then Memory_Services('SetValue', ServiceKeyID, StaticSQLColumnValues)
        end else
            Error_Services('Add', 'The OITableName argument was missing in the ' : Service : ' service.')
        end
    end

    Response    = StaticSQLColumnValues

end service


//----------------------------------------------------------------------------------------------------------------------
// GetAMVMaster
//
// OITableName  - The name of the OpenInsight table that is mapped to the SQL table. - [Required]
// SQLTableName - The name of the SQL table that the Openinsight table is mapped to. - [Required]
//
// Returns the name of the OpenInsight column used as the AMV (associated multivalue) master used to determine the
// number of values needed to write into the secondary SQL table.
//----------------------------------------------------------------------------------------------------------------------
Service GetAMVMaster(OITableName, SQLTableName)

    Convert @Lower_Case to @Upper_Case in OITableName

    ServiceKeyID  := '*' : OITableName : '*' : SQLTableName
    AMVMaster      = Memory_Services('GetValue', ServiceKeyID)

    If AMVMaster EQ '' then
        If (OITableName NE '') AND (SQLTableName NE '') then
            OITableToSQLMap = SQL_Services('GetOITableToSQLMap', OITableName)
            JoinArray       = ''    ; // Initialize the array.
            If SRP_JSON(MapObj, 'Parse', OITableToSQLMap) EQ '' then
                PrimarySQLTableName = SRP_JSON(MapObj, 'GetValue', 'PrimarySQLTable.Name')
                If SQLTableName _EQC PrimarySQLTableName then
                    // Primary SQL tables do not have a Seq Key. Just return the empty string.
                end else
                    // Look for a matching secondary SQL table.
                    TableArray = SRP_JSON(MapObj, 'Get', 'SecondarySQLTables')
                    TableObjs  = SRP_JSON(TableArray, 'GetElements')
                    TableFound = False$
                    For Each TableObj in TableObjs using @FM
                        SecondarySQLTableName   = SRP_JSON(TableObj, 'GETVALUE', 'Name', '')
                        If SQLTableName _EQC SecondarySQLTableName then
                            JoinClassArray = SRP_JSON(TableObj, 'Get', 'JoinClause')
                            JoinClassObjs  = SRP_JSON(JoinClassArray, 'GetElements')
                            For Each JoinClassObj in JoinClassObjs using @FM
                                If SRP_JSON(JoinClassObj, 'GetValue', 'AMVMaster') EQ True$ then
                                    AMVMaster     = SRP_JSON(JoinClassObj, 'GetValue', 'OIColumnName')
                                end
                                SRP_JSON(JoinClassObj, 'Release')
                            Until AMVMaster NE ''
                            Next JoinClassObj
                            TableFound = True$
                            SRP_JSON(JoinClassArray, 'Release')
                        end
                        SRP_JSON(TableObj, 'Release')
                    Until TableFound
                    Next TableObj
                    SRP_JSON(TableArray, 'Release')
                end
                SRP_JSON(MapObj, 'Release')
            end

            If AMVMaster NE '' then
                Memory_Services('SetValue', ServiceKeyID, AMVMaster)
            end
        end else
            Error_Services('Add', 'The OITableName or SQLTableName argument was missing in the ' : Service : ' service.')
        end
    end

    Response    = AMVMaster

end service


//----------------------------------------------------------------------------------------------------------------------
// GetMappedJoinedOIColumnNames
//
// OITableName      - The name of the OpenInsight table that is mapped to the SQL table. - [Required]
// SQLTableName     - The name of the SQL table that the Openinsight table is mapped to. - [Required]
// ValuesReturned   - A return varabiable that will contain the type of values each associated column should return.
//                    See below for more information. - [Optional]
//
// Returns an @FM list of column names from the indicated OpenInsight table that are used to join with the indicated
// SQL table. This service will also populate the ValuesReturned variable with the intended value to be returned by the
// associated column (.e.g, Column Data or Index Position).
//----------------------------------------------------------------------------------------------------------------------
Service GetMappedJoinedOIColumnNames(OITableName, SQLTableName, Ref ValuesReturned)

    Convert @Lower_Case to @Upper_Case in OITableName

    ServiceKeyID   := '*' : OITableName : '*' : SQLTableName
    ColumnNames     = Memory_Services('GetValue', ServiceKeyID)
    ValuesReturned  = Memory_Services('GetValue', ServiceKeyID : '*ValuesReturned')

    If ColumnNames EQ '' then
        If (OITableName NE '') AND (SQLTableName NE '') then
            OITableToSQLMap = SQL_Services('GetOITableToSQLMap', OITableName)
            JoinArray       = ''    ; // Initialize the array.
            If SRP_JSON(MapObj, 'PARSE', OITableToSQLMap) EQ '' then
                PrimarySQLTableName = SRP_JSON(MapObj, 'GETVALUE', 'PrimarySQLTable.Name')
                If SQLTableName _EQC PrimarySQLTableName then
                    // Use the join array from the primary SQL Table.
                    JoinArray   = SRP_JSON(MapObj, 'GET', 'PrimarySQLTable.JoinClause')
                end else
                    // Look for a matching secondary SQL table.
                    TablesArray = SRP_JSON(MapObj, 'GET', 'SecondarySQLTables')
                    NumTables   = SRP_JSON(TablesArray, 'GETCOUNT')
                    MatchFound  = False$
                    For TableNum = 1 to NumTables
                        TableObj                = SRP_JSON(TablesArray, 'GET', '[' : TableNum : ']')
                        SecondarySQLTableName   = SRP_JSON(TableObj, 'GETVALUE', 'Name', '')
                        If SQLTableName _EQC SecondarySQLTableName then
                            JoinArray       = SRP_JSON(TableObj, 'GET', 'JoinClause')
                            MatchFound      = True$
                        end
                        SRP_JSON(TableObj, 'RELEASE')
                    Until MatchFound
                    Next TableNum
                    SRP_JSON(TablesArray, 'RELEASE')
                end
                SRP_JSON(MapObj, 'RELEASE')
            end

            If JoinArray GT 0 then
                NumColumns  = SRP_JSON(JoinArray, 'GETCOUNT')
                For ColumnNo = 1 to NumColumns
                    OIColumnName    = SRP_JSON(JoinArray, 'GETVALUE', '[' : ColumnNo : ']' : 'OIColumnName', '')
                    ValueReturned   = SRP_JSON(JoinArray, 'GETVALUE', '[' : ColumnNo : ']' : 'ValueReturned', '')
                    ColumnNames    := OIColumnName : @FM
                    ValuesReturned := ValueReturned : @FM
                Next ColumnNo
                ColumnNames[-1, 1]  = ''
                ValuesReturned[-1, 1]   = ''
                SRP_JSON(JoinArray, 'RELEASE')
            end

            If ColumnNames NE '' then
                Memory_Services('SetValue', ServiceKeyID, ColumnNames)
                Memory_Services('SetValue', ServiceKeyID : '*ValuesReturned', ValuesReturned)
            end
        end else
            Error_Services('Add', 'The OITableName or SQLTableName argument was missing in the ' : Service : ' service.')
        end
    end

    Response    = ColumnNames

end service


//----------------------------------------------------------------------------------------------------------------------
// GetMappedJoinedSQLColumnNames
//
// OITableName      - The name of the OpenInsight table that is mapped to the SQL table. - [Required]
// SQLTableName     - The name of the SQL table that the Openinsight table is mapped to. - [Required]
//
// Returns an @FM list of column names from the indicated SQL table that are used to join with the indicated OpenInsight
// table.
//----------------------------------------------------------------------------------------------------------------------
Service GetMappedJoinedSQLColumnNames(OITableName, SQLTableName)

    Convert @Lower_Case to @Upper_Case in OITableName

    ServiceKeyID   := '*' : OITableName : '*' : SQLTableName
    ColumnNames     = Memory_Services('GetValue', ServiceKeyID)

    If ColumnNames EQ '' then
        If (OITableName NE '') AND (SQLTableName NE '') then
            OITableToSQLMap = SQL_Services('GetOITableToSQLMap', OITableName)
            JoinArray       = ''    ; // Initialize the array.
            If SRP_JSON(MapObj, 'PARSE', OITableToSQLMap) EQ '' then
                PrimarySQLTableName = SRP_JSON(MapObj, 'GETVALUE', 'PrimarySQLTable.Name')
                If SQLTableName _EQC PrimarySQLTableName then
                    // Use the join array from the primary SQL Table.
                    JoinArray   = SRP_JSON(MapObj, 'GET', 'PrimarySQLTable.JoinClause')
                end else
                    // Look for a matching secondary SQL table.
                    TablesArray = SRP_JSON(MapObj, 'GET', 'SecondarySQLTables')
                    NumTables   = SRP_JSON(TablesArray, 'GETCOUNT')
                    MatchFound  = False$
                    For TableNum = 1 to NumTables
                        TableObj                = SRP_JSON(TablesArray, 'GET', '[' : TableNum : ']')
                        SecondarySQLTableName   = SRP_JSON(TableObj, 'GETVALUE', 'Name', '')
                        If SQLTableName _EQC SecondarySQLTableName then
                            JoinArray       = SRP_JSON(TableObj, 'GET', 'JoinClause')
                            MatchFound      = True$
                        end
                        SRP_JSON(TableObj, 'RELEASE')
                    Until MatchFound
                    Next TableNum
                    SRP_JSON(TablesArray, 'RELEASE')
                end
                SRP_JSON(MapObj, 'RELEASE')
            end

            If JoinArray GT 0 then
                NumColumns  = SRP_JSON(JoinArray, 'GETCOUNT')
                For ColumnNo = 1 to NumColumns
                    SQLColumnName   = SRP_JSON(JoinArray, 'GETVALUE', '[' : ColumnNo : ']' : 'SQLColumnName', '')
                    ColumnNames    := SQLColumnName : @FM
                Next ColumnNo
                ColumnNames[-1, 1]  = ''
                SRP_JSON(JoinArray, 'RELEASE')
            end

            If ColumnNames NE '' then
                Memory_Services('SetValue', ServiceKeyID, ColumnNames)
            end
        end else
            Error_Services('Add', 'The OITableName or SQLTableName argument was missing in the ' : Service : ' service.')
        end
    end

    Response    = ColumnNames

end service


//----------------------------------------------------------------------------------------------------------------------
// GetMappedJoinedOperators
//
// OITableName      - The name of the OpenInsight table that is mapped to the SQL table. - [Required]
// SQLTableName     - The name of the SQL table that the Openinsight table is mapped to. - [Required]
//
// Returns an @FM list of operators from the indicated SQL table that are used to join with the indicated OpenInsight
// table.
//----------------------------------------------------------------------------------------------------------------------
Service GetMappedJoinedOperators(OITableName, SQLTableName)

    Convert @Lower_Case to @Upper_Case in OITableName

    ServiceKeyID   := '*' : OITableName : '*' : SQLTableName
    Operators       = Memory_Services('GetValue', ServiceKeyID)

    If Operators EQ '' then
        If (OITableName NE '') AND (SQLTableName NE '') then
            OITableToSQLMap = SQL_Services('GetOITableToSQLMap', OITableName)
            JoinArray       = ''    ; // Initialize the array.
            If SRP_JSON(MapObj, 'PARSE', OITableToSQLMap) EQ '' then
                PrimarySQLTableName = SRP_JSON(MapObj, 'GETVALUE', 'PrimarySQLTable.Name')
                If SQLTableName _EQC PrimarySQLTableName then
                    // Use the join array from the primary SQL Table.
                    JoinArray   = SRP_JSON(MapObj, 'GET', 'PrimarySQLTable.JoinClause')
                end else
                    // Look for a matching secondary SQL table.
                    TablesArray = SRP_JSON(MapObj, 'GET', 'SecondarySQLTables')
                    NumTables   = SRP_JSON(TablesArray, 'GETCOUNT')
                    MatchFound  = False$
                    For TableNum = 1 to NumTables
                        TableObj                = SRP_JSON(TablesArray, 'GET', '[' : TableNum : ']')
                        SecondarySQLTableName   = SRP_JSON(TableObj, 'GETVALUE', 'Name', '')
                        If SQLTableName _EQC SecondarySQLTableName then
                            JoinArray       = SRP_JSON(TableObj, 'GET', 'JoinClause')
                            MatchFound      = True$
                        end
                        SRP_JSON(TableObj, 'RELEASE')
                    Until MatchFound
                    Next TableNum
                    SRP_JSON(TablesArray, 'RELEASE')
                end
                SRP_JSON(MapObj, 'RELEASE')
            end

            If JoinArray GT 0 then
                NumColumns  = SRP_JSON(JoinArray, 'GETCOUNT')
                For ColumnNo = 1 to NumColumns
                    Operator    = SRP_JSON(JoinArray, 'GETVALUE', '[' : ColumnNo : ']' : 'Operator', '')
                    Operators  := Operator : @FM
                Next ColumnNo
                Operators[-1, 1]    = ''
                SRP_JSON(JoinArray, 'RELEASE')
            end

            If Operators NE '' then
                Memory_Services('SetValue', ServiceKeyID, Operators)
            end
        end else
            Error_Services('Add', 'The OITableName or SQLTableName argument was missing in the ' : Service : ' service.')
        end
    end

    Response    = Operators

end service


//----------------------------------------------------------------------------------------------------------------------
// GetMappedJoinedSQLColumnValues
//
// OITableName  - The name of the OpenInsight table that is mapped to the SQL table to. - [Required]
// SQLTableName - The name of the SQL table that the Openinsight table is mapped to. - [Required]
// OIKeyID      - The Key ID to the OpenInsight row being written to SQL. - [Required]
// OIRow        - The OpenInsight row being written to SQL. - [Optional]
// AMVPosition  - The position within the multivalue column that should be used in the join clause if the OpenInsight
//                column is supposed to use its Index Position. - [Optional]
//
// Returns an @FM delimited list of SQL column values for the indicated table that are derived from the indicated
// OpenInsight table and associated joined columns. This service is normally called to create a fully formed Insert
// query statement. Note: OIRow will likely be an empty string since most join clauses will be based on the OpenInsight
// Key ID.
//----------------------------------------------------------------------------------------------------------------------
Service GetMappedJoinedSQLColumnValues(OITableName, SQLTableName, OIKeyID, OIRow, AMVPosition)

    Convert @Lower_Case to @Upper_Case in OITableName

    If Num(AMVPosition) else AMVPosition = ''

    ColumnValues    = ''

    If (OITableName NE '') AND (SQLTableName NE '') AND (OIKeyID NE '') then
        FieldArray      = Database_Services('ReadDataRow', 'DICT.' : OITablename, '%FIELDS%')
        OIColumnNames   = SQL_Services('GetMappedJoinedOIColumnNames', OITableName, SQLTableName, ValuesReturned)
        If Error_Services('NoError') then
            If OIColumnNames NE '' then
                For Each OIColumnName in OIColumnNames using @FM setting fPos
                    ValueReturned = ValuesReturned<fPos>
                    If ValueReturned EQ '' then ValueReturned = 'Column Data'
                    Locate OIColumnName in FieldArray<FIELDS_NAME$> using @VM setting vPos then
                        OIColumnToSQLMap    = SQL_Services('GetOIColumnToSQLMap', OITableName, OIColumnName)
                        If Error_Services('NoError') then
                            If SRP_JSON(MapObj, 'PARSE', OIColumnToSQLMap) EQ '' then
                                SQLColumnType   = SRP_JSON(MapObj, 'GETVALUE', 'SQLColumnType')
                                OIColumnType    = FieldArray<FIELDS_TYPE$, vPos>
                                IsOIKey         = False$    ; // Assume false for now.
                                If ValueReturned EQ 'Column Data' then
                                    If OIColumnType EQ 'F' then
                                        OIColumnPos = FieldArray<FIELDS_FIELD_NO$, vPos>
                                        If OIColumnPos GT 0 then
                                            OIColumnValue   = OIRow<OIColumnPos>
                                        end else
                                            IsOIKey         = True$
                                            OIKeyPart       = FieldArray<FIELDS_PART$, vPos>
                                            OIColumnValue   = Field(OIKeyID, '*', OIKeyPart, 1)
                                            If OIColumnValue EQ '' then
                                                Error_Services('Add', 'Key ID Column ' : OIColumnName : ' was assigned an empty string value in ' : Service : '.')
                                            end
                                        end
                                    end else
                                        // OIColumnValue   = Calculate(OIColumnName)    ; // Actual logic used in MFS
                                        OIColumnValue   = Xlate(OITablename, OIKeyID, OIColumnName, 'X')
                                    end
                                    If (AMVPosition NE '') AND (IsOIKey NE True$) then
                                        OIColumnValue   = OIColumnValue<0, AMVPosition>
                                    end
                                end else
                                    If AMVPosition EQ '' then AMVPosition = 1
                                    OIColumnValue   = AMVPosition
                                end
                                If ValueReturned EQ 'Column Data' then
                                    // Make applicable data conversions.
                                    Begin Case
                                        Case OIColumnValue EQ ''
                                            // Empty strings are treated as NULL values.
                                            OIColumnValue   = 'NULL'
                                        Case InList('varchar,nvarchar,char,nchar', SQLColumnType, ',')
                                            // Single quote the value.
                                            SQLColumnLength = SRP_JSON(MapObj, 'GETVALUE', 'SQLColumnLength')
                                            OIColumnConv    = FieldArray<FIELDS_CONV$, vPos>
                                            If OIColumnConv NE '' then
                                                OIColumnValue   = Fmt(OIColumnValue, OIColumnConv)
                                            end
                                            If (SQLColumnLength EQ '') OR (SQLColumnLength EQ '-1') then SQLColumnLength = Len(OIColumnValue)
                                            OIColumnValue   = OIColumnValue[1, SQLColumnLength, 1]
                                            Swap "'" with "''" in OIColumnValue
                                            OIColumnValue   = "'" : OIColumnValue : "'"
                                        Case InList('tinyint,smallint,bigint,int', SQLColumnType, ',')
                                            // Treat as an integer.
                                            OIColumnConv    = 'MD0'
                                            If OIColumnConv NE '' then
                                                OIColumnValue   = Fmt(OIColumnValue, OIColumnConv)
                                            end
                                        Case InList('decimal,numeric,float,real', SQLColumnType, ',')
                                            // Treat as a floating point number using any OI column output conversion if it
                                            // exists.
                                            OIColumnConv    = FieldArray<FIELDS_CONV$, vPos>
                                            Convert 'Z' to '' in OIColumnConv   ; // No empty strings with numeric columns.
                                            If OIColumnConv NE '' then
                                                OIColumnValue   = Fmt(OIColumnValue, OIColumnConv)
                                            end
                                        Case SQLColumnType _EQC 'bit'
                                            // Treat as a Boolean, but only return a 1, 0, or NULL.
                                            If OIColumnValue EQ '' then
                                                OIColumnValue = 'NULL'
                                            end else
                                                If OIColumnValue NE False$ then OIColumnValue = True$
                                            end
                                        Case SQLColumnType _EQC 'date'
                                            OIColumnValue   = "'" : (SRP_Date('Format', OIColumnValue, 'YYYY-MM-DD')) : "'"
                                        Case SQLColumnType _EQC 'datetime'
                                            OIColumnValue   = "'" : (SRP_DateTime('Format', OIColumnValue, 'YYYY-MM-DD hh:mm:ss')) : "'"
                                        Case SQLColumnType _EQC 'time'
                                            OIColumnValue   = "'" : (SRP_Time('Format', OIColumnValue, 'hh:mm:ss')) : "'"
                                        Case Otherwise$
                                            // Treat as a character type.
                                            SQLColumnLength = SRP_JSON(MapObj, 'GETVALUE', 'SQLColumnLength')
                                            OIColumnConv    = FieldArray<FIELDS_CONV$, vPos>
                                            If OIColumnConv NE '' then
                                                OIColumnValue   = Fmt(OIColumnValue, OIColumnConv)
                                            end
                                            If (SQLColumnLength EQ '') OR (SQLColumnLength EQ '-1') then SQLColumnLength = Len(OIColumnValue)
                                            OIColumnValue   = OIColumnValue[1, SQLColumnLength, 1]
                                            Swap "'" with "''" in OIColumnValue
                                            OIColumnValue   = "'" : OIColumnValue : "'"
                                    End Case
                                end
                                // Basic conversion rules.
                                Convert \09\ to ' ' in OIColumnValue
                                Swap \0D0A\ with ' ' in OIColumnValue
                                Convert @VM to '|' in OIColumnValue
                                Convert @SVM to ' ' in OIColumnValue
                                Convert @TM to ' ' in OIColumnValue
                                Convert @STM to ' ' in OIColumnValue
                                ColumnValues   := OIColumnValue : @FM

                                SRP_JSON(MapObj, 'RELEASE')
                            end
                        end
                    end
                While Error_Services('NoError')
                Next OIColumnName
            end
            ColumnValues[-1, 1]    = ''
        end
    end else
        Error_Services('Add', 'The OITableName, SQLTableName, OIKeyID, or OIRow argument was missing in the ' : Service : ' service.')
    end

    Response    = ColumnValues

end service


//----------------------------------------------------------------------------------------------------------------------
// GetJoinClause
//
// OITableName      - The name of the OpenInsight table that is mapped to the SQL table. - [Required]
// SQLTableName     - The name of the SQL table that the Openinsight table is mapped to. - [Required]
// OIKeyID          - The Key ID to the OpenInsight row being written to SQL. - [Required]
// OIRow            - The OpenInsight row being written to SQL. - [Optional]
// AMVPosition      - The position within the multivalue column that should be used in the join clause if the
//                    OpenInsight column is supposed to use its Index Position. - [Optional]
// IgnoreAMVMaster  - Boolean falg to indicate if the joined column associated with the OpenInsight AMV Master should
//                    be ignored. This is necessary for Join Clauses used for Select and Delete statements. - [Optional]
//
// Returns a well formed join clause using the indicated OpenInsight Key ID and row for the indicated OpenInsight table
// and SQL table. Note: OIRow will likely be an empty string since most join clauses will be based on the OpenInsight
// Key ID.
//----------------------------------------------------------------------------------------------------------------------
Service GetJoinClause(OITableName, SQLTableName, OIKeyID, OIRow, AMVPosition, IgnoreAMVMaster=BOOLEAN)

    Convert @Lower_Case to @Upper_Case in OITableName

    JoinClause  = ''

    If IgnoreAMVMaster NE True$ then IgnoreAMVMaster = False$

    If (OITableName NE '') AND (SQLTableName NE '') AND (OIKeyID NE '') then
        JoinedSQLColumns    = SQL_Services('GetMappedJoinedSQLColumnNames', OITableName, SQLTableName)
        If JoinedSQLColumns NE '' then
            JoinedOperators = SQL_Services('GetMappedJoinedOperators', OITableName, SQLTableName)
            JoinedSQLValues = SQL_Services('GetMappedJoinedSQLColumnValues', OITableName, SQLTableName, OIKeyID, OIRow, AMVPosition)
            If IgnoreAMVMaster EQ True$ then
                // Get all of the OpenInsight columns associated with this join and the AMV Master. Remove the SQL column,
                // operator, and value associated with the AMV Master.
                AMVMaster   = SQL_Services('GetAMVMaster', OITableName, SQLTableName)
                If AMVMaster NE '' then
                    JoinedOIColumns = SQL_Services('GetMappedJoinedOIColumnNames', OITableName, SQLTableName)
                    Locate AMVMaster in JoinedOIColumns using @FM setting Pos then
                        JoinedSQLColumns    = Delete(JoinedSQLColumns, Pos, 0, 0)
                        JoinedOperators     = Delete(JoinedOperators, Pos, 0, 0)
                        JoinedSQLValues     = Delete(JoinedSQLValues, Pos, 0, 0)
                    end
                end
            end
            For Each JoinedColumn in JoinedSQLColumns using @FM setting Pos
                JoinClause := JoinedColumn : ' '
                JoinClause := JoinedOperators<Pos> : ' '
                JoinClause := JoinedSQLValues<Pos>
                JoinClause := @FM
            Next JoinedColumn
            JoinClause[-1, 1]   = ''
            Swap @FM with ' AND ' in JoinClause
        end
    end else
        Error_Services('Add', 'The OITableName, SQLTableName, OIKeyID argument was missing in the ' : Service : ' service.')
    end

    Response    = JoinClause

end service


//----------------------------------------------------------------------------------------------------------------------
// GetJoinClause
//
// OITableName      - The name of the OpenInsight table that is mapped to the SQL table. - [Required]
// SQLTableName     - The name of the SQL table that the Openinsight table is mapped to. - [Required]
// OIKeyID          - The Key ID to the OpenInsight row being written to SQL. - [Required]
// OIRow            - The OpenInsight row being written to SQL. - [Optional]
// AMVPosition      - The position within the multivalue column that should be used in the join clause if the
//                    OpenInsight column is supposed to use its Index Position. - [Optional]
// IgnoreAMVMaster  - Boolean falg to indicate if the joined column associated with the OpenInsight AMV Master should
//                    be ignored. This is necessary for Join Clauses used for Select and Delete statements. - [Optional]
//
// Returns a well formed join clause using the indicated OpenInsight Key ID and row for the indicated OpenInsight table
// and SQL table. Note: OIRow will likely be an empty string since most join clauses will be based on the OpenInsight
// Key ID.
//----------------------------------------------------------------------------------------------------------------------
Service GetMergeClause(OITableName, SQLTableName, OIKeyID, OIRow, AMVPosition, IgnoreAMVMaster=BOOLEAN)

    Convert @Lower_Case to @Upper_Case in OITableName

    JoinClause  = ''

    If IgnoreAMVMaster NE True$ then IgnoreAMVMaster = False$

    If (OITableName NE '') AND (SQLTableName NE '') AND (OIKeyID NE '') then
        JoinedSQLColumns    = SQL_Services('GetMappedJoinedSQLColumnNames', OITableName, SQLTableName)
        If JoinedSQLColumns NE '' then
            JoinedOperators = SQL_Services('GetMappedJoinedOperators', OITableName, SQLTableName)
            JoinedSQLValues = SQL_Services('GetMappedJoinedSQLColumnValues', OITableName, SQLTableName, OIKeyID, OIRow, AMVPosition)
            If IgnoreAMVMaster EQ True$ then
                // Get all of the OpenInsight columns associated with this join and the AMV Master. Remove the SQL column,
                // operator, and value associated with the AMV Master.
                AMVMaster   = SQL_Services('GetAMVMaster', OITableName, SQLTableName)
                If AMVMaster NE '' then
                    JoinedOIColumns = SQL_Services('GetMappedJoinedOIColumnNames', OITableName, SQLTableName)
                    Locate AMVMaster in JoinedOIColumns using @FM setting Pos then
                        JoinedSQLColumns    = Delete(JoinedSQLColumns, Pos, 0, 0)
                        JoinedOperators     = Delete(JoinedOperators, Pos, 0, 0)
                        JoinedSQLValues     = Delete(JoinedSQLValues, Pos, 0, 0)
                    end
                end
            end
            For Each JoinedColumn in JoinedSQLColumns using @FM setting Pos
                JoinClause := 'tgt.':JoinedColumn : ' '
                JoinClause := JoinedOperators<Pos> : ' '
                JoinClause := 'src.':JoinedColumn
                JoinClause := @FM
            Next JoinedColumn
            JoinClause[-1, 1]   = ''
            Swap @FM with ' AND ' in JoinClause
        end
    end else
        Error_Services('Add', 'The OITableName, SQLTableName, OIKeyID argument was missing in the ' : Service : ' service.')
    end

    Response    = JoinClause

end service


//----------------------------------------------------------------------------------------------------------------------
// GetMappedOIColumnNames
//
// OITableName              - The name of the OpenInsight table that is mapped to the SQL table. - [Required]
// SQLTableName             - The name of the SQL table that the Openinsight table is mapped to. - [Required]
// ExcludeJoinedColumns     - Boolean flag indicating if the columns used for the join should be excluded from the
//                            list of columns returned. This would be done if the columns are used to insert or update
//                            the SQL table. Default is true. - [Optional]
//
// Returns an @FM delimited list of OpenInsight column names from the indicated table that are mapped to the indicated
// SQL table.
//----------------------------------------------------------------------------------------------------------------------
Service GetMappedOIColumnNames(OITableName, SQLTableName, ExcludeJoinedColumns=BOOLEAN)

    Convert @Lower_Case to @Upper_Case in OITableName
    If ExcludeJoinedColumns NE False$ then ExcludeJoinedColumns = True$

    ServiceKeyID := '*' : OITableName : '*' : SQLTableName : '*' : ExcludeJoinedColumns
    ColumnNames   = Memory_Services('GetValue', ServiceKeyID)

    If ColumnNames EQ '' then
        If (OITableName NE '') AND (SQLTableName NE '') then
            // Go through all of the OpenInsight column SQL maps and add any column which is bound to the same SQL
            // table. Note, one or more of these columns might already be in the list from the
            // GetMappedJoinedOIColumnNames service. These will be skipped.
            AllOIColumnToSQLMaps    = SQL_Services('GetAllOIColumnToSQLMaps', OITableName)
            For Each OIColumnToSQLMapList in AllOIColumnToSQLMaps using @FM setting fPos
                (OIColumnName, OIColumnToSQLMap) using @VM = OIColumnToSQLMapList
                If SRP_JSON(MapObj, 'PARSE', OIColumnToSQLMap) EQ '' then
                    MappedSQLTableName  = SRP_JSON(MapObj, 'GETVALUE', 'SQLTableName')
                    MappedSQLColumnName = SRP_JSON(MapObj, 'GETVALUE', 'SQLColumnName')
                    If (MappedSQLTableName NE '') AND (MappedSQLColumnName NE '') AND (SQLTableName EQ MappedSQLTableName) then
                        Locate OIColumnName in ColumnNames using @FM setting fPos else
                            ColumnNames := OIColumnName : @FM
                        end
                    end
                    SRP_JSON(MapObj, 'Release')
                end
            Next OIColumnToSQLMap

            If ExcludeJoinedColumns EQ True$ then
                // Get all OpenInsight columns defined in the SQL Join Clause. Then remove any that are rely upon Column
                // Data mapping rather than Index Position. This will avoid duplicate columns being used in a SQL statement.
                JoinedValuesReturned = ''
                JoinedOIColumns = SQL_Services('GetMappedJoinedOIColumnNames', OITableName, SQLTableName, JoinedValuesReturned)
                If JoinedOIColumns NE '' then
                    For Each JoinedColumnName in JoinedOIColumns using @FM Setting JoinedPos
                        Locate JoinedColumnName in ColumnNames using @FM setting Pos then
                            If JoinedValuesReturned<JoinedPos> EQ 'Column Data' then
                                ColumnNames = Delete(ColumnNames, Pos, 0, 0)
                            end
                        end
                    Next JoinedColumnName
                end
            end

            ColumnNames[-1, 1]  = ''
            If ColumnNames NE '' then
                Memory_Services('SetValue', ServiceKeyID, ColumnNames)
            end
        end else
            Error_Services('Add', 'The OITableName or SQLTableName argument was missing in the ' : Service : ' service.')
        end
    end

    Response    = ColumnNames

end service


//----------------------------------------------------------------------------------------------------------------------
// GetMappedOIColumnValues
//
// OITableName  - The name of the OpenInsight table that is mapped to the SQL table. - [Required]
// SQLTableName - The name of the SQL table that the Openinsight table is mapped to. - [Required]
// SQLRow       - The SQL row that will be mapped to the OpenInsight row. - [Required]
// OIRow        - The OpenInsight row with pre-existing data. See below for more information. - [Optional]
//
// Returns an OpenInsight data row for the indicated table that are derived from the indicated SQL table and
// corresponding SQL data. Note, since data can come from multiple SQL tables, the OIRow arguments can already contain
// data from a previous call to this service. This allows primary and secondary SQL tables to be passed into this
// service so that all relevant SQL data can be denormalized into a single OpenInsight data row. This service is
// normally called to create a denormalized OpenInsight row.
//----------------------------------------------------------------------------------------------------------------------
Service GetMappedOIColumnValues(OITableName, SQLTableName, SQLData, OIRow)

    Convert @Lower_Case to @Upper_Case in OITableName

    If (OITableName NE '') AND (SQLTableName NE '') AND (SQLData NE '') then
        MappedOIColumnNames     = SQL_Services('GetMappedOIColumnNames', OITableName, SQLTableName, False$)
        MappedSQLColumnNames    = SQL_Services('GetMappedSQLColumnNames', OITableName, SQLTableName, '', False$)
        FieldArray              = Database_Services('ReadDataRow', 'DICT.' : OITablename, '%FIELDS%')
        If Error_Services('NoError') then
            For Each SQLRow in SQLData using @RM setting ValuePos
                For Each SQLColumnName in MappedSQLColumnNames using @FM setting fPos
                    OIColumnName    = MappedOIColumnNames<fPos>
                    Locate OIColumnName in FieldArray<FIELDS_NAME$> using @VM setting vPos then
                        OIColumnType    = FieldArray<FIELDS_TYPE$, vPos>
                        OIColumnPos     = FieldArray<FIELDS_FIELD_NO$, vPos>
                        If OIColumnType EQ 'F' AND OIColumnPos NE 0 then
                            OIColumnToSQLMap    = SQL_Services('GetOIColumnToSQLMap', OITableName, OIColumnName)
                            If Error_Services('NoError') then
                                If SRP_JSON(MapObj, 'PARSE', OIColumnToSQLMap) EQ '' then
                                    SQLColumnType   = SRP_JSON(MapObj, 'GETVALUE', 'SQLColumnType')
                                    OIColumnValue   = SQLRow<fPos>
                                    Swap '<@VM>' with @VM in OIColumnValue
                                    Swap @SVM with '<@SVM>' in OIColumnValue
                                    Swap @TM with '<@TM>' in OIColumnValue
                                    Swap @STM with '<@STM>' in OIColumnValue

                                    Begin Case
                                        Case InList('varchar,nvarchar,char,nchar', SQLColumnType, ',')
                                            // Single quote the value.
                                            OIColumnConv    = FieldArray<FIELDS_CONV$, vPos>
                                            If OIColumnConv NE '' then
                                                OIColumnValue   = Iconv(OIColumnValue, OIColumnConv)
                                            end
                                        Case InList('tinyint,smallint,bigint,int', SQLColumnType, ',')
                                            // Treat as an integer.
                                            OIColumnConv    = 'MD0'
                                            If OIColumnConv NE '' then
                                                OIColumnValue   = Iconv(OIColumnValue, OIColumnConv)
                                            end
                                        Case InList('decimal,numeric,float,real', SQLColumnType, ',')
                                            // Treat as a floating point number using any OI column output conversion if it
                                            // exists.
                                            OIColumnConv    = FieldArray<FIELDS_CONV$, vPos>
                                            If OIColumnConv NE '' then
                                                OIColumnValue   = Iconv(OIColumnValue, OIColumnConv)
                                            end
                                        Case SQLColumnType _EQC 'date'
                                            OIColumnValue   = SRP_Date('Parse', OIColumnValue, 'MM/DD/YYYY')
                                        Case SQLColumnType _EQC 'datetime'
                                            OIColumnValue   = SRP_DateTime('Parse', OIColumnValue, 'MM/DD/YYYY hh:mm:ss')
                                        Case SQLColumnType _EQC 'time'
                                            OIColumnValue   = SRP_Time('Parse', OIColumnValue, 'hh:mm:ss')
                                        Case Otherwise$
                                            // Treat as a character type.
                                            OIColumnConv    = FieldArray<FIELDS_CONV$, vPos>
                                            If OIColumnConv NE '' then
                                                OIColumnValue   = Iconv(OIColumnValue, OIColumnConv)
                                            end
                                    End Case
                                    OIRow<OIColumnPos, ValuePos>    = OIColumnValue
                                end
                                SRP_JSON(MapObj, 'RELEASE')
                            end
                        end
                    end
                Next SQLColumnName
            Next SQLRow
        end

    end else
        Error_Services('Add', 'The SQLTableName, SQLData, or OITableName argument was missing in the ' : Service : ' service.')
    end

    Response    = OIRow

end service


//----------------------------------------------------------------------------------------------------------------------
// GetMappedSQLColumnNames
//
// OITableName              - The name of the OpenInsight table that is mapped to the SQL table. - [Required]
// SQLTableName             - The name of the SQL table that the Openinsight table is mapped to. - [Required]
// OIColumns                - An @FM delimited list of OpenInsight table columns that should only be used to write to
//                            SQL. If empty, all columns that have a map defintion will be used. - [Optional]
// ExcludeJoinedColumns     - Boolean flag indicating if the columns used for the join should be excluded from the
//                            list of columns returned. This would be done if the columns are used to insert or update
//                            the SQL table. Default is true. - [Optional]
//
// Returns an @FM delimited list of SQL column names from the indicated table that are mapped from the indicated
// OpenInsight table and associated OpenInsight columns.
//----------------------------------------------------------------------------------------------------------------------
Service GetMappedSQLColumnNames(OITableName, SQLTableName, OIColumns, ExcludeJoinedColumns)
    
    Convert @Lower_Case to @Upper_Case in OITableName
    If ExcludeJoinedColumns NE False$ then ExcludeJoinedColumns = True$
    ServiceKeyID := '*' : OITableName : '*' : SQLTableName : '*' : ExcludeJoinedColumns
    ColumnNames   = Memory_Services('GetValue', ServiceKeyID)
    If ColumnNames EQ '' then
        If (OITableName NE '') AND (SQLTableName NE '') then
            If OIColumns NE '' then
                Transfer OIColumns to OIColumnNames
            end else
                // The GetMappedOIColumnNames service will automatically remove any columns that are also mapped in
                // a join clause with the "Column Data" return value attribute.
                OIColumnNames   = SQL_Services('GetMappedOIColumnNames', OITableName, SQLTableName, ExcludeJoinedColumns)
            end
            If OIColumnNames NE '' then
                For Each OIColumnName in OIColumnNames using @FM setting fPos
                    OIColumnToSQLMap    = SQL_Services('GetOIColumnToSQLMap', OITableName, OIColumnName)
                    If Error_Services('NoError') then
                        If SRP_JSON(MapObj, 'PARSE', OIColumnToSQLMap) EQ '' then
                            MappedSQLTableName  = SRP_JSON(MapObj, 'GETVALUE', 'SQLTableName')
                            MappedSQLColumnName = SRP_JSON(MapObj, 'GETVALUE', 'SQLColumnName')
                            If (MappedSQLTableName EQ SQLTablename) AND (MappedSQLColumnName NE '') then
                                ColumnNames    := MappedSQLColumnName : @FM
                            end
                            SRP_JSON(MapObj, 'RELEASE')
                        end
                    end
                Next OIColumnName
            end
            ColumnNames[-1, 1]  = ''
            If ColumnNames NE '' then Memory_Services('SetValue', ServiceKeyID, ColumnNames)
        end else
            Error_Services('Add', 'The OITableName or SQLTableName argument was missing in the ' : Service : ' service.')
        end
    end
    Response    = ColumnNames
    
end service


//----------------------------------------------------------------------------------------------------------------------
// GetMappedSQLColumnValues
//
// OITableName  - The name of the OpenInsight table that is mapped to the SQL table to. - [Required]
// SQLTableName - The name of the SQL table that the Openinsight table is mapped to. - [Required]
// OIKeyID      - The Key ID to the OpenInsight row being written to SQL. - [Required]
// OIRow        - The OpenInsight row being written to SQL. - [Required]
// AMVPosition  - The position within the multivalue column that should be used in the join clause if the OpenInsight
//                column is supposed to use its Index Position. - [Optional]
// OIColumns    - An @FM delimited list of OpenInsight table columns that should only be used to write to SQL. If empty,
//                all columns that have a map defintion will be used. - [Optional]
//
// Returns an @FM delimited list of SQL column values for the indicated table that are derived from the indicated
// OpenInsight table and associated OpenInsight columns. This service is normally called to create a fully formed Insert
// query statement.
//----------------------------------------------------------------------------------------------------------------------
Service GetMappedSQLColumnValues(OITableName, SQLTableName, OIKeyID, OIRow, AMVPosition, OIColumns)
    
    Convert @Lower_Case to @Upper_Case in OITableName
    If Num(AMVPosition) else AMVPosition = ''
    ColumnValues    = ''
    If (OITableName NE '') AND (SQLTableName NE '') AND (OIKeyID NE '') AND (OIRow NE '') then
        FieldArray      = Database_Services('ReadDataRow', 'DICT.' : OITablename, '%FIELDS%')
        If OIColumns NE '' then
            Transfer OIColumns to OIColumnNames
        end else
            // The GetMappedOIColumnNames service will automatically remove any columns that are also mapped in
            // a join clause with the "Column Data" return value attribute.
            OIColumnNames   = SQL_Services('GetMappedOIColumnNames', OITableName, SQLTableName, True$)
        end
        If OIColumnNames NE '' then
            For Each OIColumnName in OIColumnNames using @FM setting fPos
                Locate OIColumnName in FieldArray<FIELDS_NAME$> using @VM setting vPos then
                    OIColumnToSQLMap    = SQL_Services('GetOIColumnToSQLMap', OITableName, OIColumnName)
                    If Error_Services('NoError') then
                        If SRP_JSON(MapObj, 'PARSE', OIColumnToSQLMap) EQ '' then
                            MappedSQLTableName  = SRP_JSON(MapObj, 'GETVALUE', 'SQLTableName')
                            If MappedSQLTableName EQ SQLTablename then
                                SQLColumnType   = SRP_JSON(MapObj, 'GETVALUE', 'SQLColumnType')
                                OIColumnType    = FieldArray<FIELDS_TYPE$, vPos>
                                IsOIKey         = False$    ; // Assume false for now.
                                If OIColumnType EQ 'F' then
                                    OIColumnPos = FieldArray<FIELDS_FIELD_NO$, vPos>
                                    If OIColumnPos GT 0 then
                                        OIColumnValue   = OIRow<OIColumnPos>
                                    end else
                                        IsOIKey         = True$
                                        OIKeyPart       = FieldArray<FIELDS_PART$, vPos>
                                        OIColumnValue   = Field(OIKeyID, '*', OIKeyPart, 1)
                                        If OIColumnValue EQ '' then
                                            Error_Services('Add', 'Key ID Column ' : OIColumnName : ' was assigned an empty string value in ' : Service : '.')
                                        end
                                    end
                                end else
                                    // OIColumnValue   = Calculate(OIColumnName)    ; // Actual logic used in MFS
                                    OIColumnValue   = Xlate(OITablename, OIKeyID, OIColumnName, 'X')
                                end
                                If (AMVPosition NE '') AND (IsOIKey NE True$) then
                                    OIColumnValue   = OIColumnValue<0, AMVPosition>
                                end
                                Begin Case
                                    Case SQLColumnType _EQC 'uniqueidentifier'
                                        OIColumnValue = 'NEWID()'
                                    Case OIColumnValue EQ ''
                                        // Empty strings are treated as NULL values.
                                        OIColumnValue   = 'NULL'
                                    Case InList('varchar,nvarchar,char,nchar', SQLColumnType, ',')
                                        // Single quote the value.
                                        SQLColumnLength = SRP_JSON(MapObj, 'GETVALUE', 'SQLColumnLength')
                                        OIColumnConv    = FieldArray<FIELDS_CONV$, vPos>
                                        If OIColumnConv NE '' then
                                            OIColumnValue   = Fmt(OIColumnValue, OIColumnConv)
                                        end
                                        If (SQLColumnLength EQ '') OR (SQLColumnLength EQ '-1') then SQLColumnLength = Len(OIColumnValue)
                                        OIColumnValue   = OIColumnValue[1, SQLColumnLength, 1]
                                        Swap "'" with "''" in OIColumnValue
                                        OIColumnValue   = "'" : OIColumnValue : "'"
                                    Case InList('tinyint,smallint,bigint,int', SQLColumnType, ',')
                                        // Treat as an integer.
                                        OIColumnConv    = 'MD0'
                                        If Len(OIColumnConv) then
                                            OIColumnValue   = Fmt(OIColumnValue, OIColumnConv)
                                            If Not(Num(OIColumnValue)) then
                                                // Not a true number so format like a string.
                                                OIColumnValue   = "'" : OIColumnValue : "'"
                                            end
                                        end else
                                            // Empty value so treat it as a NULL value.
                                            OIColumnValue   = 'NULL'
                                        end
                                    Case InList('decimal,numeric,float,real', SQLColumnType, ',')
                                        // Treat as a floating point number using any OI column output conversion if it
                                        // exists.
                                        OIColumnConv    = FieldArray<FIELDS_CONV$, vPos>
                                        Convert 'Z' to '' in OIColumnConv   ; // No empty strings with numeric columns.
                                        If Len(OIColumnConv) then
                                            OIColumnValue   = Fmt(OIColumnValue, OIColumnConv)
                                            If Not(Num(OIColumnValue)) then
                                                // Not a true number so format like a string.
                                                OIColumnValue   = "'" : OIColumnValue : "'"
                                            end
                                        end else
                                            // Empty value so treat it as a NULL value.
                                            OIColumnValue   = 'NULL'
                                        end
                                    Case SQLColumnType _EQC 'bit'
                                        // Treat as a Boolean, but only return a 1, 0, or NULL.
                                        If OIColumnValue EQ '' then
                                            OIColumnValue = 'NULL'
                                        end else
                                            If OIColumnValue NE False$ then OIColumnValue = True$
                                        end
                                    Case SQLColumnType _EQC 'date'
                                        OIColumnValue = "'" : (SRP_Date('Format', OIColumnValue, 'YYYY-MM-DD')) : "'"
                                    Case SQLColumnType _EQC 'datetime'
                                        OIColumnValue = "'" : (SRP_DateTime('Format', OIColumnValue, 'YYYY-MM-DD hh:mm:ss')) : "'"
                                    Case SQLColumnType _EQC 'time'
                                        OIColumnValue = "'" : (SRP_Time('Format', OIColumnValue, 'hh:mm:ss')) : "'"
                                    Case Otherwise$
                                        // Treat as a character type.
                                        SQLColumnLength = SRP_JSON(MapObj, 'GETVALUE', 'SQLColumnLength')
                                        OIColumnConv    = FieldArray<FIELDS_CONV$, vPos>
                                        If OIColumnConv NE '' then
                                            OIColumnValue   = Fmt(OIColumnValue, OIColumnConv)
                                        end
                                        If (SQLColumnLength EQ '') OR (SQLColumnLength EQ '-1') then SQLColumnLength = Len(OIColumnValue)
                                        OIColumnValue   = OIColumnValue[1, SQLColumnLength, 1]
                                        Swap "'" with "''" in OIColumnValue
                                        OIColumnValue   = "'" : OIColumnValue : "'"
                                End Case
                                // Basic conversion rules.
                                Convert \09\ to ' ' in OIColumnValue
                                Swap \0D0A\ with ' ' in OIColumnValue
                                Convert @VM to '|' in OIColumnValue
                                Convert @SVM to ' ' in OIColumnValue
                                Convert @TM to ' ' in OIColumnValue
                                Convert @STM to ' ' in OIColumnValue
                                ColumnValues   := OIColumnValue : @FM
                            end
                            SRP_JSON(MapObj, 'RELEASE')
                        end
                        Error_Services('Clear') ; // Any errors at this point would have been created during calculated column logic, which is unrelated to this service.
                    end
                end
            While Error_Services('NoError')
            Next OIColumnName
        end
        ColumnValues[-1, 1] = ''
    end else
        Error_Services('Add', 'The OITableName, SQLTableName, OIKeyID, or OIRow argument was missing in the ' : Service : ' service.')
    end
    Response    = ColumnValues
    
end service


//----------------------------------------------------------------------------------------------------------------------
// GetSQLInsertStatements
//
// OITableName  - The name of the OpenInsight table that is mapped to the SQL table. - [Required]
// OIKeyID      - The Key ID to the OpenInsight row being deleted from SQL. - [Required]
// OIRow        - The OpenInsight row associated with the OpenInsight Key ID. - [Optional]
// OIColumns    - An @FM delimited list of OpenInsight table columns that should only be used to write to SQL. If empty,
//                all columns that have a map defintion will be used. - [Optional]
//
// Returns an @FM delimited list of SQL insert statements for the SQL table mapped to the indicated OpenInsight table.
// This will automatically create all SQL insert statements for secondary SQL tables as well.
//----------------------------------------------------------------------------------------------------------------------
Service GetSQLInsertStatements(OITableName, OIKeyID, OIRow, OIColumns)

    Convert @Lower_Case to @Upper_Case in OITableName
    SQLInsertStatements = ''

    If (OITableName NE '') AND (OIKeyID NE '') AND (OIRow NE '') then

        // Start with the primary SQL table that is mapped to this OpenInsight table.
        PrimarySQLTableName     = SQL_Services('GetPrimarySQLTable', OITableName)
        MappedSQLColumnNames    = SQL_Services('GetMappedSQLColumnNames', OITableName, PrimarySQLTableName, OIColumns)
        If MappedSQLColumnNames NE '' then MappedSQLColumnNames := @FM
        MappedSQLColumnValues   = SQL_Services('GetMappedSQLColumnValues', OITablename, PrimarySQLTableName, OIKeyID, OIRow, '', OIColumns)
        If MappedSQLColumnValues NE '' then MappedSQLColumnValues := @FM
        JoinedSQLColumnNames    = SQL_Services('GetMappedJoinedSQLColumnNames', OITableName, PrimarySQLTableName)
        If JoinedSQLColumnNames NE '' then
            MappedSQLColumnNames  := JoinedSQLColumnNames : @FM
            JoinedSQLColumnValues  = SQL_Services('GetMappedJoinedSQLColumnValues', OITableName, PrimarySQLTableName, OIKeyID, OIRow)
            MappedSQLColumnValues := JoinedSQLColumnValues : @FM
        end
        StaticSQLColumnNames    = SQL_Services('GetStaticSQLColumnNames', OITableName)
        If StaticSQLColumnNames NE '' then
            MappedSQLColumnNames  := StaticSQLColumnNames : @FM
            StaticSQLColumnValues  = SQL_Services('GetStaticSQLColumnValues', OITableName)
            MappedSQLColumnValues := StaticSQLColumnValues : @FM
        end
        MappedSQLColumnNames[-1, 1]   = ''
        MappedSQLColumnValues[-1, 1]  = ''
        Swap @FM with ', ' in MappedSQLColumnNames
        Swap @FM with ", " in MappedSQLColumnValues
        SQLInsertStatement            = 'INSERT INTO ' : PrimarySQLTableName : ' '
        SQLInsertStatement           := '('
        SQLInsertStatement           := MappedSQLColumnNames
        SQLInsertStatement           := ') VALUES ('
        SQLInsertStatement           := MappedSQLColumnValues
        SQLInsertStatement           := ')'
        SQLInsertStatements          := SQLInsertStatement : @FM

        If Error_Services('NoError') then
            // Add any secondary SQL table inserts that might be required.
            SecondarySQLTableNames  = SQL_Services('GetSecondarySQLTables', OITableName)
            If SecondarySQLTableNames NE '' then
                For Each SecondarySQLTableName in SecondarySQLTableNames using @FM
                    OIColumnValue = ''  ; // Default to empty string. Update if AMV Master is found.
                    NumPositions  = 1   ; // Default to only 1 index position. Update if AMV Master is found.
                    AMVMaster     = SQL_Services('GetAMVMaster', OITableName, SecondarySQLTableName)
                    If AMVMaster NE '' then
                        FieldArray = Database_Services('ReadDataRow', 'DICT.' : OITablename, '%FIELDS%')
                        If Error_Services('NoError') then
                            Locate AMVMaster in FieldArray<FIELDS_NAME$> using @VM setting vPos then
                                OIColumnType    = FieldArray<FIELDS_TYPE$, vPos>
                                If OIColumnType EQ 'F' then
                                    OIColumnPos = FieldArray<FIELDS_FIELD_NO$, vPos>
                                    If OIColumnPos GT 0 then
                                        OIColumnValue   = OIRow<OIColumnPos>
                                    end else
                                        OIKeyPart       = FieldArray<FIELDS_PART$, vPos>
                                        OIColumnValue   = Field(OIKeyID, '*', OIKeyPart, 1)
                                    end
                                end else
                                    // OIColumnValue   = Calculate(OIColumnName)    ; // Actual logic used in MFS
                                    OIColumnValue   = Xlate(OITablename, OIKeyID, OIColumnName, 'X')
                                end
                                NumPositions    = DCount(OIColumnValue, @VM)
                            end
                        end
                    end

                    MappedSQLColumnNames    = SQL_Services('GetMappedSQLColumnNames', OITableName, SecondarySQLTableName)
                    If MappedSQLColumnNames NE '' then
                        JoinedSQLColumnNames    = SQL_Services('GetMappedJoinedSQLColumnNames', OITableName, SecondarySQLTableName)
                        If JoinedSQLColumnNames NE '' then
                            MappedSQLColumnNames   := @FM : JoinedSQLColumnNames
                        end
                        Swap @FM with ', ' in MappedSQLColumnNames
                        For AMVPosition = 1 to NumPositions
                            If OIColumnValue<0, AMVPosition> NE '' then
                                MappedSQLColumnValues   = SQL_Services('GetMappedSQLColumnValues', OITablename, SecondarySQLTableName, OIKeyID, OIRow, AMVPosition)
                                If JoinedSQLColumnNames NE '' then
                                    JoinedSQLColumnValues   = SQL_Services('GetMappedJoinedSQLColumnValues', OITableName, SecondarySQLTableName, OIKeyID, OIRow, AMVPosition)
                                    MappedSQLColumnValues  := @FM : JoinedSQLColumnValues
                                end
                                Swap @FM with ', ' in MappedSQLColumnValues
                                SQLInsertStatement      = 'INSERT INTO ' : SecondarySQLTableName : ' '
                                SQLInsertStatement     := '('
                                SQLInsertStatement     := MappedSQLColumnNames
                                SQLInsertStatement     := ') VALUES ('
                                SQLInsertStatement     := MappedSQLColumnValues
                                SQLInsertStatement     := ')'
                                SQLInsertStatements    := SQLInsertStatement : @FM
                            end
                        Next AMVPosition
                    end
                Next SecondarySQLTableName
            end
        end

        SQLInsertStatements[-1, 1]  = ''
    end else
        Error_Services('Add', 'The OITableName, OIKeyID, or OIRow argument was missing in the ' : Service : ' service.')
    end
    
    Response    = SQLInsertStatements

end service


//----------------------------------------------------------------------------------------------------------------------
// GetSQLUpdateStatements
//
// OITableName  - The name of the OpenInsight table that is mapped to the SQL table. - [Required]
// OIKeyID      - The Key ID to the OpenInsight row being deleted from SQL. - [Required]
// OIRow        - The OpenInsight row associated with the OpenInsight Key ID. - [Optional]
// OIColumns    - An @FM delimited list of OpenInsight table columns that should only be used to write to SQL. If empty,
//                all columns that have a map defintion will be used. - [Optional]
//
// Returns an @FM delimited list of SQL update statements for the SQL table mapped to the indicated OpenInsight table.
// This will automatically create all SQL update statements for secondary SQL tables as well.
//----------------------------------------------------------------------------------------------------------------------
Service GetSQLUpdateStatements(OITableName, OIKeyID, OIRow, OIColumns)

    Convert @Lower_Case to @Upper_Case in OITableName
    SQLUpdateStatements = ''

    If (OITableName NE '') AND (OIKeyID NE '') AND (OIRow NE '') then

        // Start with the primary SQL table that is mapped to this OpenInsight table.
        PrimarySQLTableName     = SQL_Services('GetPrimarySQLTable', OITableName)
        JoinClause              = SQL_Services('GetJoinClause', OITableName, PrimarySQLTableName, OIKeyID, OIRow)
        MappedSQLColumnNames    = SQL_Services('GetMappedSQLColumnNames', OITableName, PrimarySQLTableName, OIColumns)
        If MappedSQLColumnNames NE '' then MappedSQLColumnNames := @FM
        MappedSQLColumnValues   = SQL_Services('GetMappedSQLColumnValues', OITablename, PrimarySQLTableName, OIKeyID, OIRow, '', OIColumns)
        If MappedSQLColumnValues NE '' then MappedSQLColumnValues := @FM
        JoinedSQLColumnNames    = SQL_Services('GetMappedJoinedSQLColumnNames', OITableName, PrimarySQLTableName)
        StaticSQLColumnNames    = SQL_Services('GetStaticSQLColumnNames', OITableName)
        If StaticSQLColumnNames NE '' then
            MappedSQLColumnNames  := StaticSQLColumnNames : @FM
            StaticSQLColumnValues  = SQL_Services('GetStaticSQLColumnValues', OITableName)
            MappedSQLColumnValues := StaticSQLColumnValues : @FM
        end
        MappedSQLColumnNames[-1, 1]   = ''
        MappedSQLColumnValues[-1, 1]  = ''
        SQLUpdateStatement  = ''
        If MappedSQLColumnNames NE '' then
            SQLUpdateStatement := 'UPDATE ' : PrimarySQLTableName : ' SET '
            For Each SQLColumnName in MappedSQLColumnNames using @FM setting fPos
                SQLUpdateStatement     := SQLColumnName : '=' : MappedSQLColumnValues<fPos> : ', '
            Next SQLColumnName
            SQLUpdateStatement[-2, 1]   = ''
            SQLUpdateStatement         := 'WHERE '
            SQLUpdateStatement         := JoinClause : ' '
            SQLUpdateStatements        := SQLUpdateStatement : @FM
        end

        If Error_Services('NoError') then
            // Add any secondary SQL table inserts that might be required.
            SecondarySQLTableNames  = SQL_Services('GetSecondarySQLTables', OITableName)
            If SecondarySQLTableNames NE '' then
                For Each SecondarySQLTableName in SecondarySQLTableNames using @FM
                    OIColumnValue = ''  ; // Default to empty string. Update if AMV Master is found.
                    NumPositions  = 1   ; // Default to only 1 index position. Update if AMV Master is found.
                    AMVMaster     = SQL_Services('GetAMVMaster', OITableName, SecondarySQLTableName)
                    If AMVMaster NE '' then
                        FieldArray = Database_Services('ReadDataRow', 'DICT.' : OITablename, '%FIELDS%')
                        If Error_Services('NoError') then
                            Locate AMVMaster in FieldArray<FIELDS_NAME$> using @VM setting vPos then
                                OIColumnType    = FieldArray<FIELDS_TYPE$, vPos>
                                If OIColumnType EQ 'F' then
                                    OIColumnPos = FieldArray<FIELDS_FIELD_NO$, vPos>
                                    If OIColumnPos GT 0 then
                                        OIColumnValue   = OIRow<OIColumnPos>
                                    end else
                                        OIKeyPart       = FieldArray<FIELDS_PART$, vPos>
                                        OIColumnValue   = Field(OIKeyID, '*', OIKeyPart, 1)
                                    end
                                end else
                                    // OIColumnValue   = Calculate(OIColumnName)    ; // Actual logic used in MFS
                                    OIColumnValue   = Xlate(OITablename, OIKeyID, OIColumnName, 'X')
                                end
                                NumPositions    = DCount(OIColumnValue, @VM)
                            end
                        end
                    end

                    // Remove all rows from the secondary table. These will be added back using the INSERT statements as needed.
                    SQLUpdateStatement   = 'DELETE FROM ' : SecondarySQLTableName : ' '
                    SQLUpdateStatement  := 'WHERE '
                    SQLUpdateStatement  := JoinClause
                    SQLUpdateStatements := SQLUpdateStatement : @FM

                    MappedSQLColumnNames = SQL_Services('GetMappedSQLColumnNames', OITableName, SecondarySQLTableName)
                    If MappedSQLColumnNames NE '' then
                        JoinedSQLColumnNames    = SQL_Services('GetMappedJoinedSQLColumnNames', OITableName, SecondarySQLTableName)
                        If JoinedSQLColumnNames NE '' then
                            MappedSQLColumnNames   := @FM : JoinedSQLColumnNames
                        end
                        Swap @FM with ', ' in MappedSQLColumnNames
                        For AMVPosition = 1 to NumPositions
                            If OIColumnValue<0, AMVPosition> NE '' then
                                MappedSQLColumnValues   = SQL_Services('GetMappedSQLColumnValues', OITablename, SecondarySQLTableName, OIKeyID, OIRow, AMVPosition)
                                If JoinedSQLColumnNames NE '' then
                                    JoinedSQLColumnValues   = SQL_Services('GetMappedJoinedSQLColumnValues', OITableName, SecondarySQLTableName, OIKeyID, OIRow, AMVPosition)
                                    MappedSQLColumnValues  := @FM : JoinedSQLColumnValues
                                end
                                Swap @FM with ', ' in MappedSQLColumnValues
                                SQLUpdateStatement      = 'INSERT INTO ' : SecondarySQLTableName : ' '
                                SQLUpdateStatement     := '('
                                SQLUpdateStatement     := MappedSQLColumnNames
                                SQLUpdateStatement     := ') VALUES ('
                                SQLUpdateStatement     := MappedSQLColumnValues
                                SQLUpdateStatement     := ')'
                                SQLUpdateStatements    := SQLUpdateStatement : @FM
                            end
                        Next AMVPosition
                    end
                Next SecondarySQLTableName
            end
        end

        SQLUpdateStatements[-1, 1]  = ''
    end else
        Error_Services('Add', 'The OITableName, OIKeyID, or OIRow argument was missing in the ' : Service : ' service.')
    end

    Response    = SQLUpdateStatements

end service


//----------------------------------------------------------------------------------------------------------------------
// GetSQLMergeStatements
//
// OITableName  - The name of the OpenInsight table that is mapped to the SQL table. - [Required]
// OIKeyID      - The Key ID to the OpenInsight row being deleted from SQL. - [Required]
// OIRow        - The OpenInsight row associated with the OpenInsight Key ID. - [Optional]
// OIColumns    - An @FM delimited list of OpenInsight table columns that should only be used to write to SQL. If empty,
//                all columns that have a map defintion will be used. - [Optional]
//
// Returns an @FM delimited list of SQL insert statements for the SQL table mapped to the indicated OpenInsight table.
// This will automatically create all SQL insert statements for secondary SQL tables as well.
//----------------------------------------------------------------------------------------------------------------------
Service GetSQLMergeStatements(OITableName, OIKeyID, OIRow, OIColumns)

    Convert @Lower_Case to @Upper_Case in OITableName
    SQLInsertStatements = ''

    If (OITableName NE '') AND (OIKeyID NE '') AND (OIRow NE '') then

        // Start with the primary SQL table that is mapped to this OpenInsight table.
        PrimarySQLTableName   = SQL_Services('GetPrimarySQLTable', OITableName)
        MappedSQLColumnNames  = SQL_Services('GetMappedSQLColumnNames', OITableName, PrimarySQLTableName, OIColumns)
        MergeClause           = SQL_Services('GetMergeClause', OITableName, PrimarySQLTableName, OIKeyID, OIRow)
        If MappedSQLColumnNames NE '' then MappedSQLColumnNames := @FM
        MappedSQLColumnValues = SQL_Services('GetMappedSQLColumnValues', OITablename, PrimarySQLTableName, OIKeyID, OIRow, '', OIColumns)
        If MappedSQLColumnValues NE '' then MappedSQLColumnValues := @FM
        JoinedSQLColumnNames  = SQL_Services('GetMappedJoinedSQLColumnNames', OITableName, PrimarySQLTableName)
        If JoinedSQLColumnNames NE '' then
            MappedSQLColumnNames  := JoinedSQLColumnNames : @FM
            JoinedSQLColumnValues  = SQL_Services('GetMappedJoinedSQLColumnValues', OITableName, PrimarySQLTableName, OIKeyID, OIRow)
            MappedSQLColumnValues := JoinedSQLColumnValues : @FM
        end
        StaticSQLColumnNames    = SQL_Services('GetStaticSQLColumnNames', OITableName)
        If StaticSQLColumnNames NE '' then
            MappedSQLColumnNames  := StaticSQLColumnNames : @FM
            StaticSQLColumnValues  = SQL_Services('GetStaticSQLColumnValues', OITableName)
            MappedSQLColumnValues := StaticSQLColumnValues : @FM
        end
        MappedSQLColumnNames[-1, 1]   = ''
        MappedSQLColumnValues[-1, 1]  = ''
        Swap @FM with ', ' in MappedSQLColumnNames
        Swap @FM with ", " in MappedSQLColumnValues
        
        SQLInsertStatement   = 'BEGIN '
        SQLInsertStatement  := 'SET NOCOUNT ON; '  
        SQLInsertStatement  := 'MERGE ':PrimarySQLTableName:' AS tgt '
        SQLInsertStatement  := 'USING (VALUES (':MappedSQLColumnValues:')) AS src (':MappedSQLColumnNames:') ' 
        SQLInsertStatement  := 'ON (':MergeClause:') '
        SQLInsertStatement  := 'WHEN MATCHED THEN '
        SQLInsertStatement  := '    UPDATE SET '
        For Each SQLColumnName in MappedSQLColumnNames using ','setting fPos
            SQLColumnName       = Trim(SQLColumnName)
            SQLInsertStatement := SQLColumnName : ' = src.' : SQLColumnName : ', '
        Next SQLColumnName
        SQLInsertStatement[-2, 1] = ''
        SQLInsertStatement  := 'WHEN NOT MATCHED THEN '
        SQLInsertStatement  := '    INSERT (':MappedSQLColumnNames:') '
        SQLInsertStatement  := '    VALUES (':MappedSQLColumnValues:'); '  
        SQLInsertStatement  := 'END;' 
        SQLInsertStatements := SQLInsertStatement : @FM

        If Error_Services('NoError') then
            // Add any secondary SQL table inserts that might be required.
            SecondarySQLTableNames  = SQL_Services('GetSecondarySQLTables', OITableName)
            If SecondarySQLTableNames NE '' then
                For Each SecondarySQLTableName in SecondarySQLTableNames using @FM
                    OIColumnValue = ''  ; // Default to empty string. Update if AMV Master is found.
                    NumPositions  = 1   ; // Default to only 1 index position. Update if AMV Master is found.
                    AMVMaster     = SQL_Services('GetAMVMaster', OITableName, SecondarySQLTableName)
                    If AMVMaster NE '' then
                        FieldArray = Database_Services('ReadDataRow', 'DICT.' : OITablename, '%FIELDS%')
                        If Error_Services('NoError') then
                            Locate AMVMaster in FieldArray<FIELDS_NAME$> using @VM setting vPos then
                                OIColumnType    = FieldArray<FIELDS_TYPE$, vPos>
                                If OIColumnType EQ 'F' then
                                    OIColumnPos = FieldArray<FIELDS_FIELD_NO$, vPos>
                                    If OIColumnPos GT 0 then
                                        OIColumnValue   = OIRow<OIColumnPos>
                                    end else
                                        OIKeyPart       = FieldArray<FIELDS_PART$, vPos>
                                        OIColumnValue   = Field(OIKeyID, '*', OIKeyPart, 1)
                                    end
                                end else
                                    // OIColumnValue   = Calculate(OIColumnName)    ; // Actual logic used in MFS
                                    OIColumnValue   = Xlate(OITablename, OIKeyID, OIColumnName, 'X')
                                end
                                NumPositions    = DCount(OIColumnValue, @VM)
                            end
                        end
                    end

                    MappedSQLColumnNames    = SQL_Services('GetMappedSQLColumnNames', OITableName, SecondarySQLTableName)
                    If MappedSQLColumnNames NE '' then
                        JoinedSQLColumnNames    = SQL_Services('GetMappedJoinedSQLColumnNames', OITableName, SecondarySQLTableName)
                        If JoinedSQLColumnNames NE '' then
                            MappedSQLColumnNames   := @FM : JoinedSQLColumnNames
                        end
                        Swap @FM with ', ' in MappedSQLColumnNames
                        For AMVPosition = 1 to NumPositions
                            If OIColumnValue<0, AMVPosition> NE '' then
                                MergeClause           = SQL_Services('GetMergeClause', OITableName, SecondarySQLTableName, OIKeyID, OIRow)
                                MappedSQLColumnValues = SQL_Services('GetMappedSQLColumnValues', OITablename, SecondarySQLTableName, OIKeyID, OIRow, AMVPosition)
                                If JoinedSQLColumnNames NE '' then
                                    JoinedSQLColumnValues   = SQL_Services('GetMappedJoinedSQLColumnValues', OITableName, SecondarySQLTableName, OIKeyID, OIRow, AMVPosition)
                                    MappedSQLColumnValues  := @FM : JoinedSQLColumnValues
                                end
                                Swap @FM with ', ' in MappedSQLColumnValues
                                
                                SQLInsertStatement   = 'BEGIN '
                                SQLInsertStatement  := 'SET NOCOUNT ON; '  
                                SQLInsertStatement  := 'MERGE ':SecondarySQLTableName:' AS tgt '
                                SQLInsertStatement  := 'USING (VALUES (':MappedSQLColumnValues:')) AS src (':MappedSQLColumnNames:') ' 
                                SQLInsertStatement  := 'ON (':MergeClause:') '
                                SQLInsertStatement  := 'WHEN MATCHED THEN '
                                SQLInsertStatement  := '    UPDATE SET '
                                For Each SQLColumnName in MappedSQLColumnNames using ',' setting fPos
                                    SQLColumnName       = Trim(SQLColumnName)
                                    SQLInsertStatement := SQLColumnName : ' = src.' : SQLColumnName : ', '
                                Next SQLColumnName
                                SQLInsertStatement[-2, 1] = ''
                                SQLInsertStatement  := 'WHEN NOT MATCHED THEN '
                                SQLInsertStatement  := '    INSERT (':MappedSQLColumnNames:') '
                                SQLInsertStatement  := '    VALUES (':MappedSQLColumnValues:'); '  
                                SQLInsertStatement  := 'END;' 
                                SQLInsertStatements := SQLInsertStatement : @FM
                                
                            end
                        Next AMVPosition
                    end
                Next SecondarySQLTableName
            end
        end

        SQLInsertStatements[-1, 1]  = ''
    end else
        Error_Services('Add', 'The OITableName, OIKeyID, or OIRow argument was missing in the ' : Service : ' service.')
    end
    
    Response    = SQLInsertStatements

end service


//----------------------------------------------------------------------------------------------------------------------
// GetSQLSelectStatement
//
// OITableName  - The name of the OpenInsight table that is mapped to the SQL table. - [Required]
// OIKeyID      - The Key ID to the OpenInsight row being deleted from SQL. - [Required]
// OIRow        - The OpenInsight row associated with the OpenInsight Key ID. - [Optional]
//
// Returns the SQL select statement for the indicated SQL table based on its mapping to the indicated OpenInsight table.
//----------------------------------------------------------------------------------------------------------------------
Service GetSQLSelectStatement(OITableName, SQLTableName, OIKeyID, OIRow)

    Convert @Lower_Case to @Upper_Case in OITableName

    SQLSelectStatement  = ''

    If (OITableName NE '') AND (SQLTableName NE '') AND (OIKeyID NE '') then

        JoinClause  = SQL_Services('GetJoinClause', OITableName, SQLTableName, OIKeyID, OIRow, '', True$)
        If JoinClause NE '' then
            MappedSQLColumnNames    = SQL_Services('GetMappedSQLColumnNames', OITableName, SQLTableName)
            If MappedSQLColumnNames NE '' then
                // Include the join columns among the columns being returned so their values can be updated in their
                // joined OpenInsight columns (if applicable).
                MappedSQLColumnNames   := @FM : SQL_Services('GetMappedJoinedSQLColumnNames', OITableName, SQLTableName)
                Swap @FM with ', ' in MappedSQLColumnNames
                SQLSelectStatement     := 'SELECT '
                SQLSelectStatement     := MappedSQLColumnNames : ' '
                SQLSelectStatement     := 'FROM ' : SQLTableName : ' '
                SQLSelectStatement     := 'WHERE '
                SQLSelectStatement     := JoinClause : ' '
            end
        end

    end else
        Error_Services('Add', 'The OITableName, SQLTableName, or OIKeyID argument was missing in the ' : Service : ' service.')
    end

    Response    = SQLSelectStatement

end service


//----------------------------------------------------------------------------------------------------------------------
// GetSQLDeleteStatements
//
// OITableName  - The name of the OpenInsight table that is mapped to the SQL table. - [Required]
// OIKeyID      - The Key ID to the OpenInsight row being deleted from SQL. - [Required]
// OIRow        - The OpenInsight row associated with the OpenInsight Key ID. - [Optional]
//
// Returns an @FM delimited list of SQL delete statements for the SQL table mapped to the indicated OpenInsight table.
// This will automatically create all SQL delete statements for secondary SQL tables as well. Note: OIRow will likely be
// an empty string since most join clauses will be based on the OpenInsight Key ID.
//----------------------------------------------------------------------------------------------------------------------
Service GetSQLDeleteStatements(OITableName, OIKeyID, OIRow)

    Convert @Lower_Case to @Upper_Case in OITableName
    SQLDeleteStatements = ''

    If (OITableName NE '') AND (OIKeyID NE '') then

        // Start with the primary SQL table that is mapped to this OpenInsight table.
        PrimarySQLTableName     = SQL_Services('GetPrimarySQLTable', OITableName)
        JoinClause              = SQL_Services('GetJoinClause', OITableName, PrimarySQLTableName, OIKeyID, OIRow, '', True$)
        SQLDeleteStatement      = 'DELETE FROM ' : PrimarySQLTableName : ' '
        SQLDeleteStatement     := 'WHERE '
        SQLDeleteStatement     := JoinClause
        SQLDeleteStatements    := SQLDeleteStatement : @FM

        // Add any secondary SQL table inserts that might be required.
        SecondarySQLTableNames  = SQL_Services('GetSecondarySQLTables', OITableName)
        If SecondarySQLTableNames NE '' then
            For Each SecondarySQLTableName in SecondarySQLTableNames using @FM
                JoinClause              = SQL_Services('GetJoinClause', OITableName, SecondarySQLTableName, OIKeyID, OIRow, '', True$)
                SQLDeleteStatement      = 'DELETE FROM ' : SecondarySQLTableName : ' '
                SQLDeleteStatement     := 'WHERE '
                SQLDeleteStatement     := JoinClause
                SQLDeleteStatements    := SQLDeleteStatement : @FM
            Next SecondarySQLTableName
        end

        SQLDeleteStatements[-1, 1]  = ''
    end else
        Error_Services('Add', 'The OITableName or OIKeyID argument was missing in the ' : Service : ' service.')
    end

    Response    = SQLDeleteStatements

end service


//----------------------------------------------------------------------------------------------------------------------
// PostSQLRequest
//
// This service can be used to post SQL queries to databases that are not reachable from the originating server.
// Requests are processed within the ProcessSQLRequests service.
//
// Input:
//  ServerID - Key to SERVERS record containing external SQL database connection string
//  Query    - SQL query to execute 
//
// Output:
//  Response - Query response
//----------------------------------------------------------------------------------------------------------------------
Service PostSQLRequest(ServerID, Query)

    Response = ''
    If (ServerID NE '') and (Query NE '') then
        RequestDate                       = Date()
        RequestTime                       = Time()
        RequestKeyID                      = @Station:'*':RequestDate :'*':RequestTime
        RequestRow                        = ''
        RequestRow<SQL_REQUESTS.DB_PATH$> = ServerID
        RequestRow<SQL_REQUESTS.QUERY$>   = Query
        Database_Services('WriteDataRow', 'SQL_REQUESTS', RequestKeyID, RequestRow, True$, False$, True$)
        If Error_Services('NoError') then
            TimeExpired = False$
            Start       = GetTickCount()
            Loop
                RequestRow      = Database_Services('ReadDataRow', 'SQL_REQUESTS', RequestKeyID)
                ResponseDate    = RequestRow<SQL_REQUESTS.RESPONSE_DATE$>
                If ResponseDate NE '' then
                    Response = RequestRow<SQL_REQUESTS.RESPONSE$>
                    Response = SRP_Decode(Response)
                end
                // Time will expire after 60 seconds.
                If GetTickCount() - Start GE 60000 then TimeExpired = True$
            Until (ResponseDate NE '') OR TimeExpired
                Sleepery(10)
                WinYield()
                Yield();Yield();Yield();Yield();Yield();Yield();Yield();Yield()
            Repeat
            If TimeExpired then
                Error_Services('Add', 'SQL request timed out in the ' : Service : ' service.')
            end
            Open 'SQL_REQUESTS' to hSQLRequests then
                Delete hSQLRequests, RequestKeyID else
                    Null
                    //Error_Services('Add', 'Error in ':Service:' service. Failed to delete SQL_REQUESTS ':RequestKeyID)
                end
            end
        end
    end else
        If RowExists('SQL_REQUESTS', RequestKeyID) then
            Open 'SQL_REQUESTS' to hSQLRequests then
                Delete hSQLRequests, RequestKeyID else
                    Null
                    //Error_Services('Add', 'Error in ':Service:' service. Failed to delete SQL_REQUESTS ':RequestKeyID)
                end
            end
        end
        Error_Services('Add', 'Null parameter passed into service call. All parameters are required.')
    end

return


//----------------------------------------------------------------------------------------------------------------------
// PostSQLStatement
//
// This service can be used to post SQL statements to databases that are not reachable from the originating server.
// Requests are processed within the ProcessSQLRequests service.
//
// Input:
//  ServerID  - Key to SERVERS record containing external SQL database connection string
//  Statement - SQL statement to execute 
//
// Output:
//  Response - Query response
//----------------------------------------------------------------------------------------------------------------------
Service PostSQLStatement(ServerID, Statement)

    Response = ''
    If (ServerID NE '') and (Statement NE '') then
        RequestDate                       = Date()
        RequestTime                       = Time()
        RequestKeyID                      = @Station:'*':RequestDate :'*':RequestTime
        RequestRow                        = ''
        RequestRow<SQL_REQUESTS.DB_PATH$> = ServerID
        RequestRow<SQL_REQUESTS.QUERY$>   = Statement
        Database_Services('WriteDataRow', 'SQL_REQUESTS', RequestKeyID, RequestRow, True$, False$, True$)
        If Error_Services('HasError') then
            Error_Services('Add', 'Error in ':Service:' service. Failed to post SQL statement to queue!')
        end
    end else
        If RowExists('SQL_REQUESTS', RequestKeyID) then
            Open 'SQL_REQUESTS' to hSQLRequests then
                Delete hSQLRequests, RequestKeyID else
                    Error_Services('Add', 'Error in ':Service:' service. Failed to delete SQL_REQUESTS ':RequestKeyID)
                end
            end
        end
        Error_Services('Add', 'Null parameter passed into service call. All parameters are required.')
    end

return


//----------------------------------------------------------------------------------------------------------------------
// ProcessSQLRequests
//
// Service that attempts to process external SQL requests. These requests are queued in the SQL_REQUESTS table.
//----------------------------------------------------------------------------------------------------------------------
Service ProcessSQLRequests()

    hSysLists           = Database_Services('GetTableHandle', 'SYSLISTS')
    Lock hSysLists, ServiceKeyID then
        Tablename           = 'SQL_REQUESTS'
        hSQLRequests = Database_Services('GetTableHandle', Tablename)
        If Error_Services('NoError') then
            GoSub ClearCursors
            Sentence    = 'SELECT ':Tablename:' WITH RESPONSE_DATE EQ "" BY REQUEST_DATE BY REQUEST_TIME'
            Set_Status(0)
            Update_Index(Tablename, 'REQUEST_DATE', False$, True$)
            Update_Index(Tablename, 'REQUEST_TIME', False$, True$)
            Update_Index(Tablename, 'RESPONSE_DATE', False$, True$)
            Update_Index(Tablename, 'RESPONSE_TIME', False$, True$)
            RList(Sentence, TARGET_ACTIVELIST$, '', '', '')
            EOF         = False$
            Loop
                ReadNext RequestKeyID else EOF = True$           
            Until EOF                
                RequestRow                              = Database_Services('ReadDataRow', 'SQL_REQUESTS', RequestKeyID)
                ServerID                                = RequestRow<SQL_REQUESTS.DB_PATH$>
                Query                                   = RequestRow<SQL_REQUESTS.QUERY$>
                Response                                = SQL_Services('GetDataRows', ServerID, Query, 3)
                Response                                = SRP_Encode(Response)
                RequestRow<SQL_REQUESTS.RESPONSE_DATE$> = Date()
                RequestRow<SQL_REQUESTS.RESPONSE_TIME$> = Time()
                RequestRow<SQL_REQUESTS.RESPONSE$>      = Response
                Database_Services('WriteDataRow', 'SQL_REQUESTS', RequestKeyID, RequestRow, True$, False$, True$)
                KeyLocked                               = Database_Services('IsKeyIDLocked', 'SQL_REQUESTS', RequestKeyID)
                If KeyLocked then Database_Services('ReleaseKeyIDLock', 'SQL_REQUESTS', RequestKeyID)
            Repeat
            // Cleanup really old requests (i.e. those that are older than 48 hours)
            GoSub ClearCursors
            Set_Status(0)
            CleanDate = Date() - 1
            Sentence  = 'SELECT ':Tablename:' WITH RESPONSE_DATE NE "" AND WITH RESPONSE_DATE LT ':Quote(OConv(CleanDate, 'D4/'))
            RList(Sentence, TARGET_ACTIVELIST$, '', '', '')
            EOF       = False$
            Loop
                ReadNext RequestKeyID else EOF = True$
            Until EOF
                Database_Services('DeleteDataRow', 'SQL_REQUESTS', RequestKeyID)
                KeyLocked = Database_Services('IsKeyIDLocked', 'SQL_REQUESTS', RequestKeyID)
                If KeyLocked then Database_Services('ReleaseKeyIDLock', 'SQL_REQUESTS', RequestKeyID)
            Repeat
            // Cleanup old requests that may have been "abandoned" by the requesting process (i.e. those that are older
            // than two minutes).
            GoSub ClearCursors
            Set_Status(0)
            CleanTime = Time() - 240
            Sentence  = 'SELECT ':Tablename:' WITH RESPONSE_DATE NE "" AND WITH REQUEST_TIME LT ':Quote(OConv(CleanTime, 'MT'))
            RList(Sentence, TARGET_ACTIVELIST$, '', '', '')
            EOF       = False$
            Loop
                ReadNext RequestKeyID else EOF = True$
            Until EOF
                Database_Services('DeleteDataRow', 'SQL_REQUESTS', RequestKeyID)
                KeyLocked = Database_Services('IsKeyIDLocked', 'SQL_REQUESTS', RequestKeyID)
                If KeyLocked then Database_Services('ReleaseKeyIDLock', 'SQL_REQUESTS', RequestKeyID)
            Repeat
        end else
            ErrorMsg = Error_Services('GetMessage')
        end
        Unlock hSysLists, ServiceKeyID else Null
    end

end service


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Internal GoSubs
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

ClearCursors:
    For counter = 0 to 8
        ClearSelect counter
    Next counter
return


WriteLog:
    Declare function Environment_Services, Logging_Services
    Declare subroutine Logging_Services
    LogPath     = Environment_Services('GetApplicationRootPath') : '\LogFiles\Replication'
    LogDate     = Oconv(Date(), 'D4/')
    LogTime     = Oconv(Time(), 'MTS')
    LogFileName = LogDate[7, 4] : '-' : LogDate[1, 2] : '-' : LogDate[4, 2] : ' WRITE_OI_Error.csv'
    Headers     = 'Logging DTM' : @FM : 'User' : @FM : 'Table' :@FM : 'Key'
    objLog      = Logging_Services('NewLog', LogPath, LogFileName, CRLF$, Comma$, Headers, '', False$, False$)
    LoggingDTM  = LogDate : ' ' : LogTime   ; // Logging DTM

    LogData       = ''
    LogData<1, 1> = LoggingDTM
    LogData<1, 2> = @User4
    LogData<1, 3> = OITableName
    LogData<1, 4> = OIKeyID
    LogData<1, 5> = Message
    Logging_Services('AppendLog', objLog, LogData, @FM, @VM)
return




