company logo

OSI Script comments

The OSI script example creates test data for the sample database. This is, perhaps, not the most typical use case, but it also demonstrates the principle use of OSI scripts. For an OSI script, the database is the memory, i.e. no queries are necessary, since collections and instances are just there. On the other hand, OSI behaves like a normal programming language (similar to C# or Java). Thus, database access becomes very flexible and is rather familiar to programmers. More details, you will find in OSI - ODABA Script Interface in the ODABA Online Documentation.

Data source

Static or global OSI scripts require a data source, which defines the database the script is working on. In the example, the data source is defined in the script file by defining the path for DICTIONARY and DATABASE. One might also define the data source in an ini-file or data catalog and referring to the data source name by the DATASOURCE keyword.

DICTIONARY='Sample/Sample.dev';

DATABASE='Sample/Sample.dat';

Notes:

On Windows you have no common place for the Databases and Dictionaries. In contrast Linux provides a Directory in /var/lib/odaba. Therefore you have to change the provided examples or consequently ignore the presumption and write the ini-files for yourself.

Functions in an OSI script

OSI functions can be provided in the OSI script file, but also in the dictionary as class methods. Functions in the script file will replace similar OSI functions (having the same function name) in the dictionary.

When running an OSI script file, it must contain a function main(), which is the default entry point. Via an option, one may also define another entry point when calling OSI. The main() function is a global function which is not defined within the scope of any class.

In the example, the main() function calls four other functions defined in the script below. OSI does not require parenthesis when calling a function without parameters, but we tend to use them anyhow, since it makes reading script files easier.

OSI functions provide several sections for defining local variables (VARIABLES), processing code (PROCESS), error handling (ON_ERROR) and termination (FINAL). Functions that do not require definition of local variables do not need section headers like PROCESS, which is the default function section when no section has been defined.

Parameter defined in the function header might contain a default value (person_count = 1000), in which case the parameter becomes optional. Class member functions (Person::SetAttributes()) can be defined by using scoped names. Calling class member functions usually requires a calling object (persons.SetAttributes()).

bool main ( ) {

VARIABLES

    global int    comp_count   = 5;

PROCESS

    CreateCompanies();

    CreatePersons();

    InitCollections();

    AssignChildren();

}

void CreateCompanies ( ) {

VARIABLES

    ......

PROCESS

    ......

}

void CreatePersons ( int person_count = 1000 ) {

    ......

            persons.SetAttributes();

    ......

}

void Person::SetAttributes ( ) {

    ......

}

void InitCollections ( ) {

    ......

}    

bool AssignChildren ( ) {

    ......

            father.AssignChild(age_group);

}

bool Person::AssignChild ( int age_group ) {

    ......

}

Local variables

Local variables have to be defined in a VARIABLES section at the beginning of an OSI function. Usually, database variables need not to be defined, since those are defined in the database schema. There are, however, two reasons for assigning database variables to local variables.

Local variables might be of any complexity. Besides value variables, one may also define reference variables, which are typically used for referring to extents and results of path expressions.

Initializing complex variables is not yet supported. In ODABA version 11 you will get the opportunity to initialize complex variables with OIF (object interchange format) values, which is the format suggested by ODMG 3.0.

void CreateCompanies ( ) {

VARIABLES

    int           comp_number  = 0;

    int           count        = 0;

    int           car_counts[5];

    string        key;

    SET<Company> &companies   = Company;

    SET<Car>     &cars        = companies.cars;

PROCESS

   .....

}

Initialize elementary values

Elementary variables usually need not to be initialized with empty values, since this is default. Nevertheless we suggest always to initialize variables, since this makes programming more transparent. In the example the array car_counts[5] is not initialized, since the current OSI version does not yet support initializing complex values. Instead, one may initialize the array by setting each variable separately.

    car_counts[0] = 0;

    car_counts[1] = 0;

    car_counts[2] = 0;

    car_counts[3] = 0;

    car_counts[4] = 0;

Initialize extent variables

When referring to extent variables (as Company), each time, the variable is referenced a new cursor for the collection will be created. Thus, the following statement would not work properly:

    Company.get(0); // locate first instance

    Company.employees.count; // fails, no company selected

The second line uses a different cursor for the Company collection, which is not yet located and will throw an exception, since employees are not accessible. One way to avoid this is using inline functions:

    Company.get(0).{

        employees.count;

    };

The other (preferred) way is defining local variables and assigning the extent to the local variable. Those variables are defined as reference variables, since not the collection with all its data is to be assigned, but a reference to the collection, only. When initializing reference variables, always assignment by reference is used, regardless whether you use the = or &= operator.

Assigning path expressions

The second reason to use local variables is, that those are shorter and one need not always to write the complete path to the property when using operation path to access data. When referring to class properties, all references to a class property refer to the same cursor. In order to iterate to the same collection with different cursors, one may create a cursor copy, e.g. by calling

SET<Company> &employer = persons.company.cursorCopy();

Assigning operation or access paths in the initialisation code to a reference variable (as for employer or cars in the example below), the value assignment sets the reference to the result of the operation or access path. This is usually, what one wants to achieve. In this case, however, the path expression is evaluated only once, which might cause problems. In order to assign the path expression, oe may use the assignment by reference operator (&=), which assigns the path expression to the variable rather than the path result.

SET<Person> &children = persons().children(); // children for the selected person

SET<Person> &children &= persons().children(); // children for all persons

The first variable definition returns the children set for the currently selected person, while the second definition assigns the access path, which returns the children for all persons.

Calling system functions

OSI supports several built-in classes. On is the SystemClass, which provides a number of static service functions. SystemClass functions are searched, when a name could not be resolved somewhere else. Thus, one may call

    Message( ... ) but also

    SystemClass::Message( .... )

Using the scoped names is more save and prevent you from calling a function that might be defined in the context of the current class. You may use this fact, however, in order to overload (e.g. the Message() function) in one or more classes. In this case, no scope name must be defined.

The message function is called in the example in order to write output messages to the console. When composing messages from strings and integers, it is safer to cast the integer values to string as in the first message. There is automatic conversion provided within OSI functions, but in multiple operation expressions it is not so simple to determin, which will be the left operand that determines the conversion of the right operand. When there are only two operands, the second operand will be converted into the format of the first operand, which makes the example below also running without explicit type casts.

The RandomNumbers() function, which is called many times in the script, fills the array (or single value) passed as first parameter with random numbers within the range passed in the second parameter. Here, we want to get the number of cars assigned to each of the five companies in the example.

void CreateCompanies ( ) {

VARIABLES

    .....

    int           car_counts[5];

    .....

PROCESS

// create 5 companies

Message("Starting company transaction: " + (string)Time() );

    .....

    SystemClass::RandomNumbers(car_counts,20); // initialize number of cars per company between 1 and 20

Using transactions

When writing data to the database, many indexes and relationships are maintained, which makes the script, which is not very fast by nature, even slower. In order to improve the performance, transactions might be used, which perform fast operations in memory before writing data to the disk at the end.

The messages written to console will give you an impression, how long the transaction needs for being executed and for being stored.

void CreateCompanies ( ) {

VARIABLES

    .....

PROCESS

// create 5 companies

Message("Starting company transaction: " + (string)Time() );

companies.objectSpace.beginTransaction();

    .....

Message("Storing company transaction: " + (string)Time() );

companies.objectSpace.commitTransaction;

Message("Stopping company transaction: " + (string)Time() );

}

Allocate global variables

Any variable might be defined as global variable. After a global variable has been defined and initialized once, it can be accessed from within any OSI function or program. In this example, a number of global variables is defined with different filters in order to provide a reasonable algorithm for assigning children to persons.

Since referring to extents each extent reference creates a new cursor, different cursor are created for each global variable. Thus, these global variables may get different filters and may also have a different selection.

Later on the global AssignChildren() function and Person::AssignChild() refer to these variables. Then, the global variables need not to be defined, again, but could. When redefining global variables, those should not be initialized, since this will reset the previous initialization.

void InitCollections ( ) {

VARIABLES

    global set<Person>   &f20 &= Person::Persons;

    global set<Person>   &f40 &= Person::Persons;

    global set<Person>   &f60 &= Person::Persons;

    global set<Person>   &f80 &= Person::Persons;

    global set<Person>   &f100 &= Person::Persons;

    .....

PROCESS

// set filter for global male and feemale collections by age

    f20.filter('age <= 20 && sex == "female"');

    f40.filter('age > 20 && age <= 40 && sex == "female"');

    f60.filter('age > 40 && age <= 60 && sex == "female"');

    f80.filter('age > 60 && age <= 80 && sex == "female"');

    f100.filter('age > 80 && age <= 100 && sex == "female"');

    m20.filter('age > 20 && sex == "male"');

    .....

}    

Operation path

OSI supports complex operation paths. In the Person::SetAttributes() function there is an example for a complex operation path (see example below). The path starts with the Company extent from which is gets the instance with the random company number. The get function returns an instance of the collection type, on which it applies, i.e. a Company instance in this case. The Company has got a relationship employees, which extents the path in order to add the current Person instance as Employee to the companies employees collection. While doing this, the Person instance is expanded to an Employee instance.

Insert returns the Employee instance created, which calls the inline function passed in { ... }. Inline functions might be complex as class functions, i.e. when necessary, they may contain all function sections. It is, however, a better style, to use class functions rather than inline functions. Somtimes, however, inline functions are faster to implement. Finally, the inline function calculates the employees income from a random number.

  Company.get(comp_number).employees.insert(pid).{ income = number + 500; };