Skip to main content

Working with Global Arrays

The Node.js Native SDK provides mechanisms for working with global arrays from your instance of InterSystems IRIS. This chapter covers the following topics:

Note:
Creating a Database Connection in Node.js

The examples in this chapter assume that an Iris object named irisjs already exists and is connected to the server. The following code was used to create and connect it:

  const IRISNative = require('intersystems-iris-native')
  let connectionInfo = {host:'127.0.0.1', port:51773, ns:'USER', user:'_SYSTEM', pwd:'SYS'};
  const conn = IRISNative.createConnection(connectionInfo);
  const irisjs = conn.createIris();

For more information, see the Quick Reference entries for createConnection() and createIris().

Introduction to Global Arrays

A global array, like all sparse arrays, is a tree structure rather than a sequentially numbered list. The basic concept behind global arrays can be illustrated by analogy to a file structure. Each directory in the tree is uniquely identified by a path composed of a root directory identifier followed by a series of subdirectory identifiers, and any directory may or may not contain data.

Global arrays work the same way: each node in the tree is uniquely identified by a node address composed of a global name identifier and a series of subscript identifiers, and a node may or may not contain a value. For example, here is a global array consisting of five nodes, two of which contain values:

   root -->|--> foo --> SubFoo='A'
           |--> bar --> lowbar --> UnderBar=123

Values could be stored in the other possible node addresses (for example, root or root->bar), but no resources are wasted if those node addresses are valueless. In InterSystems ObjectScript globals notation, the two nodes with values would be:

   root('foo','SubFoo')
   root('bar','lowbar','UnderBar')

The global name (root) is followed by a comma-delimited subscript list in parentheses. Together, they specify the entire path to the node.

This global array could be created by two calls to the Native SDK set() method:

  irisjs.set('A', 'root', 'foo', 'SubFoo');
  irisjs.set(123, 'root', 'bar', 'lowbar', 'UnderBar');

Global array root is created when the first call assigns value 'A' to node root('foo','SubFoo'). Nodes can be created in any order, and with any set of subscripts. The same global array would be created if we reversed the order of these two calls. The valueless nodes are created automatically, and will be deleted automatically when no longer needed. For details, see “Creating, Updating, and Deleting Nodes” later in this document.

The Native SDK code to create this array is demonstrated in the following example. A Connection object connects to the server and an instance of class Iris is created. Iris methods are used to create a global array, read the resulting persistent values from the database, and then delete the global array.

The NativeDemo Program
// Create a reference to the Native SDK module
  const IRISNative = require('intersystems-iris-native')

// Open a connection to the server and create an Iris object
  let connectionInfo = {
    host: '127.0.0.1',
    port: 51773,
    ns: 'USER',
    user: '_SYSTEM',
    pwd: 'SYS'
  };
  const conn = IRISNative.createConnection(connectionInfo);
  const irisjs = conn.createIris();

// Create a global array in the USER namespace on the server
  irisjs.set('A', 'root', 'foo', 'SubFoo');
  irisjs.set(123, 'root', 'bar', 'lowbar', 'UnderBar');

// Read the values from the database and print them
  let subfoo = irisjs.get('root', 'foo', 'SubFoo')
  let underbar = irisjs.get('root', 'bar', 'lowbar', 'UnderBar')
  console.log('Created two values: ');
  console.log('   root("foo","SubFoo")=' + subfoo);
  console.log('   root("bar","lowbar","UnderBar")=' + underbar);

NativeDemo prints the following lines:

  Created two values:
     root("foo","SubFoo")=A
     root("bar","lowbar","UnderBar")=123

In this example, IRISNative is an instance of the Native SDK. Static method createConnection() defines a Connection object named conn that provides a connection to the database associated with the USER namespace. Native SDK methods perform the following actions:

  • Connection.createIRIS() creates a new instance of Iris named irisjs, which will access the database through server connection conn.

  • Iris.set() creates new persistent nodes in database namespace USER.

  • Iris.get() queries the database and returns the values of the specified nodes.

  • Iris.kill() deletes the specified root node and all of its subnodes from the database.

The next chapter provides detailed explanations and examples for all of these methods.

Glossary of Native SDK Terms

See the previous section for an overview of the concepts listed here. Examples in this glossary will refer to the global array structure listed below. The Legs global array has ten nodes and three node levels. Seven of the ten nodes contain values:

  Legs                       // root node, valueless, 3 child nodes
    fish = 0                 // level 1 node, value=0
    mammal                   // level 1 node, valueless
      human = 2              // level 2 node, value=2
      dog = 4                // level 2 node, value=4
    bug                      // level 1 node, valueless, 3 child nodes
      insect = 6             // level 2 node, value=6
      spider = 8             // level 2 node, value=8
      millipede = Diplopoda  // level 2 node, value='Diplopoda', 1 child node
        centipede = 100      // level 3 node, value=100

Child node

The nodes immediately under a given parent node. The address of a child node is specified by adding exactly one subscript to the end of the parent subscript list. For example, parent node Legs('mammal') has child nodes Legs('mammal','human') and Legs('mammal','dog').

Global name

The identifier for the root node is also the name of the entire global array. For example, root node identifier Legs is the global name of global array Legs.

Node

An element of a global array, uniquely identified by a namespace consisting of a global name and an arbitrary number of subscript identifiers. A node must either contain data, have child nodes, or both.

Node level

The number of subscripts in the node address. A ‘level 2 node’ is just another way of saying ‘a node with two subscripts’. For example, Legs('mammal','dog') is a level 2 node. It is two levels under root node Legs and one level under Legs('mammal').

Node address

The complete namespace of a node, including the global name and all subscripts. For example, node address Legs(fish) consists of root node identifier 'Legs' plus a list containing one subscript, 'fish'. Depending on context, Legs (with no subscript list) can refer to either the root node address or the entire global array.

Root node

The unsubscripted node at the base of the global array tree. The identifier for a root node is its global name with no subscripts.

Subnode

All descendants of a given node are referred to as subnodes of that node. For example, node Legs('bug') has four different subnodes on two levels. All nine subscripted nodes are subnodes of root node Legs.

Subscript / Subscript list

All nodes under the root node are addressed by specifying the global name and a list of one or more subscript identifiers. (The global name plus the subscript list is the node address).

Target address

Many Native SDK methods require you to specify a valid node address that does not necessarily point to an existing node. For example, the set() method takes a value argument and a target address, and stores the value at that address. If no node exists at the target address, a new node is created.

Value

A node can contain a value of any supported type. A node with no child nodes must contain a value; a node that has child nodes can be valueless.

Valueless node

A node must either contain data, have child nodes, or both. A node that has child nodes but does not contain data is called a valueless node. Valueless nodes are just pointers to lower level nodes.

Global Naming Rules

Global names and subscripts obey the following rules:

  • The length of a node address (totaling the length of the global name and all subscripts) can be up to 511 characters. (Some typed characters may count as more than one encoded character for this limit. For more information, see “Maximum Length of a Global Reference”).

  • A global name can include letters, numbers, and periods ('.'), and can have a length of up to 31 significant characters. It must begin with a letter, and must not end with a period.

  • A subscript can be a string, an integer, or a number. String subscripts are case-sensitive, and can include characters of all types. Length is limited only by the 511 character maximum for the total node address.

Creating, Updating, and Deleting Nodes

This section describes the Native SDK methods used to create, update, and delete nodes. set(), increment(), and kill() are the only methods that can create a global array or alter its contents. The following examples demonstrate how to use each of these methods.

Setting and changing node values

Iris.set() takes a value argument and stores the value at the specified address.

If no node exists at that address, a new one is created.

The set() method can assign values of any supported datatype. In the following example, the first call to set() creates a new node at subnode address myGlobal('A') and sets the value of the node to string 'first'. The second call changes the value of the subnode, replacing it with integer 1.

  irisjs.set('first', 'myGlobal', 'A');     // create node myGlobal('A') = 'first'
  irisjs.set(1, 'myGlobal', 'A');   // change value of myGlobal('A') to 1.

set() is a polymorphic accessor that can create and change values of any supported datatype, as demonstrated in this example.

Incrementing node values

Iris.increment() takes an integer number argument, increments the node value by that amount, and returns the incremented value. The initial target node value can be any supported numeric type, but the incremented value will be an integer. If there is no node at the target address, the method creates one and assigns the number argument as the value. This method uses a thread-safe atomic operation to change the value of the node, so the node is never locked.

In the following example, the first call to increment() creates new subnode myGlobal('B') with value -2, and assigns the returned value to total. The next two calls each increment by -2 and assign the new value to total, and the loop exits when the node value is -6.

  do {
    var total = irisjs.increment(-2,'myGlobal', 'B');
  } while (total > -6)
  console.log('total = ' + total + ' and myGlobal('B') = ' + irisjs.get('myGlobal', 'B'))

// Prints: total = -6 and myGlobal('B') = -6

Deleting a node or group of nodes

Iris.kill() — deletes the specified node and all of its subnodes. The entire global array will be deleted if the root node is deleted or if all nodes with values are deleted.

Global array myGlobal initially contains the following nodes:

   myGlobal = <valueless node>
     myGlobal('A') = 0
       myGlobal('A',1) = 0
       myGlobal('A',2) = 0
     myGlobal('B') = <valueless node>
       myGlobal('B',1) = 0

This example will delete the global array by calling kill() on two of its subnodes. The first call will delete node myGlobal('A') and both of its subnodes:

  irisjs.kill('myGlobal', 'A');
  // also kills child nodes myGlobal('A',1) and myGlobal('A',2)

The second call deletes the last remaining subnode with a value, killing the entire global array:

  irisjs.kill('myGlobal','B',1);
  • The parent node, myGlobal('B'), is deleted because it is valueless and now has no subnodes.

  • Root node myGlobal is valueless and now has no subnodes, so the entire global array is deleted from the database.

The entire global array could have been deleted immediately by calling kill() on the root node:

  irisjs.kill('myGlobal');

Finding Nodes in a Global Array

The Native SDK provides ways to iterate over part or all of a global array. The following topics describe the various iteration methods:

Iterating Over a Set of Child Nodes

Child nodes are sets of nodes immediately under the same parent node. Any child node address can be defined by appending one subscript to the subscript list of the parent. For example, the following global array has four child nodes under parent node heroes('dogs'):

The heroes global array

This global array uses the names of several heroic dogs (plus a reckless boy and a pioneering sheep) as subscripts. The values are birth years.

  heroes                                           // root node,    valueless, 2 child nodes
     heroes('dogs')                                // level 1 node, valueless, 4 child nodes
        heroes('dogs','Balto') = 1919              // level 2 node, value=1919
        heroes('dogs','Hachiko') = 1923            // level 2 node, value=1923
        heroes('dogs','Lassie') = 1940             // level 2 node, value=1940, 1 child node
           heroes('dogs','Lassie','Timmy') = 1954  // level 3 node, value=1954
        heroes('dogs','Whitefang') = 1906          // level 2 node, value=1906
     heroes('sheep')                               // level 2 node, valueless, 1 child node
        heroes('sheep','Dolly') = 1996             // level 2 node, value=1996

The following methods are used to create an iterator, define the direction of iteration, and set the starting point of the search:

  • Iris.iterator() returns an instance of Iterator for the child nodes of the specified target node.

  • Iterator.reversed() — toggles direction of iteration between forward and reverse collation order.

  • Iterator.startFrom() sets the iterator's starting position to the specified subscript. The subscript is an arbitrary starting point, and does not have to address an existing node.

Read child node values in reverse order

The following code iterates over child nodes of heroes('dogs') in reverse collation order, starting with subscript V:

// Iterate in reverse, seeking nodes lower than heroes('dogs','V') in collation order
  let iterDogs = irisjs.iterator('heroes','dogs').reversed().startFrom('V');

  let output = '\nDog birth years: ';
  for ([key,value] of iterDogs) {
    output += key + ':' + value + '  ';
  };
  console.log(output);

This code prints the following output:

  Dog birth years: Lassie:1940  Hachiko:1923  Balto:1919

In this example, two subnodes of heroes('dogs') are ignored:

  • Child node heroes('dogs','Whitefang') will not be found because it is outside of the search range (Whitefang is higher than V in collation order).

  • Level 3 node heroes('dogs','Lassie','Timmy') will not be found because it is a child of Lassie, not dogs.

See the last section in this chapter (“Testing for Child Nodes and Node Values”) for a discussion of how to iterate over multiple node levels.

Note:
Collation Order

The order in which nodes are retrieved depends on the collation order of the subscripts. When a node is created, it is automatically stored it in the collation order specified by the storage definition. In this example, the child nodes of heroes('dogs') would be stored in the order shown (Balto, Hachiko, Lassie, Whitefang) regardless of the order in which they were created. For more information, see “Collation of Global Nodes” in Using Globals.

Iteration with next()

The Native SDK also supports the standard next() and return type iterator methods:

  • Iterator.next() — positions the iterator at the next child node (if one exists) and returns an array containing properties done and value. The done property will be false if there are no more nodes. When an iterator is created, it defaults to the entries() return type.

  • Iterator.entries() — sets return type to an array containing both the key (the top level subscript of the child node) and the node value. For example, the returned value for node heroes(,'dogs','Balto') would be ['Balto',1919].

  • Iterator.keys() — sets return type to return only the key (the top level subscript).

  • Iterator.values() — sets return type to return only the node value.

In the following example, each call to the next() method sets variable iter to an array containing the current values for the iterator done and value properties. Since the keys() method was called when the iterator was created, the value property will contain only the key (top level subscript) for the current child node of heroes('dogs').

Use next() to list the subscripts under node heroes('dogs')
// Get a list of child subscripts under node heroes('dogs')
  let iterDogs = irisjs.iterator('heroes','dogs').keys();
  let output = "\nSubscripts under node heroes('dogs'): ";

  let iter = iterDogs.next();
  while (!iter.done) {
    output += iter.value + ' ';
    iter = iterDogs.next();
  }
  console.log(output);

This code prints the following output:

  Subscripts under node heroes('dogs'): Balto Hachiko Lassie Whitefang

Testing for Child Nodes and Node Values

In the previous examples, the scope of the search is restricted to child nodes of heroes('dogs'). The iterator fails to find two values in global array heroes because they are under different parents:

  • Level 3 node heroes('dogs','Lassie','Timmy') will not be found because it is a child of Lassie, not dogs.

  • Level 2 node heroes('sheep','Dolly') is not found because it is a child of sheep, not dogs.

To search the entire global array, we need to find all of the nodes that have child nodes, and create an iterator for each set of child nodes. The isDefined() method provides the necessary information:

  • Iris.isDefined() — can be used to determine if a node has a value, a subnode, or both. It returns one of the following values:

    • 0 — the specified node does not exist

    • 1 — the node exists and has a value

    • 10 — the node is valueless but has a child node

    • 11 — the node has both a value and a child node

    The returned value can be used to determine several useful boolean values:

       let exists = (irisjs.isDefined(root,subscripts) > 0);      // returns 1, 10, or 11
       let hasValue = (irisjs.isDefined(root,subscripts)%10 > 0); // returns 1 or 11
       let hasChild = (irisjs.isDefined(root,subscripts) > 9);    // returns 10 or 11
    
    

The following example consists of two methods:

  • findAllHeroes() iterates over child nodes of the current node, and calls testNode() for each node. Whenever testNode() indicates that the current node has child nodes, findAllHeroes() creates a new iterator for the next level of child nodes.

  • testNode() will be called for each node in the heroes global array. It calls isDefined() on the current node, and returns a boolean value indicating whether the node has child nodes. It also prints node information for each node.

Method findAllHeroes()

This example processes a known structure, and traverses the various levels with simple nested calls. In the less common case where a structure has an arbitrary number of levels, a recursive algorithm could be used.

  function findAllHeroes() {
    const root = 'heroes';
    console.log('List all subnodes of root node '+root+':\n'+root)
    let iterRoot = irisjs.iterator(root);
    let hasChild = false;

    // Iterate over children of root node heroes
    for ([sub1,value] of iterRoot) {
      hasChild = testNode(value,root,sub1);

      // Iterate over children of heroes(sub1)
      if (hasChild) {
        let iterOne = irisjs.iterator(root,sub1);
        for ([sub2,value] of iterOne) {
          hasChild = testNode(value,root,sub1,sub2);

          // Iterate over children of heroes(sub1,sub2)
          if (hasChild) {
            let iterTwo = irisjs.iterator(root,sub1,sub2);
            for ([sub3,value] of iterTwo) {
              testNode(value,root,sub1,sub2,sub3); //no child nodes below level 3
            }
          } //end level 2
        }
      } //end level 1
    } // end main loop
  } // end findAllHeroes()

Method testNode()
  function testNode(value, root, ...subs) {

  // Test for values and child nodes
    let state = irisjs.isDefined(root,...subs);
    let hasValue = (state%10 > 0); // has value if state is 1 or 11
    let hasChild = (state > 9);    // has child if state is 10 or 11

  // format the node address output string
    let subList = Array.from(subs);
    let level = subList.length-1;
    let indent = '  ' + String('      ').slice(0,(level*2));
    let address = indent + root+'(' + subList.join() + ')';

  // Add node value to string and note special cases
    if (hasValue) { // ignore valueless nodes
      address += ' = ' + value;
      for (name of ['Timmy','Dolly']) {
        if (name == subList[level]) {
          address += ' (not a dog!)'
        }
      }
    }
    console.log(address);
    return hasChild;
  }
}

This method will write the following lines:

List all subnodes of root node heroes:
heroes
  heroes(dogs)
    heroes(dogs,Balto) = 1919
    heroes(dogs,Hachiko) = 1923
    heroes(dogs,Lassie) = 1940
      heroes(dogs,Lassie,Timmy) = 1954  (not a dog!)
    heroes(dogs,Whitefang) = 1906
  heroes(sheep)
    heroes(sheep,Dolly) = 1996  (not a dog!)

The output of testNodes() includes some nodes that were not found in previous examples because they are not child nodes of heroes('dogs'):

  • heroes('dogs','Lassie','Timmy') is a child of Lassie, not dogs.

  • heroes('sheep','Dolly') is a child of sheep, not dogs.

FeedbackOpens in a new tab