Article
· Jan 19 4m read

Querying Dynamic Data using ASQ

IRIS 2023.3.0 ASQ

Abstract Set Query, ASQ, is a super set of JSON Path Language, JPL, which was first introduced by the 2016 ISO SQL Standard. JPL is only defined within the context of SQL. In 2023.3, we introduced classes that implement ASQ. It works on any implementation of %Library.AbstractSet. The complete syntax for ASQ will be documented later. This is just a quick introduction to a few of its features. This implementation comes with a nice utility as well. You will know it when you see it!

First, let's get some data from the NHL database. This step requires a valid SSL/TLS configuration. I named mine Default.

USER>set data = ##class(%Net.Http).GetJson("https://api.nhle.com/stats/rest/en/team",{"SSLConfiguration":"Default"})

This may take a few seconds to complete. This is a response object. It has top level fields of - well, let's see. I could just do a pretty print of the data but it is quite large. I just want the field names.

USER>set iterator = data.iterator() while iterator.hasNext() { write !,iterator.next().key }  
httpStatus
message
content

It is content we are interested in so we'll do this again, this time iterating over the content field value. I suppose I should add a feature to print a tree of the data. Maybe next week.

USER>set iterator = data.content.iterator() while iterator.hasNext() { write !,iterator.next().key }
data
total

Finally - data. let's take a look.

I want to point out something here. The code I ran didn't change other than the object from which I obtained the iterator. The rest of the command remained the same. In a few lines of code, I could implement the tree display. There is a similar function that is in 2023.3.0 that might be of interest. So let's grab one instance of the data array (I know it is an array because I peeked. Sorry.)

USER>:pp data.content.data.get(0)
do ##class(%ASQ.SetUtils).pp(data.content.data.get(0))
{
  "id": 11,
  "franchiseId": 35,
  "fullName": "Atlanta Thrashers",
  "leagueId": 133,
  "rawTricode": "ATL",
  "triCode": "ATL"
}

I'm using an alias here. You can see the call that it makes - %ASQ.SetUtils.pp. Just pass in the oref.

The data we retrieved contains the teams currently in the NHL. Let's apply a few ASQ expressions, just to play a bit. The first part of an ASQ expression is the base value. It is referenced as '$'.

USER>:pp data.apply("$.content.data[0 to 3]") 
do ##class(%ASQ.SetUtils).pp(data.apply("$.content.data[0 to 3]"))
[
  {
    "id": 11,
    "franchiseId": 35,
    "fullName": "Atlanta Thrashers",
    "leagueId": 133,
    "rawTricode": "ATL",
    "triCode": "ATL"
  },
  {
    "id": 34,
    "franchiseId": 26,
    "fullName": "Hartford Whalers",
    "leagueId": 133,
    "rawTricode": "HFD",
    "triCode": "HFD"
  },
  {
    "id": 31,
    "franchiseId": 15,
    "fullName": "Minnesota North Stars",
    "leagueId": 133,
    "rawTricode": "MNS",
    "triCode": "MNS"
  },
  {
    "id": 32,
    "franchiseId": 27,
    "fullName": "Quebec Nordiques",
    "leagueId": 133,
    "rawTricode": "QUE",
    "triCode": "QUE"
  }
]

That expression just retrieved the members of the data array from 0 to 3 - four of them. The [0 TO 3] expression is called an array accessor expression. It allows a wildcard as [*], index as [5] for example, a relative index as [last], range as [1 TO 3], a comma list of index, range and relative. [0, 2, 4, 5 TO 7, last]. The order of the index expressions doesn't matter but they will be returned in the order you specify:

USER>:pp data.content.data.apply("$[0 TO 1,last]")
do ##class(%ASQ.SetUtils).pp(data.content.data.apply("$[0 TO 1,last]"))
[
  {
    "id": 11,
    "franchiseId": 35,
    "fullName": "Atlanta Thrashers",
    "leagueId": 133,
    "rawTricode": "ATL",
    "triCode": "ATL"
  },
  {
    "id": 34,
    "franchiseId": 26,
    "fullName": "Hartford Whalers",
    "leagueId": 133,
    "rawTricode": "HFD",
    "triCode": "HFD"
  },
  {
    "id": 9,
    "franchiseId": 30,
    "fullName": "Ottawa Senators",
    "leagueId": 133,
    "rawTricode": "OTT",
    "triCode": "OTT"
  }
]
USER>:pp data.content.data.apply("$[last, 0 TO 1]")
do ##class(%ASQ.SetUtils).pp(data.content.data.apply("$[last, 0 TO 1]"))
[
  {
    "id": 9,
    "franchiseId": 30,
    "fullName": "Ottawa Senators",
    "leagueId": 133,
    "rawTricode": "OTT",
    "triCode": "OTT"
  },
  {
    "id": 11,
    "franchiseId": 35,
    "fullName": "Atlanta Thrashers",
    "leagueId": 133,
    "rawTricode": "ATL",
    "triCode": "ATL"
  },
  {
    "id": 34,
    "franchiseId": 26,
    "fullName": "Hartford Whalers",
    "leagueId": 133,
    "rawTricode": "HFD",
    "triCode": "HFD"
  }
]

Then comes the member accessor. The member accessor is simply a dot followed by the member name, also known as the field name. There is also a member wildcard, also indicated by an asterisk. Member accessors are necessary and useful but not very exciting.

What is more exciting is the behavior when a member value is an array. We can filter any value - be it an array or an object. Probably even a literal. So let's get a list of all of the Teams in our data whose name begins with the letter 'M'. In this expression, the @ in the filter expression refers to the current value.

USER>:pp data.content.data.apply("$[*]?(@.fullName STARTS WITH 'M').fullName")
do ##class(%ASQ.SetUtils).pp(data.content.data.apply("$[*]?(@.fullName STARTS WITH 'M').fullName"))
[
  "Minnesota North Stars",
  "Montreal Wanderers",
  "Montreal Maroons",
  "Montréal Canadiens",
  "Minnesota Wild"
]
Discussion (14)3
Log in or sign up to continue

I don't know if this works on Windows systems but for macOS and Linux (I use Ubuntu), I defined a file in my home folder named .iris_init with these contents:

:alias pp do ##class(%ASQ.SetUtils).pp($*)
:alias parse try { do ##class(%ASQ.SetUtils).pp(##class(%ASQ.Parser).parse("$*").toDao()) } catch e { w !,e.AsSQLMessage() }
:alias asq try { do ##class(%ASQ.SetUtils).pp($1.apply("$2")) } catch e { w !,e.AsSQLMessage() }
:alias find do find^%z($1,$2,$3,$4)

This file is automatically loaded each time I establish an IRIS session.

In fact I'm having a weird issue in docker: it looks like terminal doesn't read the last symbol from alias string. e.g. if I have the following alias (one liner to turn on IRISBI in a current namespace):

:alias enablebi do EnableDeepSee^%SYS.cspServer("/csp/"_$$$LOWER($namespace))

It results with following:

IRISAPP>:enablebi
do EnableDeepSee^%SYS.cspServer("/csp/"_$$$LOWER($namespace)

DO EnableDeepSee^%SYS.cspServer("/csp/"_$$$LOWER($namespace)
^
<SYNTAX>
IRISAPP>

Any thoughts @Robert Cemper @Dan Pasco ?

I replaced $$$Lower() but still getting <SYNTAX>

Here is the alias in the file:

:alias enablebi do EnableDeepSee^%SYS.cspServer("/csp/"_$zcvt($namespace,"L"))

And here is SYNTAX i'm getting:

IRISAPP>:enablebi 
do EnableDeepSee^%SYS.cspServer("/csp/"_$zcvt($namespace,"L")

DO EnableDeepSee^%SYS.cspServer("/csp/"_$ZCVT($NAMESPACE,"L")
^
<SYNTAX>

Here is what I have in the alias:

IRISAPP>:enablebi
do EnableDeepSee^%SYS.cspServer("/csp/"_$zcvt($namespace,"L")

DO EnableDeepSee^%SYS.cspServer("/csp/"_$ZCVT($NAMESPACE,"L")
^
<SYNTAX>
IRISAPP>:alias
  enablebi              do EnableDeepSee^%SYS.cspServer("/csp/"_$zcvt($namespace,"L")

IRISAPP>

Interesting, if I set the alias in terminal as @Robert Cemper did it works.

Perhaps the issue is in line endings? Any ideas?