Dan Pasco · Apr 28, 2021 go to post

Exactly - you haven't used the Gateways before. Why? IMO, the Gateways (mostly Java and DotNet) were a bit cumbersome to deal with. With ELS, we have default servers that can be easily managed and discovered, they require little or no configuration to get started (some ELS's do require some configuration if the language platform support is not discoverable by IRIS), and the interface is simple and direct.

One quick example, using Python, is to use the os module to get the current working directory:

USER>set python = $system.external.getPythonGateway()

USER>write python.invoke("os","getcwd")
/opt/intersystems/iris/xdbc/mgr

All that is required is a gateway connection to an external server, your code needs to be visible to that external server, either by direct placement into the default path for that language platform or by explicitly adding it by calling addToPath(), and public interfaces in your external code. By "external", I mean code that isn't written inside of the IRIS Server - ObjectScript.

When your external code writes to the "system output" device, that output is redirected to the IRIS current device. In my above example, the renderTable() function simply constructs a formatted string using the AsciiTable library (got it from GitHub) and writes it using System.out.println(formattedstring). I simply copied the output that was displayed in my IRIS session terminal window and pasted it in the original post. No extra work involved.

If your external code returns an object then you can indeed make use of that object as if it were a local IRIS Object - because it is. It is actually a network proxy object that communicates with the original external language object. That communication can actually be full duplex - your external code can talk to IRIS and IRIS can talk to your external code.

One really simple example that I think is quite profound is using JDBC in IRIS. This isn't JDBC Gateway - that still exists and it is what it is. I am talking about using a Java JAR file that contains a JDBC driver and you want to use it. I'll try a simple demo here. I have MariaDB installed on my system so I'll just use it to query a table I have defined there.

First - using mariaDB from the command line interface:

 ~ % mysql -u myusername -p
Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 4
Server version: 10.5.8-MariaDB Homebrew

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> show databases
    -> ;
+--------------------+
| Database           |
+--------------------+
| SAMPLES            |
| information_schema |
| mysql              |
| performance_schema |
| test               |
+--------------------+
5 rows in set (0.010 sec)

MariaDB [(none)]> use SAMPLES
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MariaDB [SAMPLES]> show tables;
+-------------------+
| Tables_in_SAMPLES |
+-------------------+
| person            |
+-------------------+
1 row in set (0.000 sec)

MariaDB [SAMPLES]> select * from person limit 5
    -> ;
+-----------------------+-------------+------------+-----------------------+------------+------------+----------+
| name                  | ssn         | dob        | home_street           | home_city  | home_state | home_zip |
+-----------------------+-------------+------------+-----------------------+------------+------------+----------+
| Basile,Molly M.       | 452-57-8033 | 1994-06-02 | 1153 First Street     | Xavier     | SD         | 98033    |
| Cooke,Howard F.       | 131-62-3894 | 2017-12-09 | 5172 Washington Place | Zanesville | NE         | 44980    |
| Donaldson,Phil R.     | 480-79-5019 | 1990-01-23 | 4429 Elm Street       | Miami      | WA         | 67638    |
| Eisenstien,Michael D. | 655-11-6334 | 1948-08-14 | 4676 Elm Avenue       | Reston     | KY         | 52729    |
| Faust,Mo E.           | 772-42-3921 | 2018-01-10 | 826 Maple Avenue      | Youngstown | OH         | 37180    |
+-----------------------+-------------+------------+-----------------------+------------+------------+----------+
5 rows in set (0.016 sec)

MariaDB [SAMPLES]>

Next, I'll follow up with an example of doing this from within IRIS.

Dan Pasco · Apr 28, 2021 go to post
    set java = $system.external.getJavaGateway()
    do java.addToPath("/home/myhome/mariadb-java-client-2.7.2.jar")
    set mariads = java.new("org.mariadb.jdbc.MariaDbDataSource","jdbc:mariadb://myhost:3306/SAMPLES")
    set mariaconn = mariads.getConnection("myuser","secret")
    set pstmt = mariaconn.prepareStatement("select * from person limit 5")
    set jdbcresult = pstmt.executeQuery()
    set jdbcmetadata = jdbcresult.getMetaData()
    for i=1:1:jdbcmetadata.getColumnCount() {
        write jdbcmetadata.getColumnName(i),$char(9)
    }
    while jdbcresult.next() {
        write !,$increment(rowcnt),$char(9)
        for i=1:1:jdbcmetadata.getColumnCount() {
            write jdbcresult.getString(i),$c(9)
        }
    }
    write !!!,"OR I COULD JUST USE MY ALREADY IMPLEMENTED RESULT SET RENDERER FROM ANOTHER JAVA PROJECT",!!!
    do java.addToPath(..#EXTERNALLIBPATH)
    set ers = java.new("external.test.ExternalResult",pstmt.executeQuery())
    do ers.renderTable()
    do mariaconn.close()
    do java.disconnect()

And the results:

USER>do ##class(external.test.MariaJDBC).communityDemo()
name    ssn    dob    home_street    home_city    home_state    home_zip    
1    Basile,Molly M.    452-57-8033    1994-06-02    1153 First Street    Xavier    SD    98033    
2    Cooke,Howard F.    131-62-3894    2017-12-09    5172 Washington Place    Zanesville    NE    44980    
3    Donaldson,Phil R.    480-79-5019    1990-01-23    4429 Elm Street    Miami    WA    67638    
4    Eisenstien,Michael D.    655-11-6334    1948-08-14    4676 Elm Avenue    Reston    KY    52729    
5    Faust,Mo E.    772-42-3921    2018-01-10    826 Maple Avenue    Youngstown    OH    37180    

OR I COULD JUST USE MY ALREADY IMPLEMENTED RESULT SET RENDERER FROM ANOTHER JAVA PROJECT

┌─────────────────────┬───────────┬──────────┬─────────────────────┬──────────┬──────────┬────────┐
│name                 │ssn        │dob       │home_street          │home_city │home_state│home_zip│
├─────────────────────┼───────────┼──────────┼─────────────────────┼──────────┼──────────┼────────┤
│Basile,Molly M.      │452-57-8033│1994-06-02│1153 First Street    │Xavier    │SD        │98033   │
├─────────────────────┼───────────┼──────────┼─────────────────────┼──────────┼──────────┼────────┤
│Cooke,Howard F.      │131-62-3894│2017-12-09│5172 Washington Place│Zanesville│NE        │44980   │
├─────────────────────┼───────────┼──────────┼─────────────────────┼──────────┼──────────┼────────┤
│Donaldson,Phil R.    │480-79-5019│1990-01-23│4429 Elm Street      │Miami     │WA        │67638   │
├─────────────────────┼───────────┼──────────┼─────────────────────┼──────────┼──────────┼────────┤
│Eisenstien,Michael D.│655-11-6334│1948-08-14│4676 Elm Avenue      │Reston    │KY        │52729   │
├─────────────────────┼───────────┼──────────┼─────────────────────┼──────────┼──────────┼────────┤
│Faust,Mo E.          │772-42-3921│2018-01-10│826 Maple Avenue     │Youngstown│OH        │37180   │
└─────────────────────┴───────────┴──────────┴─────────────────────┴──────────┴──────────┴────────┘
Dan Pasco · Jul 6, 2021 go to post

Hi Jonathan,

I am very interested in this problem (I wrote the early version of dynamic statement parameter binding). My first question is simple - can you execute this statement using embedded SQL? If you can then we need to dig further to discover the correct error (some errors get masked as syntax when there is a different problem).

Thanks,

Dan

PS:

Of course, you will have to supply values for ? to test compile this as an embedded statement (:hv1, :hv2... can work) - or - can you try this using an SQL utility or as a ODBC/JDBC statement?

Dan Pasco · Jul 6, 2021 go to post

I agree with Herman - to a point. Leave the IDKEY index alone and just accept the system assigned idkey. In most cases this is the best policy. But - there are potentially several "candidate keys", all can be defined - or not, but one can be chosen to be the "primary key". In that case, define the key not as unique but as "primarykey". Some tooling works better with a defined primary key. It does not interfere with the IDKEY unless it is also defined as the "idkey".

Dan Pasco · Jul 8, 2021 go to post

I didn't count the number of question marks but I assume you've verified that it matches the number of columns.

Also, there are limitations on the number of command line arguments you can pass but that number is rather high. I don't recall exactly what it is but it is certainly limited. At one time we established 16 as the maximum number of arguments. As I recall, we did nothing to impose an arbitrary limit but ObjectScript itself does have a limit.

My next questions are meant to establish where the error is reported. You can prepare a dynamic SQL statement and then execute it separately and multiple times. Is the error reported during prepare or during execute?

PS: (edited):

Sorry, I see now your code. You are using a HS Adapter to execute this statement. That leads me to a different question - can you prepare and execute this statement as a Dynamic SQL Statement?

set statement = ##class(%SQL.Statement).%New()

set status = statement.%Prepare(.tSQL)

If status is okay (1) then execute it - set result = statement.%Execute( <your parameter values go here>)

Then check result.%SQLCODE.

In the meantime, I will take a look at the Adapter (not my area but I can still look) to see if there is some limitation.

Dan Pasco · Jul 8, 2021 go to post

I suspect that is the case but I cannot verify it without seeing more of your code. Someone from our HealthShare team probably knows this but I don't work on that team. Sorry.

Dan Pasco · Dec 22, 2021 go to post

Hi Marcio,

I am a developer at InterSystems and I work with Java, JSON, and SQL every day. Perhaps I can help. Can you provide an example of the Java code where you receive the JSON output from IRIS? I think I have a couple of interesting options for you.

-Dan

Dan Pasco · Dec 24, 2021 go to post

Use the IRISList class:

        IRIS iris = IRIS.createIRIS(connection);

        IRISList list = new IRISList();
        list.add("this is a string");
        list.add(100);
        iris.set(list, "test",1);

USER>zw ^test
^test(1)=$lb("this is a string",100)

Dan Pasco · Jan 4, 2022 go to post

I like :sql as a way to launch the shell - so that means this alias should be renamed to something else!!

USER>:sql "select top 2 * from person"
do $system.SQL.Execute("select top 2 * from person").%Display()
id    name    ssn    dob    home_street    home_city    home_state    home_zip
1    Willeke,Thelma K.    934-46-2099    18948    308 Madison Blvd    Bensonhurst    HI    71501
2    Nathanson,Will P.    771-91-1008    37359    9664 Second Place    Jackson    VA    15837

2 Rows(s) Affected
Dan Pasco · Jan 4, 2022 go to post

That's because I defined that alias myself - on Linux it is the .iris_init file in my home directory. I don't know where that is on Windows...

Dan Pasco · Jan 4, 2022 go to post

Not quite - I just defined that alias this morning to test something out. I think that a pre-defined (shipped with Iris) sql alias should launch the shell - I didn't know anything about pre-installed/shipped with Iris aliases.

Dan Pasco · Jan 4, 2022 go to post

You can. Two different ways but both utilize the External Java Server with gateway connections. That is the same mechanism employed by LOAD DATA. If you have an example of what you want to do then I can provide you with a demo - using both options.

Dan Pasco · Jan 12, 2022 go to post

Interesting! I didn't see which version of InterSystems IRIS you are using but you might try a couple of things:

First, don't return a status value from your method. Instead return a %Stream.GlobalBinary instance. If you still want a status, get it some other way. I recommend just throwing an exception - IRIS Native should handle that okay. The object value you get back will be a proxy object that should allow you to read the stream. In our documentation you might search for "reverse proxy objects".

If get some time, I will try to build a sample of doing this.

Dan Pasco · Jan 12, 2022 go to post

Yes, of course "inverse" - sorry.

Persistent vs RegisteredObject - not a problem but you are calling a simple class method so we don't need any super class. I used this implementation for the IRIS Class:

ClassUtils.CSW1JavaFunctions{    ClassMethodIrisReturn(user = "user", pass = "pass") As%Stream.GlobalBinary    {         try { setcswStream=##class(%Stream.GlobalBinary).%New()             setcswReturn = {"user":(user), "pass":(pass) }             docswReturn.%ToJSON(cswStream)             returncswStream         } catchexc {             write !,"Caught Exception on server: ", exc.AsSQLMessage()         }    }}

 

And this is a crude hack at the Java code - the anonymous InputStream class could use more work but it does run for this simple example. I'll leave the rest of the InputStream coding to you.

package utils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.intersystems.jdbc.*;
import java.io.*;
import java.sql.SQLException;
public class Reader {
    public static final String CACHE_CLASS_NAME = "Utils.CSW1JavaFunctions";
    public IRISConnection connection;
    public IRIS iris;
    public Reader(IRISConnection connection) throws SQLException {
        this.connection = connection;
        this.iris = IRIS.createIRIS(connection);
}
    public static void main(String[] args) throws SQLException {
        IRISDataSource dataSource = new IRISDataSource();
dataSource.setServerName("localhost");
dataSource.setPortNumber(51776);
dataSource.setDatabaseName("USER");
dataSource.setUser("_SYSTEM");
dataSource.setPassword("SYS");
IRISConnection connection = (IRISConnection) dataSource.getConnection();
Reader reader = new Reader(connection);
        try {
            JsonNode jsonNode = reader.execute("IrisReturn", "java", "jpass");
System.out.println(jsonNode.toString());
} catch (Exception exc) {
            exc.printStackTrace();
}
    }

    public JsonNode execute(String method, Object... args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = null;
        try {
            IRISObject data  = (IRISObject) iris.classMethodObject(CACHE_CLASS_NAME, method, args[0], args[1]);
InputStream is = new InputStream() {
                byte[] buffer;
                int pos = 0;
                int len = -1;
@Override
public int read() throws IOException {
                    if (pos >= len) {
                        getBuffer();
}
                    if (len == -1) {
                      return -1;
}
                    return buffer[pos++];
}

                void getBuffer() {
                    pos = 0;
IRISReference readLen = new IRISReference(3200);
String string = (String) data.invoke("Read", readLen);
                    if (readLen.getLong() == -1) {
                        buffer = null;
len = -1;
} else {
                        buffer = string.getBytes();
len = buffer.length;
}
                }
            };
jsonNode = (JsonNode) mapper.readTree(is);
            return jsonNode;
} catch (Exception ex) {
            ex.printStackTrace();
}
        return null;
}
}

Running this produces this output:

/usr/lib/jvm/adoptopenjdk-11-hotspot-amd64/bin/java -javaagent:/home/...
{"user":"java","pass":"jpass"}

Process finished with exit code 0

Dan Pasco · Feb 22, 2022 go to post

First of all, the global names are not random but are based on a hash algorithm to reduce the length of the global name and to reduce the probability of global name collisions. This is the default global name assignment when USEEXTENTSET is true. The benefits of using EXTENTSET mapping are many but primarily, the size of indexes is reduced substantially making index filing is faster. Queries using indexes are also likely faster with USEEXTENTSET mapping.

With EXTENTSET, the storage default global is used as a base value for the set of globals used by the extent. Each index, including the master map/master data index (MDI) - also known as the "DATALOCATION", is the base value (the EXTENTLOCATION) plus ".n" where "n" is a number computed when mapping globals to indexes. The master map/MDI always is ".1". In the original post, the DEFAULTGLOBAL setting overrides the hash computation of the EXTENTLOCATION.

Compare the index reference for a simple name index on Sample.Person between USEEXTENTSET = 1 and USEEXTENTSET = 0. ^Sample.PersonI("PersonNameIndex","DOE, JOHN Q", 100) vs ^Sample.Person.2("DOE, JOHN Q", 100).

The developer has to choose whether to use conveniently named globals or better performance.

Note that not all class definitions/tables will benefit from USEEXTENTSET mapping. But many will.

Dan Pasco · Feb 22, 2022 go to post

When not using EXTENTSET mapping, all indexes are stored by default in a single global. To keep the index structures separate the first subscript of the index global, by default, is the index name. This creates two conditions that impact performance negatively. Firstly, the index name subscript creates a longer reference for each index key. That leaves less room for index key subscripts and longer references consume resources. Secondly, the size of the index global is increased and the number of index key values per block is reduced. Fewer key values per block means more blocks read during query execution and a less efficient global cache. The index name subscript is essentially "noise".

Using EXTENTSET mapping removes the need for an index identifying subscript (the global is the index).

Dan Pasco · Mar 24, 2022 go to post

There is a project that will be available soon (don't ask me to define "soon") that will allow the Java programmer to load and compile sources from the local system into an IRIS Server. The IRIS Server does not need to be running on the same system where the files are located.

This example is slightly old as the current implementation of load() returns a list of items loaded and compile() returns a list of items compiled. There are load implementations that accept directories, individual files, any Java streamable, and also JAR files.

SourceLoader sourceLoader = new SourceLoader(connection);
Path path = Paths.get("path/to/samples/cls/Sample");
sourceLoader.load(path, null);
sourceLoader.compile(true);
Dan Pasco · Mar 24, 2022 go to post

This project will be available as intersystems-utils, version 4.0.0. It is in the pipeline now.

Dan Pasco · Jun 1, 2022 go to post

%Library.ResultSet remains in the product for backward compatibility reasons but there are better ways to execute class queries. Any class query can be projected as a table valued function (TVF). TVF's can be executed if the class query also declares SQLPROC. A TVF can be included in the FROM item list, it can be joined with other FROM items, it can be ordered, restricted and a subset of available columns made available. Here is a simple example from the Sample.Person class:

select id,name,dob from sample.SP_Sample_By_Name('Ad') order by dob desc

I populated Sample.Person with some generated data and ran the above statement:

 
ID Name DOB
855 Adams,Elvira X. 03/16/2021
1378 Adams,Ed L. 01/15/2018
477 Adams,Debra S. 10/01/2015
1341 Adam,Chad U. 10/20/2013
32 Adam,Dmitry N. 10/28/2010
1099 Adams,Pam Z. 10/20/1993
897 Adam,Joe Y. 02/23/1984
1469 Adam,Phyllis N. 04/20/1982
358 Adam,Liza H. 12/13/1980
1096 Adam,Belinda Z. 08/02/1975
1269 Adam,Charlotte P. 03/03/1974
1396 Adams,Robert E. 03/14/1973
1109 Adams,Quigley H. 01/01/1968
454 Adam,Amanda A. 01/22/1964
856 Adams,Lawrence A. 03/23/1961
1104 Adam,Stavros O. 02/24/1948
1179 Adam,Pam A. 05/16/1941
426 Adams,Brian M. 01/15/1928

18 row(s) affected

And you can also execute this using a dynamic statement:

USER>set result = $system.SQL.Execute("select id,name,dob from sample.SP_Sample_By_Name('Ad') order by dob desc")

USER>write result.%Next()
1
USER>write result.Name
Adams,Elvira X.
Dan Pasco · Jul 6, 2022 go to post

I have to throw in my opinions and possibly a few facts regarding nulls and unique constraints.

IRIS Unique index - this is primarily a syntactical shortcut as it defines not only an index but also a unique constraint on the index key. Most pure SQL implementations don't merge the two concepts and the SQL standard doesn't define indexes. The SQL Standard does define unique constraints. Keep in mind that both IDKEY and PRIMARYKEY are modifiers of a unique constraint (and, in our world, the index defined as IDKEY is also special). There can be at most one index flagged as IDKEY and one that is flagged as PRIMARYKEY. An index can be both PRIMARYKEY and IDKEY.

There was once an SQL implementation that defined syntax for both "unique index" and "unique constraint" with different rules. The difference between them was simple - if an index is not fully populated (not all rows in the table appear in the index - we call this a conditional index) then the unique index only checked for uniqueness in the rows represented in the index. A unique constraint applies to all rows.

Also keep in mind that an index exists for a singular purpose - to improve the performance of a subset of queries. Any SQL constraint can be expressed as a query.

The SQL Standard is a bit inconsistent when it comes to null behavior. In the Framework document there is this definition:

A unique constraint specifies one or more columns of the table as unique columns. A unique constraint is satisfied if and only if no two rows in a table have the same non-null values in the unique columns.

In the Foundation document, there exists two optional features, F291 and F292. These features define a unique predicate (291) and unique null treatment (292). These features appear to provide syntax where the user can define the "distinct-ness" of nulls. Both are optional features, both are relatively recent (2003? 2008?). The rule when these features are not supported is left to the implementor.

IRIS is consistent with the Framework document statement - all constraints are enforced on non-null keys only. A "null" key is defined as a key in which any key column value is null.

Dan Pasco · Jul 27, 2022 go to post

I submitted some feedback to documentation regarding the content you cited.

The line in question originated from something I wrote while implementing the %On*Finally set of methods. The "calling method" in this context refers to the method that invokes the %On*Finally() method. In this case, %Save() is the calling method.

%Save may or may not initiate a transaction, depending on a few factors. If %Save does initiate a transaction then that transaction is complete prior to the %OnSaveFinally() call.

Dan Pasco · Jul 12, 2023 go to post

Warning: what I describe here is not part of any released IRIS version. It is still in development and details might change.

We are working on an implementation of something we call "ASQ". This is intended to be a superset of the ISO Standard "JSON Path Language" (JPL). I suspect our current implementation will not meet all of your requirements but perhaps you will find this interesting:

USER>set obj = {"meta":{"versionId":"2","security":[{"system":"http://terminology.hl7.org/CodeSystem/v3-Confidentiality","code":"U","display":"unrestricted"}]}}

USER>zw obj.apply("$[*].meta.security[*].system")
["http://terminology.hl7.org/CodeSystem/v3-Confidentiality"]  ; <DYNAMIC ARRAY>

We can repeat this same pattern but remove the security field value's enclosing array and we see the same result:

USER>set obj = {"meta":{"versionId":"2","security":{"system":"http://terminology.hl7.org/CodeSystem/v3-Confidentiality","code":"U","display":"unrestricted"}}}

USER>zw obj.apply("$[*].meta.security[*].system")
["http://terminology.hl7.org/CodeSystem/v3-Confidentiality"]  ; <DYNAMIC ARRAY>

Of course there is more, much more. I have more complex examples if you are interested.

-Dan

Dan Pasco · Aug 26, 2023 go to post

Property methods, or any member methods in general, originate from three places: the property's memberType class, the declared type class (sometimes call the "datatype" class) and the containing class definition.

The memberType is simply the type of class member - property, index, query, method, parameter - all are types of members that can be defined in a class. What isn't well known is that there is a memberType class for several memberTypes. (in the past, this was referred to as the property class) We'll focus on the property memberType here but keep in mind that the concepts extend to indexes, queries, triggers, etc.

There are two categories of property methods. Methods inherited from the datatype class collectively provide the datatype class runtime for the datatype generated specifically for a property. These methods are classmethods but they behave as if they are operating on an instance of the datatype class. That instance is passed as an argument when invoking the method or returned by the method. We think of these methods as running in a provided context.

The second category of property methods is those inherited from the memberType classes. Yes, I wrote classes - plural. That is because in addition to the memberType classes that are automatically inherited by each property, the class definition can specify additional classes as the value of the PropertyClass class keyword. To specify multiple classes simply enclose the list of classes in parentheses.

Class User.Special Extends%RegisteredObject [ PropertyClass = User.MyProperty ]
{

Property MyProperty As%String;
}

In this example, each property defined in User.Special will inherit methods implemented by User.MyProperty. Before we look at User.MyProperty we need to understand another class keyword - NoContext. There is also a NoContext method keyword. When used as a class keyword it applies to all methods that do not also specify NoContext.

The documentation for NoContext describes the code generation implications of using NoContext but does not explain what it means. Perhaps that is because the keyword name implies something other than its compile-time behavior. NoContext simply means that the method has access to the current instance. In other words, the member method does not provide its own context. What member methods do provide their own context? Datatype methods.

Why do we care about context? Simply put, datatype methods should not have the ability to mutate the object since they are intended to implement only the datatype behavior and can access only the provided value.

Class User.MyProperty [ NoContext ]
{

Method IsSpecial() As%Boolean [ CodeMode = generator ]
{
    set ivar = "i%"_$$$QN(%property)
    $$$GENERATE(" return $select(+"_ivar_"#2>0:1,1:0)")
}

}

This is a simple little example of a property method generator. When the Special class is compiled it will as an IsSpecial property method to each property defined by the class.

MyPropertyIsSpecial() methodimpl {
 return$select(+i%MyProperty#2>0:1,1:0)
}

And when you instantiate Special you can invoke this method.

USER>set special = ##class(Special).%New()

USER>set special.MyProperty = "200"

USER>write special.MyPropertyIsSpecial()
0
USER>set special.MyProperty = 201

USER>write special.MyPropertyIsSpecial()
1
Dan Pasco · Aug 28, 2023 go to post

$$$QN is a macro that we use in code generators to delimit identifiers. It returns either the parameter value unaltered or the quoted parameter. For example, if you have a property named select then it has to be delimited for use in SQL since it is a reserved word. $$$QN("case") returns "case".

Dan Pasco · Sep 13, 2023 go to post

Interesting question. There is no good option to manipulate the ObjectRegistry but you do have options to access stored data. My recommended option you've already taken off the table.

The FOREACH=ROW/OBJECT trigger is by far the best and easiest solution if you require access to the version of an object that is currently stored. There are several reasons why this is the best option, perhaps the most important being that ROW/OBJECT triggers are consistently applied between Objects and SQL. Of course, the same restrictions exist for triggers that exist for %OnBeforeSave - we don't recommend modifying objects/rows.

Another option is to use <property>GetStored. This may not work for every situation but it does allow code to retrieve the value of a property directly from storage. I believe this is restricted to what we call "default storage". That isn't an entirely true statement but it is for common storage types (SQL Mapped storage being the other common type).

USER>zw ^demo.person.1(1)
^demo.person.1(1)=$lb("Doe, John","123 Main Street","Cambridge","MA","02142","[{""type"":""mobile"",""country_code"":""1"",""number"":""999-999-9999""},{""type"":""work"",""country_code"":""1"",""number"":""888-888-8888""}]")

USER>set person = ##class(demo.intersystems.Person).%OpenId(1)

USER>write person.name
Doe, John
USER>set person.name = "Richard, Maurice"

USER>write person.nameGetStored(person.%Id())
Doe, John
USER>write person.name
Richard, Maurice

Of course, you can always use SQL to retrieve stored values.

For the intrepid explorer, there is another option. This code isn't supported as the %Load* api's aren't supposed to be public. I'm sure most people have examined generated code and have discovered these methods so I'm divulging any big secrets here. First, the results and then the code.

USER>set person = ##class(demo.intersystems.Person).%OpenId(1)

USER>set person.name = "Enono, William"

USER>set sperson = ##class(demo.intersystems.Person).GetStored(person.%Id())

USER>write sperson.name
Leavitt, Timothy
USER>write person.name
Enono, William

Fair warning - this code isn't supported, it is considered to be user-implemented code. I also didn't rigorously test this code. I added this class method to my demo.intersystems.Person class:

ClassMethod GetStored(id As%Integer) As demo.intersystems.Person
{
    try {
        set obj = ..%New()
        set cur = obj.%Concurrencyset obj.%Concurrency = 0$$$THROWONERROR(sc,obj.%LoadInit(,,1))
        $$$THROWONERROR(sc, obj.%LoadData(id))
        set obj.%Concurrency = cur
        do obj.%SetModified(0)
    } catch e {
        set obj = $$$NULLOREF
    }
    return obj
}

The object referenced by the oref returned by this code is not assigned an id. If you were to save this object it would create a new stored object and it would be assigned its own ID. Exercise caution.

Dan Pasco · Sep 13, 2023 go to post

I don't recommend using the "embedded extent" model. It might be possible to achieve this model but it isn't simple to do and it does produce some odd behavior. I could, reluctantly, describe how this is done. It may not apply to this case anyway. The default serialization of a LIST collection is as an embedded list and we do not support - currently - a serialized list as a child table.

This is an example of a MANY to MANY relationship. IRIS does not support this relationship type. Rather, we recommend using the Associative Entity model where there is a third class/table that maintains the relationship. The structure of the AE class/table is quite simple, consisting of two properties/columns, one referencing of the related classes and the other referencing the other related classes.

If restructuring the user class is not possible then the %OnDelete recommendation is good. Perhaps it would be better to use a FOREACH=ROW/OBJECT, ONAFTER=DELETE trigger to iterate over the collection and delete each referenced item. Triggers have the added advantage of working with both Object and SQL filing operations. Keep in mind that collections use OID format when crafting the delete.

Dan Pasco · Oct 23, 2023 go to post

Hi,

Using the new(ish) External Java Server and Gateway connection, this worked for me. I copied your class, refactored it for the Apache PDFBox version 3.0.0 and then called it from the command line. I did receive some warnings that have nothing to do with PDFBox, they are related to the larger project that I placed this code into. The call worked just fine. I don't know exactly when the new external language server/gateway support was added but it is documented in 2021.1: https://docs.intersystems.com/iris20211/csp/docbook/DocBook.UI.Page.cls?KEY=BEXTSERV_intro. I'm pasting the IRIS Session commands I used:

USER>set java = $system.external.getJavaGateway()

USER>do java.addToPath("<path to my project/target/intersystems-demo-1.0-SNAPSHOT.jar")

USER>set text = java.invoke("demo.intersystems.utils.PdfToText","getText","/home/danp/Downloads/DM32.2-2017-00157-ambiguity-in-JSON-array-constructor.pdf")

USER>zw text
text="ISO/IEC JTC1/SC32 WG3:CMH-023"_$c(10)_"ANSI INCITS DM32.2-2017-00157"_$c(10)_"- 1 of 7 -"_$c(10)_"Title: Ambiguity in <JSON array constructor>"_$c(10)_"Author: <<snipped>>

I even have a nice way to return the text as an instance of %Stream.Object if that is interesting to you.

HTH,

Dan