The Storage Kit: BQuery

Derived from: public BObject

Declared in: <storage/Query.h>


Overview

The BQuery class defines functions that let you search for records that satisfy certain criteria. Querying is the primary means for retrieving, or "fetching," records from a database.


Defining a Query

To define a query, you construct a BQuery object and supply it with the criteria upon which its record search will be based. This criteria consists of tables and a predicate:

Let's say you want to find all records in the "People" table that have "age" values greater than 12. The BQuery definition would look like this:

   /* We'll assume that myDb is a valid BDatabase object. */
   BQuery *teenOrMore = new BQuery();
   BTable *people = myDb->FindTable("People");
   
   /* Add the table to the BQuery. */
   teenOrMore->AddTable(people);
   
   /* Create the predicate. */
   teenOrMore->PushField("age");
   teenOrMore->PushLong(12);
   teenOrMore->PushOp(B_GT);

The Table List

A single BQuery, during a single fetch, can search in more than one table. When you call AddTable(), the previously added table (if any) isn't bumped out of the table list; instead, the tables accumulate to widen the range of candidate records. However, all BTables that you pass as arguments to AddTable() (for a single BQuery) must belong to the same BDatabase object.

Another way to add multiple tables to a query is to use the AddTree() function. AddTree() adds the table represented by the argument and all tables that inherit from it. Table inheritance is explained in the BTable class specification.

You can't selectively remove tables from a BQuery's table list. If you feel the need to remove tables, you have two choices: You can remove all tables (and the predicate) through the Clear() function, or you can throw the BQuery object away and start from scratch with a new one.

The Predicate

As mentioned earlier, the BQuery predicate is constructed using "reverse Polish notation" (or "RPN"). In this construction, operators are "post-fixed"; in other words, the operands to an operation are pushed first, followed by the operator that acts upon them. That's why the predicate used in the example, "age > 12", was created by pushing the elements in the order shown:

   /* Predicate construction for "age > 12" */
   teenOrMore->PushField("age");
   teenOrMore->PushLong(12);
   teenOrMore->PushOp(B_GT);

The query operators that you can use are represented by the following constants:

Constant Meaning
B_EQ equal
B_NE not equal
B_GT greater than
B_GE greater than or equal to
B_LT less than
B_LE less than or equal to
B_AND logical AND
B_OR logical OR
B_NOT negation
B_ALL wildcard (matches all records)

Except for B_ALL, the query operators expect to operate on two previously pushed operands. B_ALL , which is used to retrieve all the records in the target tables, should be pushed all by itself (through PushOp()).

Complex Predicates

You can create complex predicates by using the conjunction operators B_AND and B_OR. As with comparison operators, a conjunction operator is pushed after its operands; but with the conjunctions, the two operands are the results of the two previous comparisons (or previous complex predicates).

For example, let's say you want to find the records for people that are between 12 and 36 years old. The programmatic representation of this notion, and its reverse Polish notation, looks like this:

Programmatic expression: ( "age" > 12 ) && ( "age" < 36)

Reverse Polish Notation: "age" 12 B_ GT "age" 36 B_LT B_AND

The RPN version prescribes the order of the BQuery function calls:

   /* Predicate construction for "(age > 12) and (age < 36)" */
   teenOrMore->PushField("age");
   teenOrMore->PushLong(12);
   teenOrMore->PushOp(B_GT);
   
   teenOrMore->PushField("age");
   teenOrMore->PushLong(36);
   teenOrMore->PushOp(B_LT);
   
   teenOrMore->PushOp(B_AND);

Predicates can be arbitrarily deep; the complex predicate shown above can be conjoined with other predicates (simple or complex), and so on.


Fetching

Once you've defined your BQuery, you tell it to perform its search by calling the Fetch() function:

   if (teenOrMore->Fetch() != B_NO_ERROR)
      /* the fetch failed */

When it's told to fetch, a BQuery object sends the table and predicate information to the Storage Server and asks it to find the satisfactory records. The winning records (identified by their record IDs) are returned to the BQuery and placed in the BQuery's record ID list, which you can then step through using CountRecordIDs() and RecordIDAt():

   long num_recs = teenOrMore->CountRecordIDs();
   record_id this_rec;
   
   for (int i = 0; i < num_recs; i++)
      this_rec = teenOrMore->RecordIDAt(i);

To turn the BQuery's record IDs into BRecord objects, you pass the IDs to the BRecord constructor:

   BList *teens = new BList();
   long num_recs = teenOrMore->CountRecordIDs();
   record_id this_rec;
   BRecord *teen_rec;
   
   for (int i = 0; i < num_recs; i++)
   {
      this_rec = teenOrMore->RecordIDAt(i);
      teen_rec = BRecord new(people->Database(), this_rec);
      teens->AddItem(teen_rec); 
   }


Live Queries

By default, a BQuery performs a "one-shot" fetch: Each Fetch() call retrieves record IDs, sets them in the BQuery's record ID list, and that's the end of it. Alternatively, you can declare a BQuery to keep working--you can declare it to be "live"--by passing TRUE as the argument to the constructor:

   BQuery *live_q = new BQuery(TRUE);

When you tell a live BQuery to fetch, it searches for and retrieves record ID values, just as in the default version, but then the Storage Server continues to monitor the database for you, noting changes to records that would affect your BQuery's results. If the data in a record is modified such that the record now passes the predicate whereas before it didn't, or now doesn't pass but used to, the Server automatically sends messages that will, ultimately, update your BQuery's record list to reflect the change. In short, a live BQuery's record list is always synchronized with the state of the database. But you have to do some work first.

Preparing your Application for a Live Query

It was mentioned above that the Storage Server sends messages to update a live BQuery. The receiver of these messages (BMessage objects) is your application object. In order to get the update messages from your application over to your BQuery, you have to subclass BApplication's MessageReceived() function to recognize the Server's messages. Below are listed the messages (as they're identified by the BMessage what field) that the function needs to recognize:

what Value Meaning
B_RECORD_ADDED A record ID needs to be added to the record list.
B_RECORD_REMOVED An ID needs to be removed from the list.
B_RECORD_MODIFIED Data has changed in a record currently in the list.

The only thing your MessageReceived() function needs to do to properly respond to a Storage Server message is pass the message along in a call to the Storage Kit's global update_query() function, as shown below:

   #include <Query.h>
   
   void MyApp::MessageReceived(BMessage *a_message)
   {
      switch(a_message->what) {
         case B_RECORD_ADDED     :
         case B_RECORD_REMOVED :
         case B_RECORD_MODIFIED :
            update_query(a_message);
            break;   
          /* Other app-defined messages go here */
         ...
         default:
            BApplication::MessageReceived(a_message);
            break;
      }
   }

update_query() finds the appropriate BQuery object and calls its MessageReceived() function. The default BQuery MessageReceived() implementation handles the B_RECORD_ADDED and B_RECORD_REMOVED messages by manipulating the record list appropriately. In the case of a B_RECORD_MODIFIED message, the BQuery does nothing.

If you want to handle modified records in your application, you can create your own BQuery-derived class and re-implement MessageReceived() . To get the identity of the record, you retrieve, from the BMessage, the long data named "rec_id". The following code demonstrates the general look of such a function:

   /* Re-implementation of MessageReceived() for MyQuery,
    * a BQuery-derived class 
    */
   void MyQuery::MessageReceived(BMessage *a_message)
   {
      record_id rec;
      
      rec = a_message->FindLong("rec_id");
   
      switch(a_message->what) {
         case B_RECORD_MODIFIED :
            /* do something with the record */
            break;
         case B_RECORD_ADDED:
         case B_RECORD_REMOVED:
            /* Pass the other two message types to BQuery. */
            BQuery::MessageReceived(a_message);
            break;
      }
   ...

Keep in mind that you don't have to derive your own class to take advantage of the live query mechanism. Simply getting to the update_query() step is enough to keep the your BQuery's record list up-to-date.


Hook Functions

MessageReceived() Can be overridden to handle live BQuery notifications.


Constructor and Destructor


BQuery()

      BQuery(bool live = FALSE)

Creates a new BQuery object and returns it to you. If live is TRUE , the BQuery's record list is kept in sync with the state of the database (after the object performs its first fetch). If it's  FALSE , the database isn't monitored.

See the class description for more information on live BQuery objects.


~BRecord()

      ~BRecord(void)

Frees the memory allocated for the object's record list. If this is a live BQuery, the Storage Server is informed of the object's imminent destruction (so it won't send back any more database-changed notifications).


Member Functions


AddRecordID()

      void AddRecordID(record_id id)

Tells the BQuery to consider the argument record to be a winner, whether it passes the predicate or not. You call this function before you fetch; after the fetch, you'll find that id has been added to the record list (and will be monitored, if this is a live query). You can call this function any number of times and so add multiple "predicate-exempt" records, but you can add each specific record only once (duplicate entries are automatically squished to a single representative).

The set of exempt records isn't forgotten after the BQuery performs a fetch. For example, in the following sequence of calls...

   query->AddRecordID(MyRecord);
   query->Fetch();
   query->Fetch(); 

... you don't have to "re-prime" the second fetch by re-adding MyRecord.

Conversely, AddRecordID() doesn't instantly add the record to the BQuery's record list: The records that you add through AddRecordID() aren't put in the record list until you call Fetch(). For example, in this sequence:

   query->AddRecordID(MyRecord);
   query->Fetch();
   query->AddRecordID(YourRecord);

... MyRecord is in query's record list, but YourRecord isn't.

Although this isn't the normal way to add records to the list--normally, you define the BQuery's predicate and then fetch records--it can be useful if you want to "fine-tune" the record list. For example, if you want to monitor a particular record through a live query regardless of whether that record passes the BQuery's predicate, you can add it through this function.

Important: Currently, the AddRecordID() function is slightly flawed: The records that you add through this function must conform to one of the BQuery's tables.


AddTable(), AddTree

      void AddTable(BTable *a_table)
      void AddTree(BTable *a_table)

Adds one or more BTable objects to the BQuery's table list. The first version adds just the BTable identified by the argument. The second adds the argument and all BTables that inherit from it (where "inheritance" is meant as it's defined by the BTable class).

You can add as many BTables as you want; invocations of these functions augment the table list. However, any BTable that you attempt to add must belong to the same BDatabase object.

There's no way to remove BTables from the table list. If you tire of a BTable, you throw the BQuery away and start over.

See also: CountTables() , TableAt()


Clear()

       void Clear(void)

Erases the BQuery's predicate (the table list and record lists are kept intact). Although this function can be convenient in some cases, it usually better to create a new BQuery for each distinct predicate that you want to test.


CountRecordIDs()

      long CountRecordIDs(void)

Returns the number of records in the BQuery's record list. If the object isn't live, the value returned by this function will remain constant between fetches; if it's live, it may change at any time.

See also: RecordIDAt()


CountTables()

      long CountTables(void)

Returns the number of BTables in the BQuery's table list.

See also: TableAt()


Fetch(), FetchOne()

      long Fetch(void)
      long FetchOne(void)

Tests the BQuery's predicate against the records in the designated tables (in the database), and fills the record list with the record ID numbers of the records that pass the test:

Note: Currently, FetchOne() doesn't--it simply invokes Fetch() . Single record fetching will be added in a subsequent release.

The object's record list is cleared before the winning records are added to it.

If the BQuery is live, Fetch() turns on the Storage Server's database monitoring; FetchOne() doesn't.

Fetching is performed in the thread in which the Fetch() function is called; the function doesn't return until all the necessary records have been tested. The on-going monitoring requested by a live query is performed in the Storage Server's thread.

Both functions return B_NO_ERROR if the fetch was successfully executed (even if no records were found that pass the predicate); B_ERROR is returned if the fetch couldn't be performed.

See also: RunOn()


FieldAt()

      char *FieldAt(long index)

Returns a pointer to the index'th field name that you pushed onto the predicate stacked. The pointed-to string belongs to the query--you shouldn't modify or free it. The string itself is a copy of the string that you used to push the field; in other words, the names that are returned by FieldAt() are the same names that you used as arguments in previous PushField() calls. If index is out of bounds, the function returns NULL.

Field names are kept in the order that they were pushed. FieldAt( 0), for example returns the first field name that you pushed on the stack.

This function is provided, mainly, as an aid to interface design. It's not meant as a diagnostic tool.


FromFlat() see ToFlat()


HasRecordID()

      bool HasRecordID(record_id id)

Returns TRUE if the argument is present in the object's record list. Otherwise it returns FALSE .

See also: RecordIDAt(), CountRecordIDs()


IsLive()

      bool IsLive(void)

Returns TRUE if the BQuery is live. You declare a BQuery to be live (or not) when you construct it. You can't change its persuasion thereafter.


MessageReceived()

      virtual void MessageReceived(BMessage *a_message)

Invoked automatically by the update_query() function, as discussed in Live Queries . You never call this function directly, but you can override it in a BQuery-derived class to change its behavior. The messages it can receive (as defined by their what fields) are these:

what Value Meaning
B_RECORD_ADDED A record ID needs to be added to the record list.
B_RECORD_REMOVED A record ID needs to be removed from the list.
B_RECORD_MODIFIED Data has changed in a record in the list.

The default responses to the first two messages do the right thing with regard to the record list: The specified record ID is added to or removed from the BQuery's record list. The default response to the modified message, however, is to do nothing.

The record that has been added, removed, or modified is identified by its record ID in the BMessage's "rec_id" slot:

      record_id rec = a_message->FindLong("rec_id");


PrintToStream()

      void PrintToStream(void)

Prints the BQuery's predicate to standard output in the following format:

   arg count = count
      element_type    element_value
      element_type    element_value
      element_type    element_value
      ...

element_type is one of "longarg", "strarg", "field", or "op". element_value gives the element's value as declared when it was pushed. The order in which the elements are printed is the order in which they were pushed onto the stack.


PushLong(), PushDouble() , PushString(), PushField() , PushOp()

      void PushLong(long value)
      void PushDouble(double value)

      void PushString(const char *string)

      void PushField(const char *field_name)

      void PushOp(query_op operator)

These functions push elements onto the BQuery's predicate stack. The first four push values (or, in the case of PushField() , potential values), that are operated on by the operators that are pushed through PushOp().

The query_op constants are:

Constant Meaning
B_EQ equal
B_NE not equal
B_GT greater than
B_GE greater than or equal to
B_LT less than
B_LE less than or equal to
B_AND logical AND
B_OR logical OR
B_NOT negation
B_ALL wildcard (matches all records)

Predicate construction is explained in The Predicate . Briefly, it's based on the "reverse Polish notation" convention in which the two operands to an operation are pushed first, followed by the operator. The result of an operation can be used as one of the operands in a subsequent operation.

See also: FieldAt()


RecordIDAt()

      record_id RecordIDAt(long index)

Returns the index'th record ID in the object's record list. The record list is empty until the object performs a fetch.

See also: CountRecordIDs()


RunOn()

      bool RunOn(record_id record)

Tests the record identified by the argument against the BQuery's predicate. If the record passes, the function returns TRUE, otherwise it returns FALSE. The record ID isn't added to the record list, even if it passes. You use this function to quickly and platonically test records--it isn't as serious as fetching.

See also: Fetch()


SetDatabase()

      void SetDatabase(Database *db)

Sets the BQuery's database to the argument. You use this function after you've called FromFlat() to tell the BQuery which database it should fetch from when next it fetches. As explained in the FromFlat() description, a flattened query doesn't remember the identity of its database.


TableAt()

      BTable *TableAt(long index)

Returns the index'th BTable in the object's table list.

See also: CountTables()


ToFlat(), FromFlat()

      char *ToFlat(long *size)
      void FromFlat(char *flatQuery)

These functions "flatten" and "unflatten" a BQuery's query. ToFlat() flattens the query: It transforms the BQuery's table and predicate information into a string. The flattened string is returned directly by ToFlat(); the length of the flattened string is returned by reference in the size argument.

FromFlat() sets the object's query as specified by the flatQuery argument. The argument, unsurprisingly, should have been created through a previous call to ToFlat(). Any query information that already resides in the calling object is wiped out.

The one piece of information that isn't translated through a flattened query is the identity of the database upon which the query is based. For flattening and unflattening to work properly, the database of the BQuery that calls FromFlat() must match that of the BQuery that flattened the query. You can use the SetDatabase() function after calling FromFlat() to set the object's database.

You use these functions to store your favorite queries, or to transmit query information between BQuery objects in separate applications.

See also: SetDatabase()




The Be Book, HTML Edition, for Developer Release 8 of the Be Operating System.

Copyright © 1996 Be, Inc. All rights reserved.

Be, the Be logo, BeBox, BeOS, BeWare, and GeekPort are trademarks of Be, Inc.

Last modified September 6, 1996.