Defining Application Types With Typedef

Introduction

Application types are defined using the typedef construct. Inq is not an object oriented language and in its simplest sense a typedef can be thought of as similar to a struct in the C programming language. However, it is much more than just a collection of attributes. As well as data fields Inq maintains in a typedef the following information

  • A default value for each data field.
  • A width hint, a formatting pattern and a label string for each data field.
  • Specific code blocks executed when instances are created, mutated and destroyed by the Inq transaction model.
  • A primary key, to define an instance's identity and enforce uniqueness.
  • Any number of additional keys, unique or otherwise, that specify ways to request an instance or set of instances from the Inq i/o system. All keys can have additional information that is used by the i/o system and is specific to the physical storage medium the typedef is bound to.
  • An optional binding to a physical storage medium, such as a relational database. If a typedef is not bound to persistent storage then instances are in-memory only.

As well as entities, typedef can be used to provide an alias name, width hint, formatting pattern, label string and default value to the value types. Entity typedef fields can be declared by reference to these or other typedef fields, making their definition well insulated from the actual data types used.

Lastly, a entity field or typedef alias can define a set of values that instances are intended to take on. This is similar to an enum although the values are of the data type the field (or typedef alias) is.

typedef Aliases

In Inq, its a better than usual idea to alias fundamental types according to application needs. This is because the typedef carries more than just the data type. Here is an example:

typedef decimal:15 FXRate = 0
        width = 12
        format="#,##0.00####"
        label={i18n}.general.FX_RATE;

and here is what is happening:

  • The symbol FXRate is defined as a decimal to a precision of 15 decimal places.
  • The default value for instances of FXRate is zero. If no initialiser is specified then new instances would hold the value null.
  • The width hint is used to dimension GUI components and table columns. It is interpreted as a character width and takes into account the font used when rendered.
  • The formatting pattern of "#,##0.00####" means that when instances of FXRate are rendered to GUI components they will be displayed to minimum of two and a maximum of six decimal places. Formatting patterns are as described in the JavaTM documentation for numeric and date values.
  • A label string is specified as an expression. This is evaluated at parse time and the result can be applied when laying out GUI components. In this example an indirection through the expression {i18n} is being used. If this evaluated to path($catalog.en) then $catalog.en.general.FX_RATE might be "FX Rate".
Fixme (Tom)
These are all defined in the server, so it doesn't make sense to evaluate an internationalised label string until the typedefs are downloaded to the client.

Once a typedef alias has been created it can be referenced when defining fields in typedef entity definitions.

Defining Enumerated Values

Here is another example, this time including a set of preferred values.

  typedef string AccountType = "M"
          width=6
          label={i18n}.general.ACCOUNT_TYPE
  (
    // Interest bearing
    M  : "M" : "Margin";

    // No interest accrued.
    P  : "P" : "Pending";
  );

  

Each value has three parts to it:

  1. A symbol by which the value can be referred to in scripts
  2. The actual value that variables hold for the associated symbol
  3. The external representation for this value

Having defined AccountType the following code is valid:

  // Create a new variable of type AccountType. Its really a string
  any accType1 = new(AccountType);

  // Override default value
  any accType2 = new(AccountType, enum(AccountType, P));

  // Print out nicely
  writeln($catalog.system.out, enumext(AccountType, .accType2));
  

This example is somewhat contrived to introduce the functions enum and enumext. When creating instances of AccountType we are doing nothing more than creating a string. Although this is not completely useless (we don't need to know its a string), there is no association between the variables accType1 and accType2 and the type information from which they were created. When applying enum and enumext we have to specify the type we want to use.

The enumext function can accept either a symbol or a node reference as its second argument, depending on whether static or runtime evaluation is required. For example, the following are equivalent:

enumext(AccountType, P);
enumext(AccountType, .accType2);

Note, however, that to distinguish between a symbol and a stack-assumed variable the latter must be preceeded with a period "."

Inq does not mandate that any variable can only take on one of the preferred values. Scripts must ensure this with consistent use of enum.

typedef Entities

Typedef entities are used to define high-level application types. Instead of simple variables, an entity is used to create what Inq terms managed instances. They are called managed because the Inq server defines their life-cycle, supports specific code blocks that are executed at various life-cycle stages and controls how references to them are made available.

An instance enters the managed state when it is either read from persistent storage or created for the first time. In-memory types can only become managed instances by creation. When an instance is in the managed state, subsequent mutations or a deletion of that instance take place under the control of a transaction. Every Inq server process has its own transaction environment that implicitly commits managed instance creation, mutation and deletion, or aborts these operations should an exception occur. We will return to these subjects in later sections.

Typedef Body

In the following sections we discuss the elements that make up the body of a typedef by way of an example that defines a Currency type. The definition relies on the following aliases having been defined:

typedef int    NumDays  = 0  label="No. of Days" width=3;
typedef string LongName = "" width=25;
typedef string Description = "" width=50;

typedef string Active = "Y" label = "Active";
(
  Y  : "Y" : "Active";
  N  : "N" : "Inactive";
);

typedef date LastUpdated width=20 format="dd MMM yyyy HH:mm:ss";

// Part of a typedef for User
typedef User
{
  fields
  (
    string        User width=10;
    Description   Name;
    Active;
  )
    .
    .

Unless otherwise stated, a typedef's body elements must appear in the order they are documented here.

The typedef Name

The following syntax defines the typedef name and opens the body

typedef Currency
{

It is the convention that the typedef name starts with a capital letter. Assuming the body parses correctly, the typedef called Currency will reside in the current package and will be distinct from any typedef called Currency in any other package.

Fields Definition

A typedef can contain any number of data fields. A field definition can specify an explicit type or refer to either another typedef field or alias. Here are the field definitions for Currency:

  fields
  (
    string        Currency       label={i18n}.ccy.CURRENCY width=3;
    LongName      CcyName;
    NumDays       SettlementDays label={i18n}.ccy.SETTLEMENT_DAYS;
    NumDays       AccrualDays    label={i18n}.ccy.ACCRUAL_DAYS;
    NumDays       IntFixDays;
    Active;
    LastUpdated;
    User;
  )

Again, it is convention to name fields starting with a capital letter. Considering each field in turn:

Currency
As we discuss further below, this field is chosen as the primary key. If there is a single or principal field that comprises the primary key then a further convention is that this field has the same name as the typedef.
CcyName
This field refers to the LongName type alias from which it takes a width hint of 25 and a default value of "".
SettlementDays, AccrualDays and IntFixDays
These fields all refer to the NumDays alias however SettlementDays and AccrualDays override the label with their own.
Active and LastUpdated
When refering to an alias it is not necessary to provide a field name, in which case the field takes the name of the alias itself.
User
It is also possible to refer to fields defined in other entity typedefs. In this case we are implicitly referring to User.User and defining the local field User.

If no default value is specified directly or derived from a referral then a field will take the value null.

Overriding the Instance Name

A typedef is always referenced symbolically by its name and the same name is used by Inq when placing instances into node structures, discussed further in the section Building Node Structures. In our example, this would give rise to paths like Currency.AccrualDays.

Inq allows the name used when building structures to be specified separately when the typedef is defined. Consider the following typedef fragments. Firstly a typedef called Trade:

package foo;

typedef Trade
{
  fields
  (
    int  Trade;
    Price TradePrice;
    Price MarketPrice;
      .
      .
  )
}

Then suppose our application maintains previous versions of Trade and we define another typedef accordingly:

package foo;

typedef TradeArchive
{
  alias Trade;
  fields
  (
    int  Trade;
    Price TradePrice;
    Price MarketPrice;
      .
      .
    int   VersionNumber;
    date  VersionDate;
  )
}

Script that refers to the TradeArchive type will always use its symbolic name, for example when creating a new TradeArchive instance with the new function, the type is specified thus:

new(foo:TradeArchive);

However when Inq builds a node structure it will do so using the string Trade.

The Constructor

A new instance of a typedef is created with the new() function:

any newCcy = new(Currency[, initialValue]);

This statement creates a new map containing the fields of the Currency typedef. Any default values will be assigned, however at this stage the instance is not under transaction control because it is not in the managed state. The new function takes an optional second argument used to initialise the newly created instance. For entity typedefs, this must be a map and any matching field names will be copied to the new instance. Any other contents will be ignored.

When the create statement is executed the typedef's constructor is run and the new instance placed into the transaction. If the transaction commits successfully the instance will become a managed instance. The constructor is a statement (or code block) where steps specific to the initialisation of candidate managed instances can be carried out. This might include generation of a unique key or creation of a mandatory dependent instance. The create statement is:

create(newCcy [, eventData [, CREATE_ERROR | CREATE_REPLACE | CREATE_LEAVE ] ]);
Note
When a transaction containing instance creations commits an event is raised for each created instance. If eventData is supplied then this data will be carried in the event and may be used by listeners to discriminate interest. This topic is covered in the section on Events.

The use of CREATE_ERROR, CREATE_REPLACE or CREATE_LEAVE tell Inq what action to take if the primary key would be violated by the creation of this instance. This is covered in Transactions.

The constructor statement is optional and may be specified like this:

construct
({
  // Force the Currency field to be upper-case
  $this.Currency = toupper($this.Currency);

  $this.LastUpdated = getdate();
  $this.User        = $process.loginName;
})

When the constructor statement is executed the following environment prevails:

  • $this refers to the instance being created;
  • The current stack frame is unchanged and the constructor has access to anything currently on the stack.

The constructor is simply a statement (a block statement in our example). Any statements can appear within it, including calls to functions.

When the constructor has completed, any further changes to the created instance (newCcy in our example) will be ignored. In fact, this instance can be re-used in further calls to create, since Inq has taken a copy of the original in order to safeguard the state of the instance in the transaction.

The Join and Mutator Statements

When a typedef instance is in the managed state, changes to its field values take place under transaction control. If a managed instance is not currently within any process's transaction then the first process that attempts to assign a field value will successfully acquire a lock on that instance. Subsequent mutation attempts by other processes will cause those processes to go into a wait condition until either the holding process's transaction commits or aborts. The Inq run-time performs these steps automatically - no source code is required to effect them.

While a process holds a write lock on an instance, other processes can still read its fields. They do not see any of the modified values while the locking process's transaction is still in progress.

When a process acquires the instance write lock the optional join statement is executed. Because it is executed early in the transaction sequence, the join statement can be used to check any preconditions that must be in place. Here is an example:

join
({
  // Jobs can be modified by the jobdispatcher or
  // a user process provided it has set the tree
  // state (to negotiate with the jobdispatcher).
  // Using the join block catches this early (before
  // the instance is first mutated) rather
  // than late (in mutate, as the transaction is
  // committing).
   
  if ($process.loginName != "jobdispatcher")
    call isTreeState(requiredState = enum(JobTreeState, USER_START));
})

When the join statement is executed the following environment prevails:

  • $this refers to the instance, as yet unmodified, being joined into the transaction.
  • A stack frame is available for general use.

As a process's transaction commits, the optional mutate statement is executed for each instance the process has modified. The mutator can be used to verify that new field values are acceptable, perform any further updates that are always required, for example modifying a LastUpdated field or dependent instances. It is also possible to veto changes to the instance. Here is an example mutator:

mutate
({
  $this.new.LastUpdated = getdate();
  $this.new.User = $process.loginName;
})

When the mutator statement is executed the following environment prevails:

  • $this refers to a node in which the new, that is modified instance and original, unmodified instance are available as $this.new and $this.old.
  • A stack frame is available for general use.

If the mutator executes the statement

$this.new = $this.old;

then all modifications to the instance are discarded. Of course, individual fields can be reset in this way also. If the mutator modifies another instance not yet contained within the transaction then this instance will be locked, joined into the transaction and have any mutator defined for it executed at a later time.

Fixme (Tom)
Not currently handled to modify an instance for which the mutator has already been run.

The Destroy Statement

The delete function is used to delete instances in the managed state. When called, the instance is entered into the transaction for deletion and any destroy statement is run.

On successful commit, all the delete-marked instances within the process's transaction become unmanaged and a deletion event is raised on each. Any structures in which the instance is contained may also be manipulated to reflect the deletion and this results in, for example, client table views showing a row deletion. This subject is covered in greater detail in the section on building working data structures.

The destroy statement can be used to delete dependent instances or release related resources such as open streams. Its syntax is

destroy(<statement>)

The environment during execution of the destroy statement is as follows:

  • $this refers to the instance being deleted;
  • A stack frame is available for general use.

When a typedef instance has been successfully deleted, any process may continue to hold a reference it obtained prior to deletion. Any mutation of such an instance will not result in transaction handling or mutator execution and it is an error to delete an instance twice.

The Expire Statement

The Inq run-time allows entities to be reparsed and replaced in the system while it is running and this applies to typedefs just as it does to functions and services. When a typedef replaces one of the same name in the same package any existing references to instances of that typedef become benign, that is they do not take part in transactions any more.

The Inq run-time places any instances of the replaced typedef into the unmanaged state, executes any expire statement for each instance and raises a single event to notify the expiry:

expire(<statement>)

The environment during execution of the expire statement is as follows:

  • $this refers to the instance being expired. Parsing a typedef occurs when loading server-side scripts;
  • A stack frame is available for general use.
Fixme (tom)
Further coverage of expiry to be written

Defining Keys

The process of defining keys establishes two facets of a typedef:

  1. A mandatory primary key. This key allows the Inq run-time to establish identity and enforce uniqueness of instances in (or entering) the managed state. Inq ensures that only a single reference to a given instance exists within the server environment.
  2. Any number of unique or non-unique keys. These keys are defined to suit the way that the application wishes to retrieve instances and build working data structures.

A key comprises a collection of fields and any information that relates to the chosen i/o binding for the typedef. In addition, Inq may cache key values that have been applied to the i/o binding against the instances returned to allow a server to make more efficient use of i/o resources. There are various options for configuring how a particular key manages its cache or whether caching is turned off completely. This is subject is explained in more detail in the section discussing I/O Keys.

Apart from the primary, which can only be composed from fields of the typedef, keys can comprise additional elements exclusive to them by referencing fields of other entity typedefs or aliases. More complex definitions are described in the I/O Keys section. At this stage we cover the simpler cases only.

Continuing our example, the Currency typedef has the following primary key:

pkey
(
  fields (Currency)

  #include <{db}/xyCurrency.pkey.sql>
)

The fields section is a comma-separated list from the fields that make up the typedef itself. In this case we specify only the Currency field and in so doing we are saying that each instance of the Currency typedef must have a unique value for this field.

The #include<...> directive instructs the parser to process input from the specified file. As we see below, the Currency typedef is bound to a relational database and the auxilliary information required by its key definitions is SQL syntax. The primary key requires SQL to retrieve, update, create and delete instances of Currency.

The content of the angle brackets is a URL, which if relative, is resolved with respect to the current parse module. Arbitrary command line arguments from the Inq environment can be substituted using the {} construct. In this example, the command line must include the -db flag. If this was passed as -db mysql the result would be the relative URL of mysql/xyCurrency.pkey.sql. Here's what the MySql version looks like. Using this approach it is straightforward to support multiple database vendors easily. Here, for example, is an Oracle version.

You may notice that these two examples use different approaches to formatting and parameterising the SQL statements. There are trade-offs between the various features available and these are discussed in the I/O Keys section. Whichever approach is taken, some of what appears in skeletal SQL files is messy, however once its done it can generally be forgotten about and database independence is yours.

Further keys can be defined as follows

key ByActive
(
  fields(Active)
  auxcfg( map(
  "read-sql",
    "
      {select-stmt}
      where Active  = {Active}
    "
  ))
)

From the primary key's definition of it, we can refer to the select statement symbolically so we only have to supply the where clause. This key is used to retrieve all instances of Currency with a given value of the Active field.

Any number of keys can be defined with a key() clause. By default, a key is non-unique unless specified otherwise. There are many ways to set up keys, for example they can be used to support complex filtering, specify inequalities and can be cached or not where appropriate. These features are dealt with in the I/O Keys section.

I/O Binding

The last component of a typdef body is any i/o binding it may have. In the definition of the keys, above, we specified various data in the auxcfg clause. This data is used by (and so related to) the particular i/o mechanism the typedef is bound to.

Our example has used SQL statements and so must be bound to an SQL server. A clause like this accomplishes that task:

iobind (SimpleSqlIO, xy)

The token SimpleSqlIO is a reserved word that tells the Inq server that the i/o mechanism will be SQL. The second token, in this case xy is an identifier of a resource that must have already been defined in the Inq server.

Note
Its not necessary for a typedef to have an i/o binding. If it does not then instances are in-memory only.

An entire application will comprise many modules that will be parsed into the server-side environment. These will typically be #included from a top-level boot file. Somewhere early on in the list will be a resource definition of the form:

resource xy ( sqlserver, 50,
              map(  "user", "xy1",
                    "password", "xy1",
                    "url", "jdbc:mysql://localhost/xydev",
                    "null", map(date.class, "NULL",
                                string.class, "NULL",
                                decimal.class, "NULL",
                                double.class, "NULL",
                                float.class, "NULL",
                                int.class, "NULL"
                                ),
                    "delim", map(date.class, "'",
                                 string.class, "'"),
                    "cardinality", false
              )
            );

Again, we'll cover the details elsewhere but the salient points here are that we define an sqlserver called xy to which Inq will hold at most 50 simultaneous open connections using the specified user and password and connecting to the given JDBC URL.

Close Your Typedef

Don't forget to close your typedef body:

}

Resolving Field References

When defining typedef fields in entity typedefs using references the following examples illustrate the alternatives:

User
Defines a field called User by reference to the field User.User if User is an entity typedef or to the typedef alias User in that case.
User Description
As above but defines a field called Description.
User.LongName
Defines a field called LongName by reference to the field LongName in the typedef User.
User.LongName Description
Defines a field called Description by reference to the field LongName in the typedef User.
refdata:User.LongName Description
Defines a field called Description by reference to the field LongName in the typedef User in the package or import symbol refdata. Package qualification is applicable in all of the above examples.

Clearly, using references creates dependencies between typedef declarations. There are no restrictions on the order in which dependent typedefs are parsed and cyclic references, though questionable from a design perspective, are allowed.

As a typedef is parsed, Inq attempts to resolve, in both directions, references between it and those already loaded. This may result in one or more typedefs having some or all of their remaining references resolved. Note that this includes any references arising from the definition of key fields, described in the section on I/O Keys.

If new is called on a typedef that is not fully resolved then an exception is thrown. When a typedef becomes fully resolved a cataloged event is raised, discussed further in the Events section.

The Typedef Instance Life-Cycle

Earlier we mentioned that typedef instances had a defined life-cycle and that specially declared statements are executed at certain life-cycle stages. The diagram below summarises the Inq statements and transaction outcomes that move a typedef instance between the various states:

Life Cycle

Notice that references to managed instances can only be obtained by performing a read() statement. This statement fetches instances from the memory cache or, if the typedef is persistent, the i/o mechanism if the key is not cached, never been used or flushed from the cache. We return to this subject in the section Building Node Structures.