Hi Steve, have a look a this github repo, it has a lot of examples :

Here is what you are looking for call python methods from objectscript and vice versa :

/// embedded python example
Class ObjectScript.Embbeded.Python Extends %SwizzleObject
{

/// HelloWorld with a parameter
ClassMethod HelloWorld(name As %String = "toto") As %Boolean [ Language = python ]
{
    print("Hello",name)
    return True
}

/// Description
Method compare(modèle, chaine) As %Status [ Language = python ]
{
    import re

    # compare la chaîne [chaîne] au modèle [modèle]
    # affichage résultats
    print(f"\nRésultats({chaine},{modèle})")
    match = re.match(modèle, chaine)
    if match:
        print(match.groups())
    else:
        print(f"La chaîne [{chaine}] ne correspond pas au modèle [{modèle}]")
}

/// Description
Method compareObjectScript(modèle, chaine) As %Status
{
    w !,"Résultats("_chaine_","_modèle_")",!
    set matcher=##class(%Regex.Matcher).%New(modèle)                             
    set matcher.Text=chaine
    if matcher.Locate() {
        write matcher.GroupGet(1)
    }
    else {
        w "La chaîne ["_chaine_"] ne correspond pas au modèle ["_modèle_"]"
    }
}

/// Description
Method DemoPyhtonToPython() As %Status [ Language = python ]
{
    # expression régulières en python
    # récupérer les différents champs d'une chaîne
    # le modèle : une suite de chiffres entourée de caractères quelconques
    # on ne veut récupérer que la suite de chiffres
    modèle = r"^.*?(\d+).*?$"

    # on confronte la chaîne au modèle
    self.compare(modèle, "xyz1234abcd")
    self.compare(modèle, "12 34")
    self.compare(modèle, "abcd")
}

Method DemoPyhtonToObjectScript() As %Status [ Language = python ]
{
    # expression régulières en python
    # récupérer les différents champs d'une chaîne
    # le modèle : une suite de chiffres entourée de caractères quelconques
    # on ne veut récupérer que la suite de chiffres
    modèle = r"^.*?(\d+).*?$"

    # on confronte la chaîne au modèle
    self.compareObjectScript(modèle, "xyz1234abcd")
    self.compareObjectScript(modèle, "12 34")
    self.compareObjectScript(modèle, "abcd")
}

/// Description
Method DemoObjectScriptToPython() As %Status
{
    // le modèle - une date au format jj/mm/aa
    set modèle = "^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$"
    do ..compare(modèle, "10/05/97")
    do ..compare(modèle, " 04/04/01 ")
    do ..compare(modèle, "5/1/01")
}

}
/// embedded python example
Class ObjectScript.Embbeded.Python Extends %SwizzleObject
{

/// HelloWorld with a parameter
ClassMethod HelloWorld(name As %String = "toto") As %Boolean [ Language = python ]
{
    print("Hello",name)
    return True
}

/// Description
Method compare(modèle, chaine) As %Status [ Language = python ]
{
    import re

    # compare la chaîne [chaîne] au modèle [modèle]
    # affichage résultats
    print(f"\nRésultats({chaine},{modèle})")
    match = re.match(modèle, chaine)
    if match:
        print(match.groups())
    else:
        print(f"La chaîne [{chaine}] ne correspond pas au modèle [{modèle}]")
}

/// Description
Method compareObjectScript(modèle, chaine) As %Status
{
    w !,"Résultats("_chaine_","_modèle_")",!
    set matcher=##class(%Regex.Matcher).%New(modèle)                             
    set matcher.Text=chaine
    if matcher.Locate() {
        write matcher.GroupGet(1)
    }
    else {
        w "La chaîne ["_chaine_"] ne correspond pas au modèle ["_modèle_"]"
    }
}

/// Description
Method DemoPyhtonToPython() As %Status [ Language = python ]
{
    # expression régulières en python
    # récupérer les différents champs d'une chaîne
    # le modèle : une suite de chiffres entourée de caractères quelconques
    # on ne veut récupérer que la suite de chiffres
    modèle = r"^.*?(\d+).*?$"

    # on confronte la chaîne au modèle
    self.compare(modèle, "xyz1234abcd")
    self.compare(modèle, "12 34")
    self.compare(modèle, "abcd")
}

Method DemoPyhtonToObjectScript() As %Status [ Language = python ]
{
    # expression régulières en python
    # récupérer les différents champs d'une chaîne
    # le modèle : une suite de chiffres entourée de caractères quelconques
    # on ne veut récupérer que la suite de chiffres
    modèle = r"^.*?(\d+).*?$"

    # on confronte la chaîne au modèle
    self.compareObjectScript(modèle, "xyz1234abcd")
    self.compareObjectScript(modèle, "12 34")
    self.compareObjectScript(modèle, "abcd")
}

/// Description
Method DemoObjectScriptToPython() As %Status
{
    // le modèle - une date au format jj/mm/aa
    set modèle = "^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$"
    do ..compare(modèle, "10/05/97")
    do ..compare(modèle, " 04/04/01 ")
    do ..compare(modèle, "5/1/01")
}

}

Great article and very useful.

In one of my project, I have almost done the same but instead of using env var, I use an mounted xml file of default settings :

LoadSettings.mac in Interoperability NameSpace

ROUTINE LoadSettings

#Include Ensemble

UseXMLVariables() PUBLIC {

    Try {
        // mounted file of default settings
        set tFileName = "/usr/irissys/conf/DefaultSettings.xml"
        if ##class(%File).Exists(tFileName) {
            do ##class(Ens.Config.DefaultSettings).%Import(tFileName)
            write !,"File : "_tFileName_" loaded the defaultsettings",!
        }
    } Catch e {
        s ^%zStartupError=e.AsStatus()
    }
}

%ZSTART

ROUTINE %ZSTART

#include %occStatus
SYSTEM() PUBLIC {
    /*
        Initial plan to use this as startup configuration.
    */
    Try {
        New $namespace
        Set $namespace ="IRISAPP"

        Do UseXMLVariables^LoadSettings

    } Catch {}
}

/usr/irissys/conf/DefaultSettings.xml

<Export generator="IRIS" version="26" zv="IRIS for UNIX (Ubuntu Server LTS for x86-64 Containers) 2020.4 (Build 521U)" ts="2021-05-10 07:39:07">
<Document name="Ens.Config.DefaultSettings.ESD">
<defaultSettings>
<item production="*" item="FHIR_STU3_Default_Operation" class="*" setting="URL" value="http://fhirserver:52773/fhir/stu3/"><Deployable>true</Deployable></item>
</defaultSettings>
</Document></Export>

@Ben Spead

With Embedded Python, code can (and should?) not be stored in the databases and still be executed on the server side with the irispython interpreter.

Here is a GitHub link that demonstrates the use of embedded python with a backend on the Flask micro framework + Iris as a database.

It is true that Embedded python will change a lot of our paradigms, best practices, reflexes.

That's why we, as ObjectScript and IRIS experts, must try to use it as much as possible.

We will have to go out of our comfort zone and it is essential, it is up to us to go towards the python community because the opposite will not happen.

Regarding the [language = python] tag, I'm not saying it's useless. It can be useful in some cases.
I just think that it should not become the norm.

PS: My previous answer is indeed provocative and this is to draw attention to Embedded Python which is not a feature but a major evolution (Révolution ?).

The king is dead, long live the king! is a traditional phrase that is proclaimed at the advent of a new monarch in France.

May I say: IRIS is dead, long live IRIS!

This is what I feel with the arrival of Embedded Python.

The old monarch spoke ObjectScript, he allowed us to accomplish great things but now let's stop looking back, let's move forward. Let's induct the new monarch who speaks Python. Let's stop worshipping his father and let's make room for the new generation.

That's why I think, we must stop coding in ObjectScript, we must strive to use Python, everywhere in IRIS and this without the wrapper ([language = python]).

Why be so radical?

  1. Coding in Python is to show by example and in a language that the uninitiated know and thus prove the extent of the possibilities of IRIS
  2. Coding in Python is to start answering and finding solutions to the new problems that we will encounter:
    a. How to integrate .py files in our CICD pipes
    b. How to integrate PyPI with ZPM
    c. How to elegantly expose future APIs coded on the Flash Framework with IRIS and CSP gateways
  3. Coding in Python encourages InterSystems to produce new packages in Python rather than ObjectScript.

Choosing your programming language in IRIS is a political act.

  • Coding in ObjectScript is to be conservative.
  • Coding in Python is to be liberal.

Choose your side.

I add the powershell version :

Invoke-WebRequest -Uri 'https://login.intersystems.com/login/SSO.UI.Login.cls?referrer=https%253A//wrc.intersystems.com/wrc/login.csp' -SessionVariable session -Method POST -Body 'UserName=<Your Login>&Password=<Your Password>' 

Invoke-WebRequest -WebSession $session -Uri https://wrc.intersystems.com/wrc/WRC.StreamServer.cls?FILE=/wrc/Live/ServerKits/IRIS-2020.1.1.408.0-win_x64.exe -outfile "iris.exe"

The difference between a stored procedure and SQL inserts is that the business logic remains on the application side and not on the database side to keep the principles (storage/logic/representation) separate.

Nevertheless, the problem doesn't seem to be at the SQL level but rather at the BPL level, so maybe it's due to the conversion of XML to object?
If it is the case, use different technique to parse the XML, like the SAX Parser which avoids to mount all the XML document in memory.
https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls...

Where do you struggle ? Is it in the BPL, in the Operation or during the insert statement to SQL Server ?

If the bottleneck is in the insert statement check out this community SQL adapter (work only on JDBC).
https://openexchange.intersystems.com/package/ETL-Interoperability-Adapter

This adapter provide you an access to insert batch statement, that can speed up the insert speed up to 10 times even 100 times.

In the github repository you have some example how to use it :
https://github.com/grongierisc/BatchSqlOutboundAdapter/blob/master/src/C...

Hi Rubén,

Another proposition on IRIS 2021.1+ can be this one with the use of the new window (OVER) function :

ClassMethod getPersonsPagWindow(iAge As %Integer, sortField As %String = 1, sortOrder As %String = 2, pageSize As %String = 20, pageIndex As %String = 1) As %DynamicObject
{
    set out = []
    set vFrom = ((pageIndex -1 ) * pageSize)+1
    set vTo = vFrom + (pageSize-1)

    set sql = "SELECT * "_
                "FROM ( SELECT persons.* "_
                "        , ROW_NUMBER() OVER (ORDER By "_sortField_" "_ $CASE(sortOrder,1:"ASC",2:"DESC",:"ASC")_
                "    ) rn "_
                "        FROM Sample.Person persons where Age > ? "_
                "    ) tmp "_
                "WHERE rn between "_vFrom_" and "_vTo_" "_
                "ORDER By "_sortField_" "_ $CASE(sortOrder,1:"ASC",2:"DESC",:"ASC")

    Set rs=##class(%ResultSet).%New("%DynamicQuery:SQL")
    set sc = rs.Prepare(sql)
    set sc = rs.Execute(iAge) If $$$ISERR(sc) Do DisplayError^%apiOBJ(sc) Quit

    while rs.%Next() {
        Do out.%Push({
                "pid": (rs.%Get("ID")),
                "ssn" : (rs.%Get("SSN")),
                "lastname" : (rs.%Get("LastName")) ,
                "givenname":    (rs.%Get("GivenName")),
                "secondaryname":       (rs.%Get("SecondaryName")) ,
                "gender": (rs.%Get("Gender")),
                "age": (rs.%Get("Age") )
                })
    }

    set outJson = []
    Do outJson.%Push({
                "pageSize":(pageSize),
                "pageIndex":(pageIndex),
                "fromIndex":(vFrom),
                "toIndex":(vTo),
                "resultSet":(out)
                })
    return outJson
}

I bench the two solutions on a dataset of 100 000 rows without index with a result of 20 elements on page 1 and here are the results :

"getPersonsPag timed : 1,647 secondes"
"getPersonsPagWindow timed : 0,247 secondes"

I guess that the window function is faster because you don't have to fetch all the data in a global before paging.

Hi Gregor,

First off all, try to connect to MySql directly by a shell command :

echo "select 1" | isql -v my-connector

Expected response :

+---------------------------------------+
| Connected!                            |
|                                       |
| sql-statement                         |
| help [tablename]                      |
| quit                                  |
|                                       |
+---------------------------------------+
SQL> select 1
+---------------------+
| 1                   |
+---------------------+
| 1                   |
+---------------------+
SQLRowCount returns 1
1 rows fetched

Where /etc/odbc.ini :

[my-connector]
Description           = MySQL connection to  database
Driver                = MySQL
Database              = example
Server                = localhost
User                  = example
Password              = example
Port                  = 3306
Socket                = /var/run/mysqld/mysqld.sock

and /etc/odbcinst.ini

[MySQL]
Description = ODBC for MySQL
Driver = /usr/local/lib/libmyodbc8a.so
Setup = /usr/local/lib/libmyodbc8w.so
FileUsage = 1

If you successfully connected to your mysql database, then you can use it in IRIS/Caché/Ensemble :

Here is an example with %SQLGatewayConnection

set gc=##class(%SQLGatewayConnection).%New()
set pDSN="my-connector"
set sc=gc.Connect(pDSN,"example","example")
set sc=gc.AllocateStatement(.hstmt) 
set pQuery= "select 1"
set sc=gc.Prepare(hstmt,pQuery)
set sc=gc.Execute(hstmt)
set sc=gc.Fetch(hstmt)
set sc=gc.GetData(hstmt, 1, 1, .val)
zw val
set sc=gc.CloseCursor(hstmt)
set sc=gc.Disconnect()

To go further check those links :
- https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI...
- https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI...

Or even better check this training, it's with an JDBC connector but most part is applicable has DSN will fit your odbc config.
- https://github.com/grongierisc/formation-template

Hi Eric,

First you are using &sql who is for internal SQL use : doc

If you want to do an external query to a remote database you can do it with Ensemble :

Include EnsSQLTypes
 
Class Batch.Example.SqlInsertOperation Extends Ens.BusinessOperation
{
 
Parameter ADAPTER = "EnsLib.SQL.OutboundAdapter";
 
Property Adapter As EnsLib.SQL.OutboundAdapter;
 
Parameter INVOCATION = "Queue";
 
Method SetResultSetView(pRequest As Ens.StringRequest, Output pResponse As Ens.StringResponse) As %Status
{
    set tStatus = $$$OK
    
    try{
                    
        set pResponse = ##class(Ens.StringResponse).%New()
    
        set SqlInsertView = "INSERT into ODS_Products (ID,ProductName,Date_Alimentation) values (?,?,TO_DATE(?,'yyyy-mm-dd hh24:mi:ss'))"
 
        set param(1) = 1
        set param(1,"SqlType")=$$$SqlInteger
 
        set param(2) = ##class(%PopulateUtils).Name()
        set param(2,"SqlType")=$$$SqlVarchar
            
        set param(3) = $ZDATETIME($NOW(),3)
        set param(3,"SqlType")=$$$SqlVarchar
 
        set param = 3
            
        $$$ThrowOnError(..Adapter.ExecuteUpdateBatchParamArray(.nrows,SqlInsertView,.param))
                                
    }
    catch exp
    {
        Set tStatus = exp.AsStatus()
    }
 
    Quit tStatus
}
 
XData MessageMap
{
<MapItems>
    <MapItem MessageType="Ens.StringRequest">
        <Method>SetResultSetView</Method>
    </MapItem>
</MapItems>
}
 
}

Or with the %SQLGatewayConnection :

    //Create new Gateway connection object
   set gc=##class(%SQLGatewayConnection).%New()
   If gc=$$$NULLOREF quit $$$ERROR($$$GeneralError,"Cannot create %SQLGatewayConnection.")
       
   //Make connection to target DSN
   s pDSN="Samples"
   s usr="_system"
   s pwd="SYS"
   set sc=gc.Connect(pDSN,usr,pwd,0)
   If $$$ISERR(sc) quit sc
   if gc.ConnectionHandle="" quit $$$ERROR($$$GeneralError,"Connection failed")
       
   set sc=gc.AllocateStatement(.hstmt)
   if $$$ISERR(sc) quit sc
       
   //Prepare statement for execution
   set pQuery= "select * from Sample.Person"
   set sc=gc.Prepare(hstmt,pQuery)
   if $$$ISERR(sc) quit sc
     //Execute statement
   set sc=gc.Execute(hstmt)
   if $$$ISERR(sc) quit sc