Working with Global Arrays
The .NET Native SDK provides mechanisms for working with global arrays from your instance of InterSystems IRIS. This chapter covers the following topics:
-
Introduction to Global Arrays — introduces global array concepts and provides a simple demonstration of how the .NET Native SDK is used.
-
Creating, Accessing, and Deleting Nodes — demonstrates how to create, change, or delete nodes in a global array, and how to retrieve node values.
-
Finding Nodes in a Global Array — describes the iteration methods that allow rapid access to the nodes of a global array.
-
Class IRIS Supported Datatypes — provides details on how to retrieve node values as specific datatypes.
The examples in this chapter assume that an IRIS object named iris already exists and is connected to the server. The following code establishes a standard .NET connection and creates an instance of IRIS:
//Open a connection to the server and create an IRIS object
IRISConnection conn = new IRISConnection();
conn.ConnectionString = "Server = localhost; "
+ "Port = 1972; " + "Namespace = USER; "
+ "Password = SYS; " + "User ID = _SYSTEM;";
conn.Open();
IRIS iris = IRIS.CreateIRIS(conn);
For more information on how to create an instance of IRIS, see the Quick Reference entry for CreateIRIS(). For general information on creating .NET connections, see Establishing .NET Connections in Using .NET with InterSystems Software. .
Introduction to Global Arrays
A global array, like all sparse arrays, is a tree structure rather than a sequential 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 six 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:
irisObject.Set("A", "root", "foo", "SubFoo");
irisObject.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, Accessing, and Deleting Nodes” later in this chapter.
The Native SDK code to create this array is demonstrated in the following example. An IRISConnection object establishes a connection to the server. The connection will be used by an instance of class IRIS named iris. Native SDK methods are used to create a global array, read the resulting persistent values from the database, and then delete the global array.
The Native SDK for .NET is part of the InterSystems.Data.IrisClient.dll library. For detailed information, see the Introduction to Using .NET with InterSystems Software.
using System;
using InterSystems.Data.IRISClient;
using InterSystems.Data.IRISClient.ADO;
namespace NativeSpace {
class NativeDemo {
static void Main(string[] args) {
try {
//Open a connection to the server and create an IRIS object
IRISConnection conn = new IRISConnection();
conn.ConnectionString = "Server = localhost; "
+ "Port = 1972; " + "Namespace = USER; "
+ "Password = SYS; " + "User ID = _SYSTEM;";
conn.Open();
IRIS iris = IRIS.CreateIRIS(conn);
//Create a global array in the USER namespace on the server
iris.Set("A", "root", "foo", "SubFoo");
iris.Set(123, "root", "bar", "lowbar", "UnderBar");
// Read the values from the database and print them
string subfoo = iris.GetString("root", "foo", "SubFoo");
string underbar = iris.GetString("root", "bar", "lowbar", "UnderBar");
Console.WriteLine("Created two values: \n"
+ " root(\"foo\",\"SubFoo\")=" + subfoo + "\n"
+ " root(\"bar\",\"lowbar\",\"UnderBar\")=" + underbar);
//Delete the global array and terminate
iris.Kill("root"); // delete global array root
iris.Close();
conn.Close();
}
catch (Exception e) {
Console.WriteLine(e.Message);
}
}// end Main()
} // end class NativeDemo
}
NativeDemo prints the following lines:
Created two values:
root("foo","SubFoo")=A
root("bar","lowbar","UnderBar")=123
In this example, an IRISConnection object named conn provides a connection to the database associated with the USER namespace. Native SDK methods perform the following actions:
-
IRIS.CreateIRIS() creates a new instance of IRIS named iris, which will access the database through conn.
-
IRIS.Set() creates new persistent nodes in the database.
-
IRIS.GetString() queries the database and returns the values of the specified nodes.
-
IRIS.Kill() deletes the specified 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
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").
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.
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.
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").
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.
The unsubscripted node at the base of the global array tree. The identifier for a root node is its global name with no subscripts.
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.
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).
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.
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.
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 only exist as 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 or a number. String subscripts are case-sensitive and can use all characters (including control and non-printing characters). Length is limited only by the 511 character maximum for the total node address.
Creating, Accessing, and Deleting Nodes
The Native SDK provides three methods that can make changes in the database: Set() and Increment() can create nodes or change node values, and Kill() can delete a node or set of nodes. Node values are retrieved by type-specific getter methods such as GetInteger() and GetString().
-
Creating Nodes and Setting Node Values — describes how to use Set() and Increment().
-
Getting Node Values — lists getter methods for each supported datatype.
-
Deleting Nodes — describes how to use Kill().
Creating Nodes and Setting Node Values
The Set() and Increment() methods can be used to create a persistent node with a specified value, or to change the value of an existing node.
IRIS.Set() takes a value argument of any supported datatype and stores the value at the specified address. If no node exists at the target address, a new one is created.
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.
iris.Set("first", "myGlobal", "A"); // create node myGlobal("A") = "first"
iris.Set(1, "myGlobal", "A"); // change value of myGlobal("A") to 1.
Set() can create and change values of any supported datatype. To read an existing value, you must use a different getter method for each datatype, as described in the next section.
IRIS.Increment() takes a number argument, increments the node value by that amount, and returns the incremented value.
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. The next two calls each increment by -2, resulting in a final value of -6:
for (int loop = 0; loop < 3; loop++) {
iris.Increment(-2,"myGlobal", "B");
}
The second argument for either Set() or Increment() is a global array name. The name can include letters, numbers, and periods. It must begin with a character, and may not end with a period. The arguments after the global name are subscripts, which can be either numbers or strings (case-sensitive, not restricted to alphanumeric characters). See “Global Naming Rules” for more information.
Getting Node Values
The Set() method can be used with all supported datatypes, but each datatype requires a separate getter. Node values can be any of the following datatypes: int?, short?, string, long?, double?, float?, byte[], bool?, DateTime?, IRISList?, and instances of objects that implement System.IO.MemoryStream (stored and retrieved as byte[]). A null value translates to ""
The following methods are used to retrieve node values of these datatypes:
-
Numeric datatypes: GetBool(), GetInt16(), GetInt32(), GetInt64(), GetSingle(), GetDouble()
-
String datatypes: GetString(), GetBytes(), GetIRISList()
-
Other datatypes: GetDateTime(), GetObject()
For more information on datatypes, see “Class IRIS Supported Datatypes” later in this chapter.
Deleting 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.
In the following example, 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
The example will delete the entire global array by calling Kill() on two of its subnodes, myGlobal("A") and myGlobal("B",1).
The first call will delete node myGlobal("A") and both of its subnodes:
iris.Kill("myGlobal", "A");
// also kills child nodes myGlobal("A",1) and myGlobal("A",2)
The second call deletes myGlobal("B",1), the last remaining subnode with a value:
iris.Kill("myGlobal","B",1);
Since neither of the remaining nodes has a value, the entire global array is deleted:
-
The parent node, myGlobal("B"), is deleted because it is valueless and now has no subnodes.
-
Now root node myGlobal is valueless and has no subnodes, so the entire global array is deleted from the database.
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 — describes how to iterate over all child nodes under a given parent node.
-
Iteration in Conditional Loops — describes methods and properties that provide more control over iteration.
-
Testing for Child Nodes and Node Values — describes how to find all subnodes regardless of node level, and identify which nodes have values.
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"):
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.GetIRISIterator() returns an instance of IRISIterator for the child nodes of the specified target node.
-
IRIS.GetIRISReverseIterator() returns an instance of IRISIterator set to backward iteration for the child nodes of the specified target node.
-
IRISIterator.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.
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
IRISIterator iterDogs = iris.GetIRISReverseIterator("heroes","dogs");
iterDogs.StartFrom("V");
String output = "\nDog birth years: ";
foreach (int BirthYear in iterDogs) {
output += BirthYear + " ";
};
Console.WriteLine(output);
This code prints the following output:
Dog birth years: 1940 1923 1919
The example does the following:
-
GetIRISReverseIterator() returns iterator iterDogs, which will find child nodes of heroes("dogs") in reverse collation order.
-
StartFrom() specifies subscript V, meaning that the search range will include all child nodes of heroes("dogs") with subscripts lower than V in collation order. The iterator will first find subscript Lassie, followed by Hachiko and Balto.
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.
The order in which nodes are retrieved depends on the collation order of the subscripts. This is not a function of the iterator. 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 in Conditional Loops
The previous section demonstrated an easy way to make a single pass over a set of child nodes, but in some cases you may want more control than a simple foreach loop can provide. This section demonstrates some methods and properties that allow more control over the iterator and provide easier access to data:
-
IRISIterator.MoveNext() implements System.Collections.IEnumerator, allowing you to control exactly when the iterator will move to the next node. It returns true if the next node has been found, or false if there are no more nodes in the current iteration.
-
IRISIterator.Reset() can be called after exiting a loop to reset the iterator to its starting position, allowing it to be used again.
-
IRISIterator.Current gets an object containing the value of the node at the current iterator position. This is the same value as the one assigned to the current loop variable in a foreach loop.
-
IRISIterator.CurrentSubscript gets an object containing the lowest level subscript for the node at the current iterator position. For example, if the iterator points to node myGlobal(23,"somenode"), the returned object will contain value "somenode".
Like the previous example, this one uses the heroes global array and iterates over the child nodes under heroes("dogs"). However, this example uses the same iterator to make several passes over the child nodes, and exits a loop as soon as certain conditions are met.
This example scans the child nodes under heroes("dogs") until it finds a specific node value or runs out of nodes. Array targetDates specifies the list of targetYear values to be used in the main foreach loop. Within the main loop, the do while loop finds each child node and compares its value to the current targetYear.
IRISIterator iterDogs = iris.GetIRISIterator("heroes","dogs");
bool seek;
int[] targetDates = {1906, 1940, 2001};
foreach (int targetYear in targetDates) {
do {
seek = iterDogs.MoveNext();
if (!seek) {
Console.WriteLine("Could not find a dog born in " + targetYear);
}
else if ((int)iterDogs.Current == targetYear) {
Console.WriteLine(iterDogs.CurrentSubscript + " was born in " + iterDogs.Current);
seek = false;
}
} while (seek);
iterDogs.Reset();
} // end foreach
This code prints the following output:
Whitefang was born in 1906
Lassie was born in 1940
Could not find a dog born in 2001
The example does the following:
-
GetIRISIterator() returns iterator iterDogs, which will find child nodes of heroes("dogs") in collation order (as demonstrated in the previous section, “Iterating Over a Set of Child Nodes”). iterDogs will be reset and used again in each pass of the foreach loop.
-
MoveNext() is called in each pass of the do while loop to find the next child node. It sets seek to true if a node is found, or false if there are no more child nodes. If seek is false, the do while loop exits after printing a message indicating that the current targetYear value was not found.
-
The Current and CurrentSubscript properties of iterDogs are set each time a child node is found. Current contains the current node value, and CurrentSubscript contains the current subscript.
-
Current is compared to targetYear. If there is a match, a message displays both the subscript and the node value, and the do while loop is terminated by setting seek to false.
-
Reset() is called at the end of each do while pass. This returns iterator iterDogs to its original starting condition so it can be used again in the next pass.
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 one. 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:
bool exists = (iris.IsDefined(root,subscripts) > 0); // value is 1, 10, or 11 bool hasValue = (iris.IsDefined(root,subscripts)%10 > 0); // value is 1 or 11 bool hasChild = (iris.IsDefined(root,subscripts) > 9); // value is 10 or 11
-
The following example consists of two methods:
-
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 checks to see if the current subscript is Timmy or Dolly, and prints a message if so.
-
FindAllHeroes() uses the return value of TestNode() to navigate the entire global array. It starts by iterating over the child nodes of root node heroes. Whenever TestNode() indicates that the current node has child nodes, FindAllHeroes() creates a new iterator to test the lower level child nodes.
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.
public void FindAllHeroes() {
string root = "heroes";
// Iterate over child nodes of root node heroes
IRISIterator iterRoot = iris.GetIRISIterator(root);
foreach (object node in iterRoot) {
object sub1 = iterRoot.CurrentSubscript;
bool hasChild1 = TestNode(iterRoot,sub1);
// Process current child of heroes(sub1)
if (hasChild1) {
IRISIterator iterOne = iris.GetIRISIterator(root,sub1);
foreach (object node in iterOne) {
object sub2 = iterOne.CurrentSubscript;
bool hasChild2 = TestNode(iterOne,sub1,sub2);
// Process current child of heroes(sub1,sub2)
if (hasChild2) {
IRISIterator iterTwo = iris.GetIRISIterator(root,sub1,sub2);
foreach (object node in iterTwo) {
object sub3 = iterTwo.CurrentSubscript;
TestNode(iterTwo,sub1,sub2,sub3); //no child nodes below level 3
}
} //end hasChild2
}
} //end hasChild1
} // end main loop
} // end FindAllHeroes()
public bool TestNode(IRISIterator iter, string root, params object[] subscripts) {
// Test for values and child nodes
int state = iris.IsDefined(root,subscripts);
bool hasValue = (state%10 > 0); // has value if state is 1 or 11
bool hasChild = (state > 9); // has child if state is 10 or 11
// Look for lost heroes
// string[] lost = {"Timmy","Dolly"};
var lost = new List<string> {"Timmy","Dolly"};
if (hasValue) { // ignore valueless nodes
string name = (string)iter.CurrentSubscript;
int year = (int)iter.Current;
// foreach (string hero in lost) {
// if (hero == name) {
if (lost.Contains(name))
Console.WriteLine("Hey, we found " + name + " (born in " + year + ")!!!");
// }
// }
}
return hasChild;
}
Class IRIS Supported Datatypes
For simplicity, examples in previous sections of this chapter have always used Integer or String node values, but the IRIS class also provides datatype-specific methods for the following supported datatypes.
The following IRIS methods assume that the node value is numeric, and attempt to convert it to an appropriate .NET variable: GetBool(), GetSingle(), GetDouble(), GetInt16(), GetInt32(), or GetInt64(). Given an integer node value, all numeric methods return meaningful values. Integer getters cannot reliably retrieve Single or Double values, and may return an inaccurate or meaningless value.
In the InterSystems IRIS database, String, byte[], and IRISList objects are all stored as strings, and no information about the original datatype is preserved. The IRIS GetString(), GetBytes(), and GetIRISList() methods get string data and attempt to coerce it to the desired format.
The string getters assume that a node value is non-numeric, and attempt to convert it appropriately. They return null if the target node is valueless or does not exist. These methods do not perform any type checking, and will not usually throw an exception if the node value is of the wrong datatype.
The IRIS class also supports getters for some .NET classes:
-
GetDateTime() — gets instances of System.DateTime.
-
GetObject() — returns the value of the target node as an object.
-
GetBytes() — can be used to get objects that implement System.IO.MemoryStream. An instance of MemoryStream will be stored as byte[] when set as a node value. Use GetBytes() to retrieve the value, then initialize a MemoryStream with the returned byte [] value.
These methods are optimized for speed, and never perform type checking. Your application should never depend on an exception being thrown if one of these methods attempts to fetch a value of the wrong datatype. Although an exception may be thrown, it is more likely that the method will fail silently, returning an inaccurate or meaningless value.