ODB for WARP User's Guide

By Staffan Flodin

ODB for WARP

An Object-Oriented Database V. 0.1

Introduction
The purpose of this system is to provide system developers an easy-to-use database facility with powerful query capabilities. ODB (ObjectDataBase) consists of a graphical maintenance interface where databases may be defined and updated and a database engine.

The database engine is accessed by applications through a simple and consistent application programming interface, API. ODB is a light-weight single-user main-memory object-oriented database.


 * ODB supports, subtyping, inheritance, complex objects and all objects have a unique identity hence ODB is object-oriented.
 * It always keeps the data in main memory, thus it is primarily intended for applications with moderately sized data requirements. It uses secondary storage only for storing the database between sessions.
 * It is light-weight because some of the traditional database facilities such as locking and logging does not exist in ODB. The reason is that it is intended to be used as a fast and easy to use storage and search module in any application where traditionally the application developer programmed the data storage, e.g. as sequential file scans etc.
 * It differs from other C++ based object-oriented databases in that it is dynamic in nature. Hence your application is linked and compiled once and the database schema is created or changed dynamically without any recompilation of the application.

ODB system
The ODB-system consists of two parts: A graphical user interface for creation and maintenance of the database and a C++ API for accessing the database engine from another application.

''Figure 1. System Overview''

The graphical user interface facilitates creation, update and deletion of types, properties and objects in an easy manner. The interface also supports loading, saving and creation of ODB-databases.

The C++ API gives any application access to a set of high-level easy to use interface methods to the database engine. The API gives the application access to the same functionality which is available in the user interface. In addition, the API also provides a powerful stream based query interface to the database.

2.1 Using ODB for Warp
The on-line version of ODB contains all functionality of the system, i.e. it is not a special shareware version. If you use ODB and find it useful the cost is US$99. Send an e-mail to sflodin@ozemail.com.au for further details.

2.2 Files
ODB consists of the following files:
 * pmstore.exe The ODB creation and maintenance interface.
 * ODB.h ODB C++ header file.
 * ODB.libThe ODB library.
 * Compdb.odb Sample ODB database files.
 * Demo.cpp Sample ODB application source code.

2.3 System Requirements
The system is intended to run under OS/2 Warp Version 3 or higher. Any other requirements do not exist.

3.0 System overview of ODB
An ODB-database is a collection of types with properties and objects which are instances of types. ODB-types are instances of the system class type and ODB-objects are instances of the object system class

The types are organized in a subtype-supertype hierarchy where subtypes inherit all properties defined or inherited to its supertype. In addition to the inherited properties new properties can be created. A property is a name which denotes values of objects. Properties can denote values of the following ODB-database types:
 * Integer
 * Real
 * Charstring
 * Object reference
 * Collection of Integer
 * Collection of Real
 * Collection of Charstring
 * Collection of object reference.

ODB-objects are instances of ODB-types where all properties defined or inherited to the type can be assigned values for the object.

Objects can be retrieved by their property values. And the extent of a type, i.e. the objects that belong to a particular type and all subtypes to that type, can be queried through a stream based query interface. To facilitate fast look-up of objects indexes can be built over some or all of the properties of a type.

The database can easily be updated or altered in the following ways: Integrity constraints will be maintained by the system when types or objects are deleted or modified. All properties and indexes are inherited automatically to any newly created subtype of a particular type.
 * Type creation
 * Property creation
 * Object creation
 * Index creation
 * Type deletion
 * Property deletion
 * Object deletion
 * Index deletion
 * Object modification.

4.0 Database operations
In this section the ODB-database engine methods in the API are described and exemplified as C++ examples. Throughout the examples a database object named My_DB is used to exemplify the ODB-API.

4.1 Database creation
The ODB-API contains a C++ class named database see Figure 3. The database object in your application is a particular important object. It is through the database object which all accesses to the ODB-database and all calls to the database engine go.

The database object contains the database name, an ODB-type named Usertypes, an object identifier (OID) generator and a string table.

The name of the database object is assigned to it at creation or when the database is loaded from file. The type Usertypes is the root of the user defined type tree. This type can not be given user defined properties and objects can not be created from it. Thus, if a type is created without a specified supertype the type named Usertypes will become its supertype by default.

The string table is a character string repository. All strings used in the database are stored in this table. This facilitates fast look-up of string valued properties. There are some cases where the application must access the string table but in most cases ODB handles this for the application.

The OID generator generates OIDs for all new objects or types in the database. The OID can not be changed by the application and the application must never reference object by their OID's as the OIDs of the objects may change when the database is loaded again.

If your application only needs one database this database is declared in your application as: database My_DB("DBName"); This statement declares the object My_DB to be an ODB-database object with name DBname. All applications using ODB must have a database object. There is no limit set by ODBto the number of databases a single application can use.

If several databases are required in the application, these can be created dynamically by using the C++ new-operator as: new database("OneMoreDB"); The database can be saved and restored from file as: database My_DB; My_DB.load_database("Db1.odb"); My.DB.save_database("Db1.odb"); In the above statements the database object My_DB is first loaded and then saved with file name Db1.odb.

4.2 Type creation
An ODB-type serve as a template for object creation. Properties and indexes are defined on types. A type can be declared as a subtype to any other type previously defined. If no supertype is declared, then the new type will become subtype to the type named Usertypes in the database object. ODB-types are instances of the system type named type, see Figure 2. An ODB-type inherits all properties defined to its supertype. Any indexes defined to the supertype will also be defined to the new type. Types are created by the ODB-database object as: My_DB.create_type("Person"); My_DB.create_type("Employee","Person"); Here a type named Person is first created. A supertype has not been specified thus the type named Person will be a subtype of the type Usertypes in the database object My_DB. In the next example a type named Employee is created as a subtype to the type named Person.

If several databases are active in some application there may be types with identical names in the different databases. Since references to types go through the database object there is never any ambiguity when referencing types by their names.

The return value from the create_type-statement is a reference to the new type or the NULL-reference if the type creation failed. The following example declares a type reference that can be bound to the return value from the create_type statement: ODB_TYPE My_NewType; My_NewType=My_DB.create_type("Person"); The return value needs to be assigned a reference. The type is inserted in the type hierarchy of the database and can be referenced by its name whenever required.

4.3 Property Creation
Properties are defined to types and for the objects of that type the properties can be given values. Properties are typed where the following types together with their identifiers are supported: To create an integer valued property named salary to the type named employee in the database My_DB we write: My_DB.add_property("Employee","Salary",_INT_); Analogously, an object valued property named Boss is created as: My_DB.add_property("Employee","Boss",_OBJECT_); The return value is either less than or greater than zero if the operation failed or succeeded, respectively.

The add_property-statement can be executed any time and all subtypes to the target type that may exist will also be given the property. All instances to the types which has been given a new property will also be given the property. The value however will be undefined and it is up to the application that the value is not referenced prior to being assigned a value.

4.4 Object creation
Objects are created as instances of a particular type. When objects are created these are given all the properties defined to the type. The values of the properties are undefined and must be assigned values prior to being referenced.

To create an instance of the type named Person in the database My_DB we write: My_DB.create_instance("Person"); The return value is a reference to the newly created object or the NULL reference. To declare a variable which can be assigned the result of the create_instance operation we write: ODB_REF My_NewObj; My_NewObj=My_DB.create_instance("Person"); The return value may be discarded but then there is a problem in referencing the object at a later stage. Perhaps the best procedure is to create an object and keep a reference to it. This reference can then be used when the properties of the objects are assigned values. When values are assigned to its properties, the reference may be discarded and the object can at a later stage be referenced through its attribute values. This is easily done through the query interface described later in this manual.

4.5 Object modification
Object modification means changing the values of its properties. This is done by using sending the message set_propertyvalue to the database object as: My_DB.setproperty_value(My_NewObj,"Salary",(ODB_INT)53200); Where My_NewObj is an ODB-object declared and created, e.g as: ODB_REF My_NewObj; My_NewObj=My_DB.create_instance("Employee"); The object is given the value 53200 for the property named Salary. Note that constants may require type casting for disambiguation purposes. In this example the constant 53200 is cast to the type ODB_INT. The result from the database method setproperty_value is greater than or less than zero if the operation was successful or failed, respectively. When property values are modified all indexes are automatically updated to consider the new value for the property.

4.6 Index creation
To make retrievals through some properties fast, indexes can be created. For example, if a frequent search in the database is on a particular attribute e.g. Salary then an index should be created on that attribute to facilitate fast access to the objects with a particular value for the attribute named Salary.

Indexes are created from the database object through the create_index method. The following example creates an index on the Salary property of the type Employee. My_DB.create_index("Employee","Salary");

Note that if an index is created to some attribute of a particular type, the index will also be created to that attribute in all subtypes. In the above example any subtype to Employee will get an index on the salary property. It does not matter if the index is created before or after the subtypes are created. It does not matter if instances to the types the index is created over has been created. All existing instances will be entered into the index structures automatically by ODB.

4.7 Type deletion
Types can be deleted from the database dynamically. This is a particularly costly operation. When a type is deleted so are the subtypes to the type as well as all objects that are instances to the type or to any subtype of the type. Hence, A type deletion is a recursively applied down the type tree rooted at the specified type to delete.

Types are deleted by the database object by sending the delete_type message. The result of the operation is either greater than or less than zero if the operation was successful or failed, respectively. To delete the type Person from the database My_DB we write: My_DB.delete_type("Person");

This will delete the type named Person, all instances of that type and then recursively delete all subtypes to the Person type. This operation can be costly to perform when there are a lot of objects and types that has to be deleted and their memory freed. Note especially that any references maintained by the application to types or objects which have been deleted must be reset by the application. Dereferencing such a reference is an undefined operation.

4.8 Property deletion
Single properties can be deleted from types. When a property is deleted from some type it is deleted from all subtypes as well.

When a property is deleted from a type the property is also deleted from all object that are instances of that type, i.e. the extent of the type.

Properties are deleted from types by sending the delete_property message to the database object. To delete the property named Salary from the type named Employee in the database My_DB we write: My_DB.delete_propery("Employee","Salary");

Note that if the property that is to be deleted is inherited to the specified type, that property can not be deleted as that would leave the database schema in an inconsistent state. To properly delete a property, it must be deleted from the most general type it is defined for. (A supertype is more general than its subtypes) This is because the object-oriented model states that any operation op applicable to a type t is also applicable to all subtypes of the type t. Hence, deleting an inherited property would violate this constraint.

4.9 Delete Object
Objects may be deleted. When an object is deleted the system automatically deletes all references to that object from other objects in the database. Any references kept by the application to the deleted object must be reset by the application itself.

Objects are deleted by sending the delete_object message to the database. When deleting an object the database object must be provided a handle to the object that is subject of deletion.

A handle can be obtained through the query interface or other methods that returns object handles.

To exemplify, consider the following example where a handle obj is declared. This handle is then assigned an object through the getobjectwithoid method. This object is then deleted by the database object: //Declare object handle, //database and an integer

ODB_REF obj; database My_DB("ExDB"); ODB_INT i=0;

My_DB.create_type("Person");

//Create 500 objects while(i++ < 500) My_DB.create_object("Person");

//Assign an object to the handle, We can be //quite certan there is an object with OID 415 now obj=My_DB.getobjectwithoid("Person", 415);

//Delete the object //If there were no object with OID=415 //then obj==NULL but that is all handeled //by ODB My_DB.delete_object(obj); Any other object that reference the deleted object through some attribute will now have that attribute value set to NULL. Note that in this example the object handle obj is outside the control of ODB and must not be used by the application after the deletion. This is up to the application programmer to see to.

4.10 Delete index
Indexes may be deleted by sending the drop_index message to the database object. In the following example the index over Salary for type Employee in database My_DB is deleted: My_DB.drop_index("Employee","Salary"); The result is greater or less than zero if the operation was successful or failed respectively.

5.0 The query interface
The query interface is a stream based query interface. In a stream based query interface the application opens a stream from which objects can be retrieved one by one until no more objects are returned. Thus, no materialization of the entire set of object that may constitute the result of some query occurs. The basic operations on streams are: Streams may be opened over any type or over another existing stream. Streams have a state which means that it depends on the previous operations what the result of the get operation will be. As a consequence of this streams may be opened and objects retrieved one by one at arbitrary occasions, the stream will hold its state and return the next object upon request.
 * Open
 * Get
 * Reset
 * Close

Open streams may be reset which means that their state is changed to some initial state. In order to redo a query this is a useful feature that saves the application from having to open and combine some new streams to redo the same query.

Streams which will not be used any more may be closed. Closing a stream means it cannot be reused and any reference to any stream which is closed must not be reused.

There are three different types of streams: Query streams are opened over types to select all or those object which comply with some condition that can be defined when opening the stream. Select streams are opened over another stream to select only those objects that satisfy a condition that is defined with the select stream.
 * Query stream
 * Select stream
 * Semi-join stream

Semi-join streams are opened over two other streams. The semi join stream returns those object from the left operand stream for which there exists an object in the right operand stream that satisfy some condition defined over the two streams.

5.1 Opening streams
Streams are opened by sending a message to the database object. The return value is either a reference to the new stream or NULL if the operation was successful or failed, respectively. In this section stream handles and open stream statements are explained.

5.1.1 Stream handles
There are three C++ types that may be used to declare stream handles. A handle, q_strm, to a query_stream is declared as: ODB_QSTREAM q_strm; A declaration of a select stream, s_strm, handle is: ODB_SSTREAM s_strm; And for a semi-join stream the handle, sj_strm, is declared as: ODB_SJSTREAM sj_strm;

5.1.2 Opening a query stream
Query streams are opened over some type. This means that the stream accesses the objects that are instances of the type, the type extent, and the instances of its subtypes.

For example, in this figure, if a query stream is opened over the type named Person then all objects of type person and of the type named Employee will be considered as possible objects to output.

A query stream can be opened over a type named Employee as: My_DB.open_stream("Employee"); This stream will retrieve every object in the extent of type Employee and in the extent of all subtypes to the type named Employee, called the deep extent of type Employee. A stream can be opened which only retrieves object that satisfy some condition defined with the stream. Assume we want to retrieve all persons aged 18, i.e. all objects in the deep extent of the type named Person that have the value 18 for its age property. This is done as: My_DB.open_stream("Person","Age", "=",(ODB_INT)18); In this statement we used the =-operator to compare the values of the objects with the value defined with the stream. For integer and real there are additional comparison operators that can be used. These are: For object reference, charstring and collection valued properties only equality is meaningful thus when opening streams with condition on such properties the operator can be excluded.

For collections the equality operator is overloaded. If one operand is a collection and the other is a non collection type then the equality means the member operator. If on the other hand both operands are sets then the equality operator means that the same members should be found in both sets, i.e. A=B means that if an object, o, is a member of set A then o must also be a member of the set B and vice versa.

To exemplify how streams are opened over character strings, collections and object reference valued properties consider this example:

A stream is to be opened over the Employee type where all Employees which have the name "Bob Smith" should be returned from the stream. This is written as: My_DB.open_stream("Employee","Name","Bob Smith"); Streams with restrictions over collections and object reference valued properties are opened analogously.

5.1.3 Opening a select stream
Select streams are opened over existing streams to add additional conditions on the objects that are sought. If for example, all Person objects with age greater than 18 and whose name is "John Doe" are interesting we write: ODB_QSTREAM s1; ODB_SSTREAM s2; //Open s1 s1=My_DB.open_stream("Person","Age",">=",(ODB_INT)18); Now, s1 retrieves all objects of type Person with age greater than or equal to 18. The additional name condition can be added by opening a select_stream over this stream as: s2=My_DB.select(s1,"Name","John Doe"); Here, a stream is opened over the existing stream, s1, and a condition on the name attribute is defined and the result is bound to the stream variable s2. The objects are retrieved from s2 by the get-method.

5.1.4 Open a semi join stream
The semi-join operation is a variant of the traditional join operator in the relational algebra. The semi-join stream is opened over two streams where attribute values from objects in one stream are compared with attribute values of the objects from the other stream. Only objects from one stream is in the result of the stream. The latter is the difference from a traditional join operator where the result is a tuple of values, (See any textbook that covers the relational algebra and relational databases).

To exemplify, assume we have two types, Pet and Person where both have a character string valued name-attribute. If we would like to retrieve those persons that has the same name as a pet has, we write: ODB_QSTREAM qst1,qst2; ODB_SJSTREAM sjs;

//Open qst1 over persons and //qst2 over pets qst1=My_DB.open_stream("Person"); qst2=My_DB.open_stream("Pet");

//Open the semi join stream sjs=My_DB.semi_join(qst1,"Name",qst2,"Name"); The objects returned will be from the stream qst1 that has the same name-value as any object of type pet.

5.2 Retrieving objects from streams
Given an open stream, objects can be retrieved through that stream by sending the get message to the database object with the stream as an additional argument with the message. The result is either an object or NULL depending on the success of the operation.

The following example opens a stream and retrieves all objects that satisfy the condition that their value for the age property must be greater than 65. ODB_QSTREAM qst; ODB_REF obj;

//Open the stream qst=My_DB.open_stream("Person","Age" ,">=",65); if (qst==NULL) { //Do some error recovery //qst may be the NULL reference if 	//the open statement failed } else { //Get the first object and //then the rest of the objects obj=My_DB.get(qst); while(obj!=NULL){ //do something with obj obj=My_DB.get(qst); } } //end else This example shows the standard way of iterating through a stream. When all objects are retrieved (a NULL reference is returned) the stream can either be closed or reset to its initial state.

5.3 Stream reset
When an opened stream has reached its end or if we by some other reason do not want to retrieve any more objects from it, the stream can be reset. When a stream is reset, it is set to the same state as if it was just opened.

Streams that will be used again to query the data should be reset instead of closed and recreated. A stream is reset by sending the reset_stream message to the database object as: My_DB.reset_stream(qst); Where qst is an object of type query, select or semi join stream.

5.4 Stream close
Streams are closed when it is no longer to be reused again. For composite streams, closing the outermost stream will cause all streams it uses to be closed as well. In Figure 4 this means that closing the stream s2 will also close the stream s1.



In the same figure, closing s1 only will leave s2 in an undefined state. Streams are closed by sending the close_stream message to the database object as: ODB_QSTREAM qst; // Open qst, and use it My_DB.close_stream(qst); After a stream has been closed, the application must not try to use the reference unless the reference is assigned another stream.

6.0 Collections
The datatype collection is somewhat more complex than the other datatypes supported by ODB. A collection is similar to a set with the difference that a collection can contain several equal objects or values.

6.1 Creating collections
Collections are created by sending the create_collection message to the database object. On successful creation a handle to an empty collection is returned. This handle must ALWAYS be assigned a reference owned by the application since the collection otherwise will be inaccessible to the application. The application owned reference can be released when the collection is referenced from elsewhere, e.g. through an attribute of some object.

A collection which may contain references to other objects is created and assigned a collection handle ch as: ODB_SET ch; ch=My_DB.create_collection(_OBJECT_); Valid type identifiers are to the create_collection method are: Thus, it is not possible to declare a collection to contain other collections.
 * _INT_
 * _REAL_
 * _CHARSTRING_
 * _OBJECT_

6.2 Adding elements to a collection
When a collection has been created, elements may be inserted into the collection. ODB supports two ways of doing this; by sending the collection_insert message to the database object or by sending the insert message directly to the collection object.

In the following example a collection of character strings are created and bound to the handle coll. Then two elements are inserted into the set by collection_insert and by insert. ODB_SET coll;

//Create a collection coll=My_DB.create_collection(_CHAR_); My_DB.collection_insert(coll,"Object"); coll->insert("Orientation"); The difference with these two ways is that by sending the collection_insert message to the database objects all necessary checks are performed before insertion. When the insert message is sent to a collection the application must ensure that the collection is of the correct type and that the collection handle is initialized properly.

The return value from collection_insert is:
 * > 0 upon successful insert
 * == -1 for improper handle
 * ==-2 if the collection was of another type than the element to insert

6.3 Delete element from collection
Elements may be deleted from a collection by either sending the delete_element message to the set object or by sending the delete_from_collection message to the database object. In the following example objects are deleted from the set coll. ODB_SET coll;

//Create collection coll=My_DB.create_collection(_INT_);

//Insert into collection using both //insertion methods My_DB.collection_insert(coll,2); coll->insert(3);

//remove from collection My_DB.delete_from_collection(coll,3); coll->delete(2); Using the delete_from_collection message to the database object is a safer way, since all necessary checks are performed before detection of any element. However, sending the delete message directly to the collection object is slightly faster but then the application must ensure that the handle is initialized etc. Note especially that before using the delete message directly to the collection object to delete an ODB_CHAR the characterstring must be substituted with the characterstring stored in the string table. The same is true for inserting characterstrings directly into a collection The following example illustrates the correct procedure: ODB_REF cstr, cstr2; ODB_SET coll; coll=My_DB.create_collection(_CHAR_);

//Get a handle to the stored ODB string //Don't bother if you know it does not //exist, ODB takes all reqd actions cstr=My_DB.getstring("ODB");

//Insert it into the collection My_DB.collection_insert(coll,cstr);

//The following will not delete //ODB from the collection coll.delete("ODB");

//The following will delete // "ODB" from the collection cstr2=My_DB.getstring("ODB"); coll.delete(cstr2);

6.4 Delete collection
Collections are deleted by either sending the delete_collection message to the database object or the delete_set message to the collection object.

7.0 Datatypes
There are a number of ODB specific datatypes and type identifiers that are used to interface ODB in the API.

7.1 C++ Datatypes
There are a number of C++ datatypes and classes defined in the API these are: These types are used by the application using ODB to declare interface variables to ODB.

The C++ classes the API provides are:

7.2 Type identifiers
There are a number of type identifiers defined in the API which are used to specify to ODB which type to consider. These are: These identifiers are used to identify objects of C++ datatypes as: Table1. Type and Id mapping Note that the C++ datatype ODB_SET is used to denote all collection types. The type identifiers are used to denote the result type of properties when these are created and to denote which type of elements a collection can contain.
 * _INT_
 * _REAL_
 * _CHAR_
 * _OBJECT_
 * _OBJECT_COLLECTION_
 * _INT_COLLECTION_
 * _REAL_COLLECTION_
 * _CHAR_COLLECTION_

8.0 More interface methods
In addition to database creation, maintenance and querying there are methods to print objects, retrieve object and types etc. these are described here.

8.1 The string table
Each database object contains a stringtable where all strings used by the database are stored. Before some operations involving strings can be executed the application must get a handle to the string maintained by the database otherwise equality tests etc. will not function properly. In most cases ODB does this for you. A string handle to a particular string is obtained by sending the getstring message to the database object. In the following example a handle is obtained to the string "ODB Rules". ODB_CHAR str; str=My_DB.getstring("ODB Rules"); The following example demonstrates the benefit if the string table: ODB_CHAR s1, s2; s1=My_DB.getstring("ODB"); s2=My_DB.getstring("ODB"); if (s1==s2) cout << "\nODB Rules\n"; else cout << "\nYou cheated\n"); The output will of course be: ODB Rules

8.2 Printing ODB objects
All objects, types and databases may be printed by sending the display message to the object. To support any print-method the application may want to utilize all display methods of all ODB objects print in a buffer which is provided by the ODBAPI. The display message can be sent to any OBD object with a display buffer as argument as: displaybuffer buf; database odb; ODB_TYPE tp;

//Create a type tp=odb.create_type("Person");

//print type to buffer tp->display(buf);

//print buffer to stdout cout << buf.get_buffer;

//reset buffer before used buf.reset_buffer;

8.2.1 Display buffer
OBD provides a class named outputbuffer which all display methods use. A buffer, Disp_Buf is declared in the application as: outputbuffer DispBuf; The buffer can be retrieved by sending the get_buffer message as: Disp_Buf.getbuffer; The return value is a reference to a character buffer as: ODB_CHAR BufPtr BufPtr=Disp_Buf.getbuffer; The reference to the buffer can then be used by the application to print the content of the buffer on a widget in a GUI or on a text display using cout etc. as e.g.: ODB_CHAR BufPtr BufPtr=Disp_Buf.getbuffer; cout << BufPtr; When the buffer has been printed or to prevent it from overflowing, it can be emptied by sending the reset_buffer message to the buffer object as: DispBuf.reset_buffer; The buffer size is 1k and if there is not enough free space in it when used by some object, the display output from the object will be lost.

8.3 Getting a type handle
If a handle is required to a type for e.g. printing the gettypenamed message to the database object as: ODB_TYPE tp; tp=My_DB.gettypenemrd("Person"); In this example a handle is returned to the type named Person. If no such type exists the NULL handle is returned.

8.4 Getting an object handle
Object handles can retrieved by using the query interface, see Section 5.0, or by sending the getobjectwithoid message to the database object as: ODB_REF o; o=My_DB.getobjectwithoid("Usertypes",45); In this example the object with oid equal 45 is returned, if no such object exist a NULL handle is returned. Note especially that there is no guarantee that objects will get the same oid when reloaded from file, in fact it is very likely that it will get another oid.

8.5 Traversing all subtypes of a type
If the application for some reason wants to traverse the type tree of the database the application then first sends the getallsubtypes message to the database object to get the first subtype. All subsequent subtypes are retrieved one by one by sending the getnextsubtype message to the database object. The following example shows how all types in the database are printed on stdout: ODB_CHAR tp; //type names ODB_TYPE tph; //type handle database My_DB;

My_DB.load_database("Compdb.odb"); tp=My_DB.getallsubtypes; while(tp!=NULL){ cout << tp << `\n'; tp=My_DB.getnextsubtype; //Type handles to the subtypes //can be obtained as	tph=My_DB.gettypenamed(tp); }

8.6 Traversing direct subtypes of a type
A direct subtype of a type, t, is a type whose supertype is the type t. The direct subtypes are traversed by first sending the getsubtypes message to the database object and then sending the getsubtype message to get the other subtype names, one by one as: char *tp; //type names ODB_TYPE tph; //type handle database My_DB;

My_DB.load_database("Compdb.odb"); tp=My_DB.getsubtypes("Person"); while(tp!=NULL){ cout << tp << '\n'; tp=My_DB.getsubtype("Person"); //Type handles to the subtypes //can be obtained as	tph=My_DB.gettypenamed(tp); } In this example all direct subtypes of the type named Person are retrieved.

8.7 Getting the supertype to a type
The supertype name of a particular type can be retrieved by using the getsupertype message to the database object. In this example the supertypename of the type named Employee is retrieved and then used to get a handle to the supertype to the type named Employee. ODB_CHAR stpnm; ODB_TYPE stp; stpnm=My_DB.getsupertype("Employee"); stp=My_DB.gettypenamed(stpnm);

8.8 Getting all properties of a type
The names of all properties of a type can be retrieved b y first sending the getfirstprop message to the database object and then get the rest of the property names by sending the getnextprop message to the database object.

In this example all properties of the type named Employee are printed to stdout. ODB_CHAR pn; //prop names database My_DB;

My_DB.load_database("Compdb.odb"); pn=My_DB.getfirstprop("Employee"); while(pn!=NULL){ cout << pn << '\n'; pn=My_DB.getnextprop("Employee"); }

8.9 Getting the type of a property
As described in Section 4.3 all properties are associated with a type where the type describes which objects the property can have as values.

The type of a property can be retrieved by sending the getpropertytype message to the database object. The returned value is a type identifier, see section 4.3. In the following example the type identifier is retrieved from the name property of the Person type. ODB_TYPE_TAG ttg; ttg=My_DB.getpropertytype("Person","Name");

8.10 Getting the value of an object property
Each property denotes a value or other object. To retrieve the denoted value or object the application sends the getproperty_value message to the database object.

It is important to note that the return value is a code indicating the success of the operation. The value is retrieved by sending the reference to the variable the result is intended to be retrieved into. In the next example the value of a set valued property is retrieved into the variable coll: ODB_SET coll; ODB_REF obj;

//obj is assigned a person object //and person have a collection //valued property named Parents //defined to them.

ok=My_DB.getproperty_value(obj,"Parents",&coll); The return value is:
 * > 0 if the operation succeeded
 * ==-1 if the object reference was NULL
 * ==-2 if the property did not exist
 * ==-3 if the expected result type did not match the resulttype of the property.

Conclusion
This system is intended to support developers in managing moderately sized data in an easy manner. The data can be created and changed outside the application by using a graphical user interface and later loaded into the application easily without any recompilation or other cumbersome procedure.

The system supports the object-oriented data modelling paradigm for the data stored. The data can easily be queried through a high level stream based interface which the API provides.

This is the firs released version of the system and any bug reports or comments are welcomed to me.

A Sample application
To give an idea of how ODB is used the following example code illustrates the interface. This application file is included in the ftp package ODB.ZIP.


 * 1) include 
 * 2) include 
 * 3) include 
 * 4) include 
 * 5) include 
 * 6) include 
 * 7) include 
 * 8) include "odb.h"

outputbuffer obuf=outputbuffer;

void browse_extent(ODB_TYPE tp,database db){ //this function browses through all objects in the //extent of the specified type tp ODB_QSTREAM q_stream; ODB_CHAR tpname; ODB_REF o;

if (tp==NULL) return;    //quit if shitty reference

tpname=tp->getname;    //this method is not described //in the manual, it is found //in odb.h

q_stream=db.open_stream(tpname);

if (q_stream==NULL) return;  //should not happen //(they all say that)

cout<<"Browsing the extent of type "<<tpname<<'\n'; obuf.reset_buffer;   //reset the buffer

//follows is the standard structure of iterating through streams //at ****** Any Code ***** application specific code are enterd //in below example the object bound to o is just printed to stdout //- //--- Std stream iteration- //- //--          o=db.get(q_stream);                -- //--         while (o!=NULL){                   -- //--           ****** Any Code *****            -- //--           o=db.get(q_stream);              -- //--         }                                  --  //-  //--- End Std stream iteration- //- o=db.get(q_stream); while (o!=NULL){ o->display(obuf); cout<<obuf.get_buffer<<'\n'; obuf.reset_buffer; o=db.get(q_stream); }

//- //--                I M P O R T A N T            -- //- //-- The application MUST deallocate the stream  -- //-- here, otherwise a memory leak would occur as-- //-- the stream can not be referenced again     -- //-- Alternatively, declare it as STATIC        -- //- db.close_stream(q_stream); }

void display_schema(database db){ //For each type in the loaded database schema //display its information by sending the display message to the //type object and then for each type display its propperties ODB_CHAR tpname, propname; ODB_TYPE tphandle;

obuf.reset_buffer; //empty any old crap in the buffer

tpname=db.getallsubtypes;  //get the first typename

while (tpname!=NULL){    //loop over all typenames

tphandle=db.gettypenamed(tpname); //given a type name the handle //can be fetched if (tphandle!=NULL) { //display the type object and all its properties tphandle->display(obuf); cout<<"\n--\n"; cout<<obuf.get_buffer<<'\n'; obuf.reset_buffer; cout<<"\n--- Press any key for more ---\n" ; getch;   //let the user control the flow };       //end while

tpname=db.getnextsubtype;    //get next typename }           //end while

}           //end function

void select_type(database db){ //In this function the user is provided all usertypes //in the database and then promted to select one whose //extent are to be browsed ODB_CHAR tpname; char usertpnm[20]; ODB_TYPE utp; tpname=db.getallsubtypes; while (tpname!=NULL){ cout<>usertpnm; utp=db.gettypenamed(usertpnm); if (utp==NULL) cout<<"\nThere no type named "<<usertpnm<<" in the database\n"; else browse_extent(utp,db); }

void find_all_janitors(database My_DB){ //In this function all janitors are retrieved. //This is done by first finding the department //object with "Janitors" as value in to the //Name attribute ODB_QSTREAM q_stream; ODB_SSTREAM s_stream; ODB_REF o,jo; char typenm[16]="Department"; char propnm[8]="Name"; char val[16]="Janitors";

//Open a stream over the department type with the condition that //the Name attribute must have Janitors as value q_stream=My_DB.open_stream(typenm,propnm,val);

//The stream may not be opened if so it is best to check before use. if (q_stream==NULL) { cout<<"\nError when opening stream bla bla bla...\n"; return; };

//The stream was opened allright. In the remaining we only consider the //first found department named Janitors, thus we do not assume the name //to be a key to departments

o=My_DB.get(q_stream);

//If o==NULL yhen there were no such department in the database if (o==NULL) { cout<<"No department named "<<val<<" exist in the database\n"; return; }

//No longer req'd My_DB.close_stream(q_stream);

//reuse q_stream to open the stream to select all employees at //the department bound to object reference o

q_stream=My_DB.open_stream("Employee","EmployedAt",o); if (q_stream==NULL) { cout<<"\nError when opening stream over employee\n"; return; }; cout<<"\nThese are the Janitors\n-\n"; obuf.reset_buffer;     //reset the buffer //Now, we can start browsing the janitors jo=My_DB.get(q_stream); while (jo!=NULL){ jo->display(obuf); cout<",(ODB_INT)10000);

cout<<"\n Theese are all the rich janitors\n"; obuf.reset_buffer;     //reset the buffer //Now, we can start browsing the rich janitors jo=My_DB.get(s_stream); while (jo!=NULL){ jo->display(obuf); cout<<obuf.get_buffer<<'\n'; obuf.reset_buffer; jo=My_DB.get(s_stream); };

My_DB.close_stream(s_stream);}

void demonstrate_index(database &My_DB){ //In this fn a new type is created. The type is then //given an integer valued proerty which can be assigned //random values. //First create 5000 objects and find the ones with a particular //value for the attribute. Measure the time for this. Then create //an index and redo the search and measure time. struct timeb t1,t2; double elapsed_time; ODB_REF o; int i,v; ODB_QSTREAM q_stream;

My_DB.set_tablesize(3000); //3K table size

//Create a type named testtype My_DB.create_type("Testtype");

//Create an integer valued property named Int_prop My_DB.add_property("Testtype","Int_prop",_INT_);

//Create 5000 objects and assign random values to //the attribute named Int_prop for each object. cout<<"\nCreating 50000 objects ..."; i=0; while (i++<50000){

//Create an object and assign o to reference it   o=My_DB.create_instance("Testtype");

//generate a random number in [0..4000] v=rand%4000;

//set the property named Int_prop of the object o   //to v    o->setproperty_value("Int_prop",(ODB_INT)v); }; cout<<"Done\n";

//Now, lets find all objects with Int_prop = 2345 //the probability that this number is less than 1 //Thus, it may be the case that no object exists //with that property value. To make sure there is //atleast one such object we simply assign an object //that value for the property.

//To make sure we do this right we first find out the //OID of the last object. Then we subtract a number less //than 50000 from the OID. We now have an OID to one of //the 50000 object we just generated. Retrieve it and //set the value. i=0; My_DB.getproperty_value(o,"OID", &(ODB_INT)i); i=i-rand%45000; o=My_DB.getobjectwithoid("Testtype",i); o->setproperty_value("Int_prop",(ODB_INT)1234);

//Now that we know there is an object with 1234 as //value for the Int_prop property we can start to  //search for it. obuf.reset_buffer; q_stream=My_DB.open_stream("Testtype","Int_prop","=",(ODB_INT)1234);

//All set for test search. Start timer and go for it ftime(&t1); o=My_DB.get(q_stream); while (o!=NULL){ o->display(obuf); cout<<obuf.get_buffer<<'\n'; obuf.reset_buffer; o=My_DB.get(q_stream); };

//All objects found, stop timer and check time ftime(&t2); elapsed_time=(t2.time*1000+t2.millitm)-(t1.time*1000+t1.millitm); cout<<"\The search took "<display(obuf); cout<<obuf.get_buffer<<'\n'; obuf.reset_buffer; o=My_DB.get(q_stream); }; ftime(&t2); elapsed_time=(t2.time*1000+t2.millitm)-(t1.time*1000+t1.millitm); cout<<"\The search took "<<elapsed_time<<"millisec with index";

//- //--                I M P O R T A N T            -- //- //-- Since the application now no longer need the-- //-- 50000objects etc we now deallocate them    -- //-- There are several ways of doing this. -- //-- 1: Open a stream over all Testtype objects  -- //-- and deallocate them one by one. -- //-- 2: Since the type named Testtype is not     -- //-- wanted anymore we simply delete the type   -- //-- and let ODB do the hard work of deallocating-- //-- everything, e.g. objects indexes ...... -- //--                                             --  //-- And also, we allocated a stream local to    -- //-- to this function. This function must be    -- //-- deallocated to prevent memory leak. -- //-

My_DB.delete_type("Testtype"); My_DB.close_stream(q_stream);

//It is now ok to exit this function

} int main (void){ database My_DB("");

int retval; retval=My_DB.load_database("Compdb.odb"); if (retval<0) cout<<"\n Failed to load company database "; else { cout<<"\n The company database is loaded"; My_DB.display(obuf); cout<<"\n"<<obuf.get_buffer; obuf.reset_buffer; };

//display the schema of the loaded database display_schema(My_DB);

//let the user select and browse a type extent select_type(My_DB);

//demonstrate the search interface find_all_janitors(My_DB);

//show the benefit of indexing when the database //is large demonstrate_index(My_DB);

cout<<"\nODB Demo is now finished\n"; return 1; };