The information about queries and indexes is organized in the following manner:
Therefore, among the database services provided by ObjectStore is support for query processing. A query facility with adequate performance must go beyond support for linear searches. So ObjectStore provides a query optimizer, which formulates efficient retrieval strategies, minimizing the number of objects examined in response to a query. The query facilities are used from within C++ programs, and they treat persistent and nonpersistent data in an entirely uniform manner.
For more information, see Chapter 2, Collection, Query, and Index Classes, in the ObjectStore Collections C++ API Reference.
Declaration
This function is declared
os_Collection<E> &query( char *type_string, char *query_string, os_database *schema_database = 0, char *file_name = 0, os_unsigned_int32 line = 0, os_boolean dups = query_dont_preserve_duplicates ) const;where E is the element type parameter.
os_database *people_database; os_Set<person*> *people; . . . os_Set<person*> &teenagers = people->query( "person*", "this->age >= 13 && this->age <= 19", people_database );
collection-expression.query( element-type-name, query-string, schema-database ) collection-expressionThe collection-expression in the above example is *people, and defines the collection over which the query will be run.
element-type-name
The argument element-type-name, person* in this example, is a string indicating the element type of the collection being queried.
The query-string is a C++ control expression indicating the query's selection criterion. In this example it is this->age >= 13 && this->age <= 19. An element, e, satisfies the selection criterion if the control expression evaluates to a nonzero int (true) when e is bound to this.
If the transient database is specified, the application's schema (stored in the application schema database) is used to evaluate the query. The application schema database almost always contains the required schema, but it might be closed at the time of the call to query(). So using it as the schema_database argument might involve the overhead of a database open.
file_name and line arguments
ObjectStore uses the file_name and line arguments when reporting errors related to the query. You can set them to identify the location of the query's source code.
If the dups argument is the enumerator query_dont_preserve_duplicates, duplicate elements that satisfy the query condition are not included in the query result. If dups is the enumerator query_preserve_duplicates, duplicate elements that satisfy the query condition are included in the query result. Using query_dont_preserve_duplicates (the default) typically results in better performance.
Return value
The return value of query() refers to a collection that is allocated on the heap. So when you no longer need the resulting collection, you should reclaim its memory with ::operator delete() to avoid memory leaks. The resulting collection has the same behavior as the collection being queried. The order of the elements in the result cannot be guaranteed to be the order of the elements in the collection being queried. Queries Compared to Collection Traversals
The query above serves as a sort of shorthand for the following collection traversal:
os_Set<person*> *people; . . . os_Cursor<person*> c(*people); os_Set<person*> *teenagers =&os_collection::create( os_database::get_transient_database() ); person *p = 0; for(p = c.first(); c.more() ; p = c.next()) if (p->age >= 13 && p->age <= 19) *teenagers |= p; os_set *people; . . . os_cursor c(*people); os_set *teenagers = &collection::create(os_database::transient_database); person *p = 0; for(p = (person*) c.first(); c.more() ; p = (person*) c.next()) if (p->age >= 13 && p->age <= 19) *teenagers |= p;
os_database *people_database; os_Set<person*> *people; . . . os_Set<person*> &teenagers = people->query( "person*", "age >= 13 && age <= 19", people_database );
For more information, see Chapter 2, Collection, Query, and Index Classes, in the ObjectStore Collections C++ API Reference.
Declaration
This function is declared
E query_pick(char*, char*, os_database*) const;where E is the element type parameter.
Calls to query_pick() have the same form as calls to query(). If using the parameterized version, os_Collection::query_pick(), the return value is the the os_Collection parameter type. If you are using the nonparameterized version, os_collection::query_pick(), the return value is void*, and you will often have to apply a cast to the result.
os_database * parts_database; os_Set<part*> *parts; . . . part *part_number_411 = parts->query_pick( "part*", "part_number == 411", parts_database );If more than one element satisfies the query's selection criterion, one of them is picked and returned. So, except for the additional opportunities for optimization, using query_pick() is equivalent to calling os_Collection::pick() on the result of invoking query().
If no element satisfies the query, 0 is returned.
Existential queries are also discussed in Nested Existential Queries.
Calls to exists() have the same form as calls to query() and query_pick(), but instead of returning a collection or element, they return a nonzero os_int32 (int or long, whichever is 32 bits on your platform) for true, and 0 for false.
Example exists()
Here is an example:
os_database *print_request_db; class page {...int page_number;...}; class print_request {...os_List<page*> *pages;...}; print_request *request; . . . if (request->pages->exists( "page*", "page_number > 100", print_request_db ) ) request->queue_at_end(print_queue);
collection-expression [: int-expression :]where collection-expression is some element of the top-level integer-expression of type os_Collection, and int-expression is the selection criterion for the nested query.
A nested single-element query has the form
collection-expression [% int-expression %]where collection-expression and int-expression are as for nested collection queries.
These nested queries all have the same characteristics as the query expressions discussed in Performing Queries with query(), except that the collection returned is converted to an int by the query processor. The returned collection is converted to 0 (that is, false) if it is empty, and a nonzero int (that is, true) if it is nonempty.
For more information, see Chapter 2, Collection, Query, and Index Classes, in the ObjectStore Collections C++ API Reference.
Example Nested Query
Here is a query that finds the musicians among a company's employees:
class employee {... os_Set<hobby*>& hobbies; ...}; class hobby {... char *name; ...}; os_Set<employee*> &employees = ...; . . . os_Set<employee*> musicians = employees->query( "employee*", "hobbies[:!strcmp(name, "music"):]", db );In this query, the selection criterion is the query string hobbies [: !strcmp(name, "music") :]. Since this is a nested query expression, the collection that it designates is converted to an int. The nested query is converted to 0 (false) when it returns an empty set (there is no hobby named music). Otherwise it is converted to a nonzero value.
The query string in the above example is therefore equivalent to
[:hobbies[:!strcmp(name, "music"):].size !=0:]
This is particularly useful for performing existential queries. Consider the following query:
os_database *db; os_Set<part*> &a_set ... ; a_set->query("part*", "children[:1:]", db);This query selects all parts in a_set that have children. This is because, for each element, e, of a_set, e->children[:1:] returns e->children, which is converted to an integer, and if the integer is nonzero (true), e is selected. If e->children is empty, it is converted to 0 (false), and so is not selected.
To find the parts in a_set that have no children, you can use the following query:
os_database *db; os_Set<part*> &a_set ... ; a_set->query("part*", "!children[:1:]", db);Here is a query that selects those descendents of a_part that have children all of which are primitive. So it selects all descendents that are strictly on the second level from the bottom of the assembly.
os_database *db; part *a_part; a_part->get_descendents()->query( "person*", "children[:1:] && !children[:children:]", db );For more information, see Chapter 2, Collection, Query, and Index Classes, in the ObjectStore Collections C++ API Reference.
Example Nested Existential Query
Below is a final example involving nesting of queries. This locates the employee whose child has social security number 123456789.
employees->query_pick( "employee*", "children[:ss==123456789:]", db );The inner query returns 0 (false), for a given employee, if none of her children has social security number 123456789. In this case, the employee is not selected. If, for a given employee, at least one of her children does have social security number 123456789, the inner query returns an int greater than 0 (true), and the employee is selected.
For more information, see Chapter 2, Collection, Query, and Index Classes, in the ObjectStore Collections C++ API Reference.
Form of the call
A preanalyzed query is created with one of the static member functions os_coll_query::create(), os_coll_query::create_pick(), or os_coll_query::create_exists(). Calls to these functions have the form
os_coll_query::create( element-type-name, query-string, schema-database )A const os_coll_query& is returned in each case.
os_coll_query::create() must be called from within an ObjectStore transaction. An os_coll_query object can be created persistently, but it is up to the application to keep track of how to get at it in subsequent transactions (navigable from a root).
For more information, see Chapter 2, Collection, Query, and Index Classes, in the ObjectStore Collections C++ API Reference.
Function Calls in Query Strings
As with the query strings introduced earlier, the query string here is an int-valued expression, and calls to strcmp() and strcoll() are allowed, as are calls involving comparison operators for which the user has defined a corresponding rank function, and calls to member functions satisfying the restrictions described in Paths and Member Functions.
const os_coll_query &age_range_query = os_coll_query::create( "person*", "age >= *(int*)min_age_ptr && age <=*(int*)max_age_ptr", db1 );This creates a preanalyzed query for people in a given age range. Note the type casts used to specify the types of the free variables min_age_ptr and max_age_ptr.
The bound query constructor takes two arguments: a preanalyzed query, and a keyword_arg list, an instance of os_keyword_arg_list. Here is an example:
int teenage_min_age = 13, teenage_max_age = 19; os_bound_query teenage_range_query( age_range_query, ( os_keyword_arg("min_age_ptr", &teenage_min_age), os_keyword_arg("max_age_ptr", &teenage_max_age) ) );This creates a bound query for finding teenagers using the analyzed query in the previous example.
( keyword_arg-expr, keyword_arg-expr, . . . , keyword_arg-expr )You create a keyword_arg with the constructor for the class os_keyword_arg, as in
os_keyword_arg("min_age_ptr", &teenage_min_age)This binds the address teenage_min_age to the variable min_age_ptr in the query string, specifying the value to be used in analyzing the query.
const os_coll_query &the_query = os_coll_query::create( "person*", "age >= (int)a_func((int) x)", db1 );might be bound with
int current_x = 7; os_bound_query the_bound_query( the_query, ( os_keyword_arg("x", current_x), os_keyword_arg("a_func", a_func) ) );Note that when a query is evaluated, functions will be invoked an undefined number of times, depending on the evaluation plan formulated by the query optimizer. So, for functions with side effects, the actual results are undefined.
The functions strcmp() and strcoll() are specially recognized by the query optimizer, so you do not have to bind them.
The bound query can then be used directly in query evaluation, as in
people.query(teenage_range_query);Note that, as with the other overloadings of query functions, the return value refers to a collection that is allocated on the heap. So when you no longer need the resulting collection, you should reclaim its memory with ::operator delete() to avoid memory leaks.
For more information, see Chapter 2, Collection, Query, and Index Classes, in the ObjectStore Collections C++ API Reference.
Indexes and Query Optimization
Adding an Index to a Collection with add_index()
You can direct ObjectStore to optimize queries over a particular collection by adding an index into the collection with the member function os_collection::add_index().
a_set->query_pick("part*", "part_number==411", db1)
os_Set<part*> *a_set; . . . os_index_path &key_spec = os_index_path::create("part*", "part_number",db1); a_set->add_index(key_spec);The key here is specified with a reference to a path. See Creating Paths.
The last argument to add_index() specifies the database, segment, or object cluster in which the index is stored. The index remains until it is removed with drop_index(). By default, an index is placed in the same segment as the collection for which the index is being added.
Index Maintenance
You might need to perform index maintenance for any data member or member function in the path used to specify the index key. See Performing or Enabling Index Maintenance.
Pointer-Valued Members and char* Keys
If you create an index with a path to a pointer-valued data member - other than a char*-valued member - you can optimize lookup based on address. char*-valued data members are treated specially. An index based on a char* member optimizes lookup by the string pointed to. Indexes and Performance
Without the index, a linear search must be used to perform such queries, and each element of a_set will have to be examined. By adding an index into a_set, you are instructing the system to maintain an access method (consisting of hash tables and/or a B-tree) allowing efficient lookup by part_number. Dropping Indexes from a Collection with drop_index()
Indexes can be added and dropped during the run of an application. If an index makes sense for only part of an application's run, the application can add an index and then remove it later. For example, if the first part of an application performs many lookups but relatively few updates, and the second part performs many updates and relatively few lookups, the program can add an index at the beginning of the first part, and then remove the index at the beginning of the second part. Example:
You remove an index with the member function drop_index(). Here is an example:
drop_index()
os_Set<part*> *a_set; . . . os_index_path &key_spec = os_index_path::create("part*","part_number", db1); . . . a_set->drop_index(key_spec);Note that you specify the key, with an os_index_path, when dropping an index, because the same collection can have several different indexes - to optimize different kinds of lookups.
The os_index_path argument does not need to be the same instance of os_index_path supplied when the index was added, but it must specify the same key. If the path strings used to create two os_index_paths differ only with regard to white space, the os_index_paths specify the same index key.
If an index with the specified key was never added to the collection,
err_no_such_index is signaled.You can add and drop indexes at run time as frequently as you like. The ObjectStore query optimizer adapts dynamically.
For more information, see Chapter 2, Collection, Query, and Index Classes, in the ObjectStore Collections C++ API Reference.
Testing for the Presence of an Index with has_index()
You can also test for the presence of an index with a specified key, using the member function has_index().
has_index(path,os_index::ordered) | Returns true |
has_index(path,os_index::unordered) | Returns true |
has_index(path,os_index::ordered) | Returns false |
has_index(path,os_index::unordered) | Returns true |
os_Set<part*> *a_set; . . . os_index_path &key_spec = ... . . . if (a_set->has_index(key_spec options)) ...options for has_index can have the value ordered or unordered.
The function os_collection::get_indexes() allows you to retrieve information on all the indexes into a specified collection. For more information, see Chapter 2, Collection, Query, and Index Classes, in the ObjectStore Collections C++ API Reference.
Indexes and Complex Paths
Path expressions can specify not just a single data member, but a navigational path involving multiple member accesses. Such path expressions can be used to specify index keys. For example, suppose you want to optimize lookup of a part based on the emp_id of any of the responsible_engineers for the part (suppose that the member responsible_engineers is collection valued). You can use the following path:
os_index_path::create( "part*","(*responsible_engineers)[]->emp_id", db1)This path is like ones you have seen before, except that here the data member name responsible_engineers is followed by the symbols [], indicating that the next component of the path (emp_id) is to be applied to each element of the collection of responsible engineers, rather than to the collection itself. Note that you cannot end an expression with the symbols [].
part_extent->query( "part*", "part_number > 411", db1 ) part_extent[:part_number > 411:]Use of a B-tree also makes iteration in order of part_number more efficient (see Controlling Traversal Order).
The os_index_path::ordered Enumerator
You request an ordered index by specifying os_index_path::ordered as the second argument to add_index() when requesting the index:
os_Set<part*> *a_set; . . . os_index_path &a_path = os_index_path::create("part*","part_number", db1); a_set->add_index(a_path, os_index_path::ordered);Here, os_index_path::ordered is an ObjectStore-supplied enumerator.
Of course, if a B-tree is best for parts of your application, and a hash table is best for other parts, you can add and drop indexes of these types dynamically, as appropriate. You cannot have an ordered and unordered index using the same os_index_path on a collection.
For more information, see os_index_path::ordered in Chapter 2, Collection, Query, and Index Classes, of the ObjectStore Collections C++ API Reference.
Index Option Enumerators
You can also specify a variety of other index options using various other enumerators. The enumerators can be combined into a bit pattern with bit-wise disjunction (using | (bit-wise or)). Here is the complete list of index option enumerators; for additional information see the corresponding entries in Chapter 2, Collection, Query, and Index Classes, in the ObjectStore Collections C++ API Reference.
os_index_path::unordered | os_index_path::allow_duplicates | os_index_path::copy_keyBy default, an index is allocated in the same segment as its associated collection. But if you want, you can supply an os_database* or os_segment* indicating where to allocate a new index. Supply this information as the third argument, for calls that include an options argument. Otherwise, supply the clustering information as the second argument.
This option simplifies the coding of updates, compared to options 2 and 3.
Option 2: user-controlled index maintenance (using os_backptr)
Declare an os_backptr and perform user-controlled index maintenance, using make_link() and break_link(). See Declaring an os_backptr Member and User-Controlled Index Maintenance with an os_backptr.
Like option 3, this option can make coding updates to the member slightly more complex, compared to option 1, but it avoids the use of "wrapper objects" and the apparent value/actual value distinction. See The Actual Value/Apparent Value Distinction for more information.
Like option 1, this option carries extra space overhead, compared to option 3, in the form of an os_backptr member for each object containing the member.
Option 3: user-controlled index maintenance (without os_backptr)
Do not declare an os_backptr and perform user-controlled index maintenance, using the collection operations insert() and remove(). This option only applies if the member is not mentioned in any multistep path used as an index key. See User-Controlled Index Maintenance Without an os_backptr.
Like option 2, this option can make coding updates to the member slightly more complex, compared to option 1, but it avoids the use of "wrapper objects" and the apparent value/actual value distinction. See The Actual Value/Apparent Value Distinction for more information.
With this option, you do not need to declare an os_backptr, so you avoid the space overhead, incurred with options 1 and 2, of an os_backptr member for each object containing the member. But the index maintenance accompanying each data member update can be more expensive.
Declaring an os_backptr Member
To make a data member indexable, you add to the class whose data member you want to be indexable a public or private data member of type os_backptr. The declaration of the data member of type os_backptr must precede the declaration of the data member (or member functions) you want to make indexable. Example: os_backptr declaration
Here is an example:
class part { public: . . . os_backptr b ; int id ; department *dept ; . . . part(); ~part(); . . . };Note that it is sufficient to define a single data member of type os_backptr for all indexable members of a class.
class base1 {... os_backptr b1 ; ...} ; class base2a : public base1 {...} ; class base2b {... os_backptr b2b ;...} ; class derived : public base2a, public base2b { char *name ; } ;Class derived's name member can use class base1's os_backptr member b1. Any data member in class base2a can also use class base1's os_backptr member b1. Indexable members in class base2b should continue to use base2b's os_backptr member b2b.
Now consider
class base1 {...os_backptr b1 ;...} ; class base2a : virtual public base1 {...} ; class base2b {...os_backptr b2b ; ... } ; class derived : public base2a, public base2b { os_backptr d ; char *name ; } ;It is not possible for class derived's indexable data members to use base1's os_backptr member, because base2a inherits class base1 virtually. For the same reason, data members in class base2a cannot use class base1's os_backptr member b1. Since base2b is not inherited along the leftmost side of the type inheritance lattice, an additional os_backptr member (d in this example) must be defined to allow name to be indexable.
This is also true for pointer-valued data members. Because the pointer is the index value, ObjectStore does not detect updates and you must use os_backptr::break_link() and os_backptr::make_link() for index maintenance whenever you update the member.
For more information, see also Chapter 4, Macros and User-Defined Functions, in the ObjectStore Collections C++ API Reference.
The os_indexable_member() Macro
For a data member normally declared as
value-type member-name ;declare it instead as
os_indexable_member( class-name,member-name,value-type) member-name ;where class-name is the name of the class defining the indexable member, member-name is the name of the data member being made indexable, and value-type is the member's type.
os_indexable_body(part,id,int,os_index(part,b)); os_indexable_body(part,idx2,int,os_index(part,b));Here, the macro calls have the form
os_indexable_body( class, member, value-type, backptr-spec)where
os_index( class,member)where class is the name of the class defining the indexable member, and member is the name of the os_backptr-valued data member appearing before indexable members of the class.
This implicitly defined class defines operator =() and a conversion operator (operator int() in this example) for converting instances of the implicitly defined type to instances of the apparent value type. Under most circumstances these operators make the container object transparent.
a_part->id = 411And to get the value and pass it to a function expecting an int argument, you do
f(a_part->id)Since f() expects an int argument (the apparent but not actual value type of id), the conversion operator will be invoked, making the above call equivalent to
f(a_part->id.operator int())For cases where the actual value cannot be transparent, you should use the functions getvalue() and setvalue() defined by the actual value type of the indexable member. For example:
printf("The id is %d \n", a_part->id); /* This won't work correctly */will not work because the compiler cannot tell that the second argument to printf() is supposed to be an int, so the conversion operator is not invoked. Instead you should use
printf("The id is %d \n", a_part->id.getvalue());or
printf("The id is %d \n", (int)(a_part->id));The getvalue() and setvalue() functions will always work correctly for getting and setting indexable data member values.
Restriction on Updates
Note that if the values of an indexable data member are instances of a user-declared class (not pointers to such instances), the values of such an instance's data members cannot be directly altered without circumventing the required index maintenance. To make such a change, the value of the indexable data member must be replaced wholesale with a modified copy of the old value. That is, the instance must be copied and altered, and then the altered object must be copied back as the new value of the indexable member. User-Controlled Index Maintenance with an os_backptr
Using the macros described in Enabling Automatic Index Maintenance allows ObjectStore to perform fully automatic index maintenance for data members. But, for any data member, you can avoid using these macros (and the accompanying actual value/apparent value distinction) by using the functions os_backptr::break_link() and os_backptr::make_link() whenever you update the member. In this case you need only define the os_backptr member; the indexable member itself can be declared in the normal way.
You also use overloadings of make_link() and break_link() to perform index maintenance for member functions called in query or path strings.
Making and Breaking Links on Indexable Data Members
Call break_link() just before making a change to an indexable data member (this removes an entry from each relevant index), and call make_link() just after making the change (this inserts a new entry into each relevant index, indexing the object by the new value of the relevant path). You can ensure that this happens by encapsulating these calls in a member function for setting the value of the data member. Example: message class definition
For example, given the following class definition:
#include <stddef.h> #include <ostore/ostore.hh> #include <ostore/coll.hh> . . . class message { public: . . . os_backptr b; int id; /* an indexable member */ char *subject_line; /* a second indexable member */ class date { public: int day; int month; int year; } date_received; /* a third indexable member */ . . . message(int id, char*subj, int dd, int mm, int yy); ~message(); int set_id(int); char *set_subject_line(char*); void set_date(int dd, int mm, int yy); };
int message::set_id(int i) { b.break_link( &id, &id, os_index(message,b) - os_index(message,id) ); id = i; b.make_link( &id, &id, os_index(message,b) - os_index(message,id) ); return i; } /* end set_id() definition */ char *message::set_subject_line(char *subj) { b.break_link( &subject_line, &subject_line, os_index(message,b) - os_index(message,subject_line) ); if (strlen(subj) < 500) strcpy(subject_line, subj); else error("string too long"); b.make_link( &subject_line, &subject_line, os_index(message,b) - os_index(message,subject_line) ); return subj; } /* end set_subject_line() definition*/ void message::set_date(int dd, int mm, int yy) { b.break_link( &date_received, &date_received, os_index(message,b) - os_index(message,date_received) ); date_received.day = dd; date_received.month = mm; date_received.year = yy; b.make_link( &date_received, &date_received, os_index(message,b) - os_index(message,date_received) ); } /* end set_date() definition */Note that since the values of date_received are instances of a user-defined class, date, this example assumes that you have defined and registered a rank function (and possibly a hash function) for the class date. See the examples in Supplying Rank and Hash Functions.
If these set-value functions provide the only interface for modifying the values of indexable members, indexes will be properly maintained. Circumventing the interface, for example, by passing the address of an indexable member value to a function that alters its value through the pointer, can result in inconsistent indexes.
Making and Breaking Links to Indexed Member Functions
To maintain indexes keyed by paths containing member function calls, use the following new overloadings of os_backptr::make_link() and os_backptr::break_link():
void make_link( void* ptr_to_obj, void* ptr_to_obj, const char* class_name, const char* function_name ) const ; void break_link( void* ptr_to_obj, void* ptr_to_obj, const char* class_name, const char* function_name ) const;Automatic index maintenance is not available for such indexes.
class_name is the name of the class that defines the member function called in the path of the indexes to be updated.
function_name is the name of the member function itself.
Call break_link() just before making the change (this removes an entry from each relevant index), and call make_link() just after making the change (this inserts a new entry into each relevant index, indexing the object by the new value of the relevant path). You can ensure that this happens by encapsulating these calls in a member function for setting the value of the data member.
For indexes keyed by paths that go through the elements of a collection (for example, * ( (*get_children())[]->get_location() ) ) index maintenance is performed automatically when you change the membership of a collection. See Example: Member Function Calls in Query and Path Strings.
User-Controlled Index Maintenance Without an
Collections do automatic index maintenance. Therefore you avoid os_backptr overhead in the following way: If a member is not mentioned in any multistep path used as an index key, you can perform index maintenance by using collection insert and remove operations. Performing index maintenance in this way allows you to avoid declaring an os_backptr member. See Performing or Enabling Index Maintenance.
os_backptr
When you update the data member, follow this procedure:
Example: Member Function Calls in Query and Path Strings
Below is a listing of three files that make up a simple program using member function calls in paths and queries: Files used in this example
get_location()->xthat is, the x-coordinate of a rectangle's location.
#include <ostore/ostore.hh> #include <ostore/coll.hh> #include <iostream.h> #include <stdlib.h> #include <string.h> class coord { public: os_backptr b ; /* needed for indexable member */ os_indexable_member(coord,x,int) x ; os_indexable_member(coord,y,int) y ; coord(int x1, int y1) { x = x1 ; y = y1 ; } coord() { x = 0 ; y = 0 ; } } ; class rectangle { private: os_backptr b ; /* needed for query functions */ char *label ; int length ; int width ; os_Collection<rectangle*> &children ; coord location ; public: rectangle( const char *lbl, int l, int w, const os_Collection<rectangle*> *chldrn_ptr, coord lcn ) ; rectangle(const char *lbl) ; ~rectangle() ; char *get_label() ; void set_label(const char *lbl) ; int get_length() ; void set_length(int l) ; int get_width() ; void set_width(int w) ; os_Collection<rectangle*> *get_children() ; coord *get_location() ; void set_location(coord lcn) ; int get_area() ; static os_typespec *get_os_typespec() ; } ; os_query_function(rectangle,get_label,char*) ; os_query_function(rectangle,get_length,int) ; os_query_function(rectangle,get_width,int) ; os_query_function(rectangle,get_location,coord*) ; os_query_function(rectangle,get_area,int) ; os_query_function(rectangle,get_children,\ os_Collection<rectangle*>*) ;Notice that there is no function for setting the children of a given rectangle. This is because the same collection is used to record the children of a rectangle throughout the rectangle's lifetime. Changes in a rectangle's children are reflected by insertions and removals performed on this collection. This means that rectangle::get_children() does not need to call make_link() and break_link(); index maintenance is performed by the collection's insert and remove operations.
#include <ostore/ostore.hh> #include <ostore/coll.hh> #include <ostore/manschem.hh> #include "rectangle.hh" OS_MARK_SCHEMA_TYPE(rectangle); OS_MARK_SCHEMA_TYPE(coord); OS_MARK_QUERY_FUNCTION(rectangle,get_label); OS_MARK_QUERY_FUNCTION(rectangle,get_length); OS_MARK_QUERY_FUNCTION(rectangle,get_width); OS_MARK_QUERY_FUNCTION(rectangle,get_location); OS_MARK_QUERY_FUNCTION(rectangle,get_area); OS_MARK_QUERY_FUNCTION(rectangle,get_children);
#include <ostore/ostore.hh> #include <ostore/coll.hh> #include <iostream.h> #include "rectangle.hh" os_indexable_body(coord,x,int,os_index(coord,b)) ; os_indexable_body(coord,y,int,os_index(coord,b)) ; os_query_function_body(rectangle,get_label,char*,b) ; os_query_function_body(rectangle,get_length,int,b) ; os_query_function_body(rectangle,get_width,int,b) ; os_query_function_body(rectangle,get_location,coord*,b) ; os_query_function_body(rectangle,get_area,int,b) ; os_query_function_body(\ rectangle,get_children,os_Collection<rectangle*>*,b) ; int coord_rank(const void *arg1, const void *arg2) { const coord *c1 = (const coord *)arg1 ; const coord *c2 = (const coord *)arg2 ; if ( c1->x < c2->x ) return os_collection::LT ; else if ( c1->x > c2->x ) return os_collection::GT ; else if ( c1->y < c2->y ) return os_collection::LT ; else if (c1->y > c2->y) return os_collection::GT ; else return os_collection::EQ ; }Notice the calls to os_query_function_body(). The rank function coord_rank() is defined here so that we can perform queries comparing coord objects, and so we can create an index keyed by a path ending in coord objects, in our case the path
* ( (*get_children())[]->get_location() )
rectangle::rectangle( const char *lbl, int l, int w, const os_Collection<rectangle*> *chldrn_ptr, coord lcn ):children( os_Collection<rectangle*>::create( os_segment::of(this)) ) { label = new( os_segment::of(this), os_typespec::get_char(), strlen(lbl)+1 ) char[strlen(lbl)+1] ; strcpy(label, lbl) ; length = l ; width = w ; children = *chldrn_ptr ; location.x = lcn.x ; location.y = lcn.y ; } rectangle::rectangle(const char *lbl) : children( os_Collection<rectangle*>::create(Note that the set functions perform index maintenance, calling break_link() and make_link() for each query function whose return value depends on the underlying private data member. For example, set_width() calls break_link() once for the query function get_width() and once for the query function get_area(), since both get_width() and get_area() use the private data member width to derive their return values.
os_segment::of(this)) ) { label = new( os_segment::of(this), os_typespec::get_char(), strlen(lbl)+1 ) char[strlen(lbl)+1] ; strcpy(label, lbl) ; length = 0 ; width = 0 ; location.x = 0 ; location.y = 0 ; } rectangle::~rectangle() { delete [] label ; } char *rectangle::get_label() { return label ; } void rectangle::set_label(const char *lbl) { b.break_link(this, this, "rectangle", "get_label") ; label = new( os_segment::of(this), os_typespec::get_char() strlen(lbl)+1 ) char[strlen(lbl)+1] ; strcpy(label, lbl) ; b.make_link(this, this, "rectangle", "get_label") ; } int rectangle::get_length() { return length ; } void rectangle::set_length(int l) { /* two query member functions depend on this data member */ /* so we call each of make_link() and break_link() twice */ b.break_link(this, this, "rectangle", "get_length") ; b.break_link(this, this, "rectangle", "get_area") ; length = l ; b.make_link(this, this, "rectangle", "get_length") ; b.make_link(this, this, "rectangle", "get_area") ; } int rectangle::get_width() { return width ; } void rectangle::set_width(int w) { /* two query member functions depend on this data member */ /* so we call each of make_link() and break_link() twice */ b.break_link(this, this, "rectangle", "get_width") ; b.break_link(this, this, "rectangle", "get_area") ; width = w ; b.make_link(this, this, "rectangle", "get_width") ; b.make_link(this, this, "rectangle", "get_area") ; } os_Collection<rectangle*> *rectangle::get_children() { return &children ; } coord *rectangle::get_location() { return &location ; } void rectangle::set_location(coord lcn) { b.break_link(this, this, "rectangle", "get_location") ; location.x = lcn.x ; location.y = lcn.y ; b.make_link(this, this, "rectangle", "get_location") ; } int rectangle::get_area() { return length * width ; } void print_rects(os_Collection<rectangle*> &the_rects) { os_Cursor<rectangle*> c(the_rects) ; for ( rectangle *r = c.first() ; r ; r = c.next() ) cout << r->get_label() << "\n" ; cout << "\n" ; }
As noted earlier, there are no make_link() and break_link() calls associated with changing a rectangle's children, because index maintenance is performed automatically by the insertion and removal operations of the collection get_children() returns.
void mquery(os_database *db) { os_index_key(coord,coord_rank,0) ; rectangle *r1 = new(db, rectangle::get_os_typespec()) rectangle("1") ; rectangle *r2 = new(db, rectangle::get_os_typespec()) rectangle("2") ; rectangle *r3 = new(db, rectangle::get_os_typespec()) rectangle("3") ; os_Collection<rectangle*> &the_rectangles = os_Collection<rectangle*>::create(db) ; the_rectangles |= r1 ; the_rectangles |= r2 ; the_rectangles |= r3 ; os_index_path &label_path = os_index_path::create("rectangle*", "get_label()", db) ; os_index_path &length_path = os_index_path::create("rectangle*", "get_length()", db) ; os_index_path &width_path = os_index_path::create("rectangle*", "get_width()", db) ; os_index_path &x_location_path = os_index_path::create("rectangle*", "get_location()->x",db); os_index_path &y_location_path = os_index_path::create("rectangle*", "get_location()->y", db) ; os_index_path &area_path = os_index_path::create("rectangle*", "get_area()", db) ; os_index_path &children_loc_path = os_index_path::create("rectangle*", "* ( (*get_children())[]->get_location() )", db) ; the_rectangles.add_index( label_path, os_index_path::unordered) ; the_rectangles.add_index(length_path, os_index_path::ordered) ; the_rectangles.add_index(width_path, os_index_path::ordered) ; the_rectangles.add_index(x_location_path, os_index_path::ordered) ; the_rectangles.add_index(y_location_path, os_index_path::ordered) ; the_rectangles.add_index(area_path, os_index_path::ordered) ; the_rectangles.add_index(children_loc_path, os_index_path::ordered) ; r1->set_label("rect1") ; r1->set_length(20) ; r1->set_width(60) ; r1->set_location(coord(10, 20)) ; r1->get_children()->insert(r2) ; r2->set_label("rect2") ; r2->set_length(40) ; r2->set_width(35) ; r2->set_location(coord(20, 40)) ; r2->get_children()->insert(r3) ; r3->set_label("rect3") ; r3->set_length(200) ; r3->set_width(80) ; r3->set_location(coord(50, 60)) ; cout << "Rectangles labeled \"rect1\":\n" ; os_Collection<rectangle*> &answer = the_rectangles.query( "rectangle*","strcmp(\"rect1\", get_label()) == 0",db) ; print_rects(answer) ; cout << "Rectangles with length < 100:\n" ; answer = the_rectangles.query("rectangle*", "get_length() < 100",db) ; print_rects(answer) ; cout << "Rectangles with width > 50:\n" ; answer = the_rectangles.query("rectangle*", "get_width() > 50",db) ; print_rects(answer) ; cout << "Rectangles with location x coord <= 25:\n" ; answer = the_rectangles.query("rectangle*", "get_location()->x <= 25",db) ; print_rects(answer) ; cout << "Rectangles with area >= 3000:\n" ; answer = the_rectangles.query("rectangle*", "get_area() >= 3000",db) ; print_rects(answer) ; cout<<"Rectangles with children whose location < (30, 50):\n"; const os_coll_query &children_loc_query = os_coll_query::create("rectangle*", "(*get_children())[:*get_location()<*(coord*)a_coord_ptr :]", db) ; coord a_coord(30, 50) ; os_bound_query bound_children_loc_query( children_loc_query, os_keyword_arg("a_coord_ptr", &a_coord)) ; answer = the_rectangles.query(bound_children_loc_query) ; print_rects(answer) ; os_Collection<rectangle*> *answer_ptr = &answer ; delete answer_ptr ; }
main(int, char **argv) { /* argv[1] is the database */ cout << "\nStarting mquery ...\n\n"; objectstore::initialize(); os_collection::initialize(); OS_BEGIN_TXN(tx1, 0, os_transaction::update) mquery(os_database::create(argv[1])) ; OS_END_TXN(tx1) cout << "Done.\n\n" ; }The function main() initializes ObjectStore and ObjectStore collections, and calls mquery(), passing in a newly created database. The function mquery() registers the coord rank function, and then creates three rectangles, adding them to a collection, the_rectangles.
Next, mquery() creates several index paths involving member functions. Then it adds indexes to the_rectangles, specifying the paths as index keys. Then various set functions are used, triggering the index maintenance coded earlier in rectangle.cc. The collection of children for two of the rectangles is modified with rectangle::get_children() and os_Collection::insert(), which triggers index maintenance performed by insert().
Finally, various queries involving member functions are performed on the_rectangles.
Starting mquery ... Rectangles labeled "rect1": rect1 Rectangles with length < 100: rect1 rect2 Rectangles with width > 50: rect1 rect3 Rectangles with location x coord <= 25: rect1 rect2 Rectangles with area >= 3000: rect3 Rectangles with children whose location < (30, 50): rect1 Done.This driver demonstrates how to express queries with member functions. To verify that index maintenance is being performed properly, you can modify the driver to create more rectangles and insert them into the_rectangles. If the_rectangles has only three elements, the query optimizer chooses not to use indexes in query evaluation. Here is an example:
char s[500]; rectangle *rx = i rectangle *ry = 0 ; for (int i = 0 ; i < 200 ; i++) { sprintf(s, "%d", i); rx = new(db, rectangle::get_os_typespec()) rectangle(s) ; the_rectangles |= rx ; rx->set_length(i) ; rx->set_width(i) ; rx->set_location(coord(i, i)) ; if (ry) rx->get_children()->insert(ry) ; ry = rx ; }
Updated: 03/31/98 15:29:01