Adding C libraries, mostly ported from Evergreen with modification
authorMike Rylander <mrylander@gmail.com>
Tue, 2 Apr 2013 15:27:46 +0000 (11:27 -0400)
committerMike Rylander <mrylander@gmail.com>
Tue, 2 Apr 2013 15:27:46 +0000 (11:27 -0400)
Signed-off-by: Mike Rylander <mrylander@gmail.com>

13 files changed:
src/c/Makefile.am [new file with mode: 0644]
src/c/buildSQL.c [new file with mode: 0644]
src/c/dump_idl.c [new file with mode: 0644]
src/c/idlval.c [new file with mode: 0644]
src/c/oils_cstore.c [new file with mode: 0644]
src/c/oils_event.c [new file with mode: 0644]
src/c/oils_idl-core.c [new file with mode: 0644]
src/c/oils_pcrud.c [new file with mode: 0644]
src/c/oils_sql.c [new file with mode: 0644]
src/c/oils_utils.c [new file with mode: 0644]
src/c/sharestuff_auth.c [new file with mode: 0644]
src/c/test_json_query.c [new file with mode: 0644]
src/c/test_qstore.c [new file with mode: 0644]

diff --git a/src/c/Makefile.am b/src/c/Makefile.am
new file mode 100644 (file)
index 0000000..f9b2637
--- /dev/null
@@ -0,0 +1,45 @@
+#-----------------------------------------------------------
+# Makefile.am for Open-ILS/src/c-apps
+# Author: Kevin Beswick (kevinbeswick00@gmail.com)
+# Process this file with automake to generate Makefile.in
+#-----------------------------------------------------------
+
+AM_CFLAGS = $(DEF_CFLAGS) -DOSRF_LOG_PARAMS -I@top_srcdir@/include/
+AM_LDFLAGS = $(DEF_LDFLAGS) -L$(DBI_LIBS) -lopensrf
+
+bin_PROGRAMS = dump_idl idlval test_json_query
+dump_idl_SOURCES = dump_idl.c
+dump_idl_LDFLAGS = $(AM_LDFLAGS) -loils_idl
+dump_idl_DEPENDENCIES = liboils_idl.la liboils_utils.la
+
+idlval_SOURCES = idlval.c oils_sql.c
+idlval_CFLAGS = $(AM_CFLAGS)
+idlval_LDFLAGS = $(AM_LDFLAGS) -loils_idl -loils_utils
+idlval_DEPENDENCIES = liboils_idl.la liboils_utils.la
+
+test_json_query_SOURCES = test_json_query.c oils_sql.c
+test_json_query_CFLAGS = $(AM_CFLAGS)
+test_json_query_LDFLAGS = $(AM_LDFLAGS) -loils_idl -loils_utils
+test_json_query_DEPENDENCIES = liboils_idl.la liboils_utils.la
+
+lib_LTLIBRARIES = liboils_idl.la liboils_utils.la oils_cstore.la oils_pcrud.la sharestuff_auth.la
+
+liboils_idl_la_SOURCES = oils_idl-core.c
+liboils_idl_la_LDFLAGS = -version-info 2:0:0
+
+liboils_utils_la_SOURCES = oils_utils.c oils_event.c
+liboils_utils_la_LDFLAGS = -loils_idl -version-info 2:0:0
+
+oils_cstore_la_SOURCES = oils_cstore.c oils_sql.c
+oils_cstore_la_LDFLAGS = $(AM_LDFLAGS) -ldbi -ldbdpgsql -loils_utils -module -version-info 1:0:0
+oils_cstore_la_DEPENDENCIES = liboils_utils.la
+
+oils_pcrud_la_SOURCES = oils_pcrud.c oils_sql.c
+oils_pcrud_la_LDFLAGS = $(AM_LDFLAGS) -ldbi -ldbdpgsql -loils_utils -module -version-info 1:0:0
+oils_pcrud_la_DEPENDENCIES = liboils_utils.la
+
+sharestuff_auth_la_SOURCES = sharestuff_auth.c
+sharestuff_auth_la_LDFLAGS = -module -loils_utils -version-info 1:0:0
+sharestuff_auth_la_DEPENDENCIES = liboils_utils.la
+
+
diff --git a/src/c/buildSQL.c b/src/c/buildSQL.c
new file mode 100644 (file)
index 0000000..a2e008d
--- /dev/null
@@ -0,0 +1,1342 @@
+/**
+       @file buildSQL.c
+       @brief Translate an abstract representation of a query into an SQL statement.
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dbi/dbi.h>
+#include "opensrf/utils.h"
+#include "opensrf/string_array.h"
+#include "opensrf/osrf_hash.h"
+#include "opensrf/osrf_application.h"
+#include "openils/oils_idl.h"
+#include "openils/oils_sql.h"
+#include "openils/oils_buildq.h"
+
+static void build_Query( BuildSQLState* state, const StoredQ* query );
+static void buildCombo( BuildSQLState* state, const StoredQ* query, const char* type_str );
+static void buildSelect( BuildSQLState* state, const StoredQ* query );
+static void buildFrom( BuildSQLState* state, const FromRelation* core_from );
+static void buildJoin( BuildSQLState* state, const FromRelation* join );
+static void buildSelectList( BuildSQLState* state, const SelectItem* item );
+static void buildGroupBy( BuildSQLState* state, const SelectItem* sel_list );
+static void buildOrderBy( BuildSQLState* state, const OrderItem* ord_list );
+static void buildCase( BuildSQLState* state, const Expression* expr );
+static void buildExpression( BuildSQLState* state, const Expression* expr );
+
+static void buildFunction( BuildSQLState* state, const Expression* exp );
+static int subexp_count( const Expression* expr );
+static void buildTypicalFunction( BuildSQLState* state, const Expression* expr );
+static void buildExtract( BuildSQLState* state, const Expression* expr );
+
+static void buildSeries( BuildSQLState* state, const Expression* subexp_list, const char* op );
+static void buildBindVar( BuildSQLState* state, const BindVar* bind );
+static void buildScalar( BuildSQLState* state, int numeric, const jsonObject* obj );
+
+static void add_newline( BuildSQLState* state );
+static inline void incr_indent( BuildSQLState* state );
+static inline void decr_indent( BuildSQLState* state );
+
+/**
+       @brief Create a jsonObject representing the current list of bind variables.
+       @param bindvar_list Pointer to the bindvar_list member of a BuildSQLState.
+       @return Pointer to the newly created jsonObject.
+
+       The returned jsonObject is a (possibly empty) JSON_HASH, keyed on the names of the bind
+       variables.  The data for each is another level of JSON_HASH with a fixed set of tags:
+       - "label"
+       - "type"
+       - "description"
+       - "default_value" (as a jsonObject)
+       - "actual_value" (as a jsonObject)
+
+       Any non-existent values are represented as JSON_NULLs.
+
+       The calling code is responsible for freeing the returned jsonOjbect by calling
+       jsonObjectFree().
+*/
+jsonObject* oilsBindVarList( osrfHash* bindvar_list ) {
+       jsonObject* list = jsonNewObjectType( JSON_HASH );
+
+       if( bindvar_list && osrfHashGetCount( bindvar_list )) {
+               // Traverse our internal list of bind variables
+               BindVar* bind = NULL;
+               osrfHashIterator* iter = osrfNewHashIterator( bindvar_list );
+               while(( bind = osrfHashIteratorNext( iter ))) {
+                       // Create an hash to represent the bind variable
+                       jsonObject* bind_obj = jsonNewObjectType( JSON_HASH );
+
+                       // Add an entry for each attribute
+                       jsonObject* attr = jsonNewObject( bind->label );
+                       jsonObjectSetKey( bind_obj, "label", attr );
+
+                       const char* type = NULL;
+                       switch( bind->type ) {
+                               case BIND_STR :
+                                       type = "string";
+                                       break;
+                               case BIND_NUM :
+                                       type = "number";
+                                       break;
+                               case BIND_STR_LIST :
+                                       type = "string_list";
+                                       break;
+                               case BIND_NUM_LIST :
+                                       type = "number_list";
+                                       break;
+                               default :
+                                       type = "(invalid)";
+                                       break;
+                       }
+                       attr = jsonNewObject( type );
+                       jsonObjectSetKey( bind_obj, "type", attr );
+
+                       attr = jsonNewObject( bind->description );
+                       jsonObjectSetKey( bind_obj, "description", attr );
+
+                       if( bind->default_value ) {
+                               attr = jsonObjectClone( bind->default_value );
+                               jsonObjectSetKey( bind_obj, "default_value", attr );
+                       }
+
+                       if( bind->actual_value ) {
+                               attr = jsonObjectClone( bind->actual_value );
+                               jsonObjectSetKey( bind_obj, "actual_value", attr );
+                       }
+
+                       // Add the bind variable to the list
+                       jsonObjectSetKey( list, osrfHashIteratorKey( iter ), bind_obj );
+               }
+               osrfHashIteratorFree( iter );
+       }
+
+       return list;
+}
+
+/**
+       @brief Apply values to bind variables, overriding the defaults, if any.
+       @param state Pointer to the query-building context.
+       @param bindings A JSON_HASH of values.
+       @return 0 if successful, or 1 if not.
+
+       The @a bindings parameter must be a JSON_HASH.  The keys are the names of bind variables.
+       The values are the corresponding values for the variables.
+*/
+int oilsApplyBindValues( BuildSQLState* state, const jsonObject* bindings ) {
+       if( !state ) {
+               osrfLogError( OSRF_LOG_MARK, "NULL pointer to state" );
+               return 1;
+       } else if( !bindings ) {
+               osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                       "Internal error: No pointer to bindings" ));
+               return 1;
+       } else if( bindings->type != JSON_HASH ) {
+               osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                       "Internal error: bindings parameter is not a JSON_HASH" ));
+               return 1;
+       }
+
+       int rc = 0;
+       jsonObject* value = NULL;
+       jsonIterator* iter = jsonNewIterator( bindings );
+       while(( value = jsonIteratorNext( iter ))) {
+               const char* var_name = iter->key;
+               BindVar* bind = osrfHashGet( state->bindvar_list, var_name );
+               if( bind ) {
+                       // Apply or replace the value for the specified variable
+                       if( bind->actual_value )
+                               jsonObjectFree( bind->actual_value );
+                       bind->actual_value = jsonObjectClone( value );
+               } else {
+                       osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                               "Can't assign value to bind variable \"%s\": no such variable", var_name ));
+                       rc = 1;
+               }
+       }
+       jsonIteratorFree( iter );
+
+       return rc;
+}
+
+/**
+       @brief Build an SQL query.
+       @param state Pointer to the query-building context.
+       @param query Pointer to the query to be built.
+       @return Zero if successful, or 1 if not.
+
+       Clear the output buffer, call build_Query() to do the work, and add a closing semicolon.
+*/
+int buildSQL( BuildSQLState* state, const StoredQ* query ) {
+       state->error  = 0;
+       buffer_reset( state->sql );
+       state->indent = 0;
+       build_Query( state, query );
+       if( ! state->error ) {
+               // Remove the trailing space, if there is one, and add a semicolon.
+               char c = buffer_chomp( state->sql );
+               if( c != ' ' )
+                       buffer_add_char( state->sql, c );  // oops, not a space; put it back
+               buffer_add( state->sql, ";\n" );
+       }
+       return state->error;
+}
+
+/**
+       @brief Build an SQL query, appending it to what has been built so far.
+       @param state Pointer to the query-building context.
+       @param query Pointer to the query to be built.
+
+       Look at the query type and branch to the corresponding routine.
+*/
+static void build_Query( BuildSQLState* state, const StoredQ* query ) {
+       if( buffer_length( state->sql ))
+               add_newline( state );
+
+       switch( query->type ) {
+               case QT_SELECT :
+                       buildSelect( state, query );
+                       break;
+               case QT_UNION :
+                       buildCombo( state, query, "UNION" );
+                       break;
+               case QT_INTERSECT :
+                       buildCombo( state, query, "INTERSECT" );
+                       break;
+               case QT_EXCEPT :
+                       buildCombo( state, query, "EXCEPT" );
+                       break;
+               default :
+                       osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                               "Internal error: invalid query type %d in query # %d",
+                               query->type, query->id ));
+                       state->error = 1;
+                       break;
+       }
+}
+
+/**
+       @brief Build a UNION, INTERSECT, or EXCEPT query.
+       @param state Pointer to the query-building context.
+       @param query Pointer to the query to be built.
+       @param type_str The query type, as a string.
+*/
+static void buildCombo( BuildSQLState* state, const StoredQ* query, const char* type_str ) {
+
+       QSeq* seq = query->child_list;
+       if( !seq ) {
+               osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                       "Internal error: No child queries within %s query # %d",
+                       type_str, query->id ));
+               state->error = 1;
+               return;
+       }
+
+       // Traverse the list of child queries
+       while( seq ) {
+               build_Query( state, seq->child_query );
+               if( state->error ) {
+                       sqlAddMsg( state, "Unable to build child query # %d within %s query %d",
+                               seq->child_query->id, type_str, query->id );
+                       return;
+               }
+               seq = seq->next;
+               if( seq ) {
+                       add_newline( state );
+                       buffer_add( state->sql, type_str );
+                       buffer_add_char( state->sql, ' ' );
+                       if( query->use_all )
+                               buffer_add( state->sql, "ALL " );
+               }
+       }
+
+       return;
+}
+
+/**
+       @brief Build a SELECT statement.
+       @param state Pointer to the query-building context.
+       @param query Pointer to the StoredQ structure that represents the query.
+*/
+static void buildSelect( BuildSQLState* state, const StoredQ* query ) {
+
+       FromRelation* from_clause = query->from_clause;
+       if( !from_clause ) {
+               sqlAddMsg( state, "SELECT has no FROM clause in query # %d", query->id );
+               state->error = 1;
+               return;
+       }
+
+       // Get SELECT list
+       buffer_add( state->sql, "SELECT" );
+       incr_indent( state );
+       buildSelectList( state, query->select_list );
+       if( state->error ) {
+               sqlAddMsg( state, "Unable to build SELECT list for query # %d", query->id );
+               state->error = 1;
+               return;
+       }
+       decr_indent( state );
+
+       // Build FROM clause, if there is one
+       if( query->from_clause ) {
+               buildFrom( state, query->from_clause );
+               if( state->error ) {
+                       sqlAddMsg( state, "Unable to build FROM clause for query # %d", query->id );
+                       state->error = 1;
+                       return;
+               }
+       }
+
+       // Build WHERE clause, if there is one
+       if( query->where_clause ) {
+               add_newline( state );
+               buffer_add( state->sql, "WHERE" );
+               incr_indent( state );
+               add_newline( state );
+               buildExpression( state, query->where_clause );
+               if( state->error ) {
+                       sqlAddMsg( state, "Unable to build WHERE clause for query # %d", query->id );
+                       state->error = 1;
+                       return;
+               }
+               decr_indent( state );
+       }
+
+       // Build GROUP BY clause, if there is one
+       buildGroupBy( state, query->select_list );
+
+       // Build HAVING clause, if there is one
+       if( query->having_clause ) {
+               add_newline( state );
+               buffer_add( state->sql, "HAVING" );
+               incr_indent( state );
+               add_newline( state );
+               buildExpression( state, query->having_clause );
+               if( state->error ) {
+                       sqlAddMsg( state, "Unable to build HAVING clause for query # %d", query->id );
+                       state->error = 1;
+                       return;
+               }
+               decr_indent( state );
+       }
+
+       // Build ORDER BY clause, if there is one
+       if( query->order_by_list ) {
+               buildOrderBy( state, query->order_by_list );
+               if( state->error ) {
+                       sqlAddMsg( state, "Unable to build ORDER BY clause for query # %d", query->id );
+                       state->error = 1;
+                       return;
+               }
+       }
+
+       // Build LIMIT clause, if there is one
+       if( query->limit_count ) {
+               add_newline( state );
+               buffer_add( state->sql, "LIMIT " );
+               buildExpression( state, query->limit_count );
+       }
+
+       // Build OFFSET clause, if there is one
+       if( query->offset_count ) {
+               add_newline( state );
+               buffer_add( state->sql, "OFFSET " );
+               buildExpression( state, query->offset_count );
+       }
+
+       state->error = 0;
+}
+
+/**
+       @brief Build a FROM clause.
+       @param Pointer to the query-building context.
+       @param Pointer to the StoredQ query to which the FROM clause belongs.
+*/
+static void buildFrom( BuildSQLState* state, const FromRelation* core_from ) {
+
+       add_newline( state );
+       buffer_add( state->sql, "FROM" );
+       incr_indent( state );
+       add_newline( state );
+
+       switch( core_from->type ) {
+               case FRT_RELATION : {
+                       char* relation = core_from->table_name;
+                       if( !relation ) {
+                               if( !core_from->class_name ) {
+                                       sqlAddMsg( state, "No relation specified for core relation # %d",
+                                               core_from->id );
+                                       state->error = 1;
+                                       return;
+                               }
+
+                               // Look up table name, view name, or source_definition in the IDL
+                               osrfHash* class_hash = osrfHashGet( oilsIDL(), core_from->class_name );
+                               relation = oilsGetRelation( class_hash );
+                       }
+
+                       // Add table or view
+                       buffer_add( state->sql, relation );
+                       if( !core_from->table_name )
+                               free( relation );   // In this case we strdup'd it, must free it
+                       break;
+               }
+               case FRT_SUBQUERY :
+                       buffer_add_char( state->sql, '(' );
+                       incr_indent( state );
+                       build_Query( state, core_from->subquery );
+                       decr_indent( state );
+                       add_newline( state );
+                       buffer_add_char( state->sql, ')' );
+                       break;
+               case FRT_FUNCTION :
+                       buildFunction( state, core_from->function_call );
+                       if ( state->error ) {
+                               sqlAddMsg( state,
+                                       "Unable to include function call # %d in FROM relation # %d",
+                                       core_from->function_call->id, core_from->id );
+                               return;
+                       }
+                       break;
+               default :
+                       osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                               "Internal error: Invalid type # %d in FROM relation # %d",
+                               core_from->type, core_from->id ));
+                       state->error = 1;
+                       return;
+       }
+
+       // Add a table alias, if possible
+       if( core_from->table_alias ) {
+               buffer_add( state->sql, " AS \"" );
+               buffer_add( state->sql, core_from->table_alias );
+               buffer_add( state->sql, "\" " );
+       }
+       else if( core_from->class_name ) {
+               buffer_add( state->sql, " AS \"" );
+               buffer_add( state->sql, core_from->class_name );
+               buffer_add( state->sql, "\" " );
+       } else
+               buffer_add_char( state->sql, ' ' );
+
+       incr_indent( state );
+       FromRelation* join = core_from->join_list;
+       while( join ) {
+               buildJoin( state, join );
+               if( state->error ) {
+                       sqlAddMsg( state, "Unable to build JOIN clause(s) for relation # %d",
+                               core_from->id );
+                       break;
+               } else
+                       join = join->next;
+       }
+       decr_indent( state );
+       decr_indent( state );
+}
+
+/**
+       @brief Add a JOIN clause.
+       @param state Pointer to the query-building context.
+       @param join Pointer to the FromRelation representing the JOIN to be added.
+*/
+static void buildJoin( BuildSQLState* state, const FromRelation* join ) {
+       add_newline( state );
+       switch( join->join_type ) {
+               case JT_NONE :
+                       sqlAddMsg( state, "Non-join relation # %d in JOIN clause", join->id );
+                       state->error = 1;
+                       return;
+               case JT_INNER :
+                       buffer_add( state->sql, "INNER JOIN " );
+                       break;
+               case JT_LEFT:
+                       buffer_add( state->sql, "LEFT JOIN " );
+                       break;
+               case JT_RIGHT:
+                       buffer_add( state->sql, "RIGHT JOIN " );
+                       break;
+               case JT_FULL:
+                       buffer_add( state->sql, "FULL JOIN " );
+                       break;
+               default :
+                       sqlAddMsg( state, "Unrecognized join type in relation # %d", join->id );
+                       state->error = 1;
+                       return;
+       }
+
+       switch( join->type ) {
+               case FRT_RELATION :
+                       // Sanity check
+                       if( !join->table_name || ! *join->table_name ) {
+                               sqlAddMsg( state, "No relation designated for relation # %d", join->id );
+                               state->error = 1;
+                               return;
+                       }
+                       buffer_add( state->sql, join->table_name );
+                       break;
+               case FRT_SUBQUERY :
+                       // Sanity check
+                       if( !join->subquery ) {
+                               sqlAddMsg( state, "Subquery expected, not found for relation # %d", join->id );
+                               state->error = 1;
+                               return;
+                       } else if( !join->table_alias ) {
+                               sqlAddMsg( state, "No table alias for subquery in FROM relation # %d",
+                                       join->id );
+                               state->error = 1;
+                               return;
+                       }
+                       buffer_add_char( state->sql, '(' );
+                       incr_indent( state );
+                       build_Query( state, join->subquery );
+                       decr_indent( state );
+                       add_newline( state );
+                       buffer_add_char( state->sql, ')' );
+                       break;
+               case FRT_FUNCTION :
+                       if( !join->table_name || ! *join->table_name ) {
+                               sqlAddMsg( state, "Joins to functions not yet supported in relation # %d",
+                                       join->id );
+                               state->error = 1;
+                               return;
+                       }
+                       break;
+       }
+
+       const char* effective_alias = join->table_alias;
+       if( !effective_alias )
+               effective_alias = join->class_name;
+
+       if( effective_alias ) {
+               buffer_add( state->sql, " AS \"" );
+               buffer_add( state->sql, effective_alias );
+               buffer_add_char( state->sql, '\"' );
+       }
+
+       if( join->on_clause ) {
+               incr_indent( state );
+               add_newline( state );
+               buffer_add( state->sql, "ON " );
+               buildExpression( state, join->on_clause );
+               decr_indent( state );
+       }
+
+       FromRelation* subjoin = join->join_list;
+       while( subjoin ) {
+               buildJoin( state, subjoin );
+               if( state->error ) {
+                       sqlAddMsg( state, "Unable to build JOIN clause(s) for relation # %d", join->id );
+                       break;
+               } else
+                       subjoin = subjoin->next;
+       }
+}
+
+/**
+       @brief Build a SELECT list.
+       @param state Pointer to the query-building context.
+       @param item Pointer to the first in a linked list of SELECT items.
+*/
+static void buildSelectList( BuildSQLState* state, const SelectItem* item ) {
+
+       int first = 1;
+       while( item ) {
+               if( !first )
+                       buffer_add_char( state->sql, ',' );
+               add_newline( state );
+               buildExpression( state, item->expression );
+               if( state->error ) {
+                       sqlAddMsg( state, "Unable to build an expression for SELECT item # %d", item->id );
+                       state->error = 1;
+                       break;
+               }
+
+               if( item->column_alias ) {
+                       buffer_add( state->sql, " AS \"" );
+                       buffer_add( state->sql, item->column_alias );
+                       buffer_add_char( state->sql, '\"' );
+               }
+               first = 0;
+               item = item->next;
+       };
+       buffer_add_char( state->sql, ' ' );
+}
+
+/**
+       @brief Add a GROUP BY clause, if there is one, to the current query.
+       @param state Pointer to the query-building context.
+       @param sel_list Pointer to the first node in a linked list of SelectItems
+
+       We reference the GROUP BY items by number, not by repeating the expressions.
+*/
+static void buildGroupBy( BuildSQLState* state, const SelectItem* sel_list ) {
+       int seq = 0;       // Sequence number of current SelectItem
+       int first = 1;     // Boolean: true for the first GROUPed BY item
+       while( sel_list ) {
+               ++seq;
+
+               if( sel_list->grouped_by ) {
+                       if( first ) {
+                               add_newline( state );
+                               buffer_add( state->sql, "GROUP BY " );
+                               first = 0;
+                       }
+                       else
+                               buffer_add( state->sql, ", " );
+
+                       buffer_fadd( state->sql, "%d", seq );
+               }
+
+               sel_list = sel_list->next;
+       }
+}
+
+/**
+       @brief Add an ORDER BY clause to the current query.
+       @param state Pointer to the query-building context.
+       @param ord_list Pointer to the first node in a linked list of OrderItems.
+*/
+static void buildOrderBy( BuildSQLState* state, const OrderItem* ord_list ) {
+       add_newline( state );
+       buffer_add( state->sql, "ORDER BY" );
+       incr_indent( state );
+
+       int first = 1;    // boolean
+       while( ord_list ) {
+               if( first )
+                       first = 0;
+               else
+                       buffer_add_char( state->sql, ',' );
+               add_newline( state );
+               buildExpression( state, ord_list->expression );
+               if( state->error ) {
+                       sqlAddMsg( state, "Unable to add ORDER BY expression # %d", ord_list->id );
+                       return;
+               }
+
+               ord_list = ord_list->next;
+       }
+
+       decr_indent( state );
+       return;
+}
+
+/**
+       @brief Build an arbitrary expression.
+       @param state Pointer to the query-building context.
+       @param expr Pointer to the Expression representing the expression to be built.
+*/
+static void buildExpression( BuildSQLState* state, const Expression* expr ) {
+       if( !expr ) {
+               osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                       "Internal error: NULL pointer to Expression" ));
+               state->error = 1;
+               return;
+       }
+
+       if( expr->parenthesize )
+               buffer_add_char( state->sql, '(' );
+
+       switch( expr->type ) {
+               case EXP_BETWEEN :
+                       if( expr->negate )
+                               buffer_add( state->sql, "NOT " );
+
+                       buildExpression( state, expr->left_operand );
+                       if( state->error ) {
+                               sqlAddMsg( state, "Unable to emit left operand in BETWEEN expression # %d",
+                                       expr->id );
+                               break;
+                       }
+
+                       buffer_add( state->sql, " BETWEEN " );
+
+                       buildExpression( state, expr->subexp_list );
+                       if( state->error ) {
+                               sqlAddMsg( state, "Unable to emit lower limit in BETWEEN expression # %d",
+                                       expr->id );
+                               break;
+                       }
+
+                       buffer_add( state->sql, " AND " );
+
+                       buildExpression( state, expr->subexp_list->next );
+                       if( state->error ) {
+                               sqlAddMsg( state, "Unable to emit upper limit in BETWEEN expression # %d",
+                                       expr->id );
+                               break;
+                       }
+
+                       break;
+               case EXP_BIND :
+                       if( !expr->bind ) {     // Sanity check
+                               osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                                       "Internal error: no variable for bind variable expression" ));
+                               state->error = 1;
+                       } else
+                               buildBindVar( state, expr->bind );
+                       break;
+               case EXP_BOOL :
+                       if( expr->negate )
+                               buffer_add( state->sql, "NOT " );
+
+                       if( expr->literal ) {
+                               buffer_add( state->sql, expr->literal );
+                               buffer_add_char( state->sql, ' ' );
+                       } else
+                               buffer_add( state->sql, "FALSE " );
+                       break;
+               case EXP_CASE :
+                       buildCase( state, expr );
+                       if( state->error )
+                               sqlAddMsg( state, "Unable to build CASE expression # %d", expr->id );
+
+                       break;
+               case EXP_CAST :                   // Type cast
+                       if( expr->negate )
+                               buffer_add( state->sql, "NOT " );
+
+                       buffer_add( state->sql, "CAST (" );
+                       buildExpression( state, expr->left_operand );
+                       if( state->error )
+                               sqlAddMsg( state, "Unable to build left operand for CAST expression # %d",
+                                       expr->id );
+                       else {
+                               buffer_add( state->sql, " AS " );
+                               if( expr->cast_type && expr->cast_type->datatype_name ) {
+                                       buffer_add( state->sql, expr->cast_type->datatype_name );
+                                       buffer_add_char( state->sql, ')' );
+                               } else {
+                                       osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                                               "No datatype available for CAST expression # %d", expr->id ));
+                                       state->error = 1;
+                               }
+                       }
+                       break;
+               case EXP_COLUMN :                 // Table column
+                       if( expr->negate )
+                               buffer_add( state->sql, "NOT " );
+
+                       if( expr->table_alias ) {
+                               buffer_add_char( state->sql, '\"' );
+                               buffer_add( state->sql, expr->table_alias );
+                               buffer_add( state->sql, "\"." );
+                       }
+                       if( expr->column_name ) {
+                               buffer_add( state->sql, expr->column_name );
+                       } else {
+                               buffer_add_char( state->sql, '*' );
+                       }
+                       break;
+               case EXP_EXIST :
+                       if( !expr->subquery ) {
+                               osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                                       "No subquery found for EXIST expression # %d", expr->id ));
+                               state->error = 1;
+                       } else {
+                               if( expr->negate )
+                                       buffer_add( state->sql, "NOT " );
+
+                               buffer_add( state->sql, "EXISTS (" );
+                               incr_indent( state );
+                               build_Query( state, expr->subquery );
+                               decr_indent( state );
+                               add_newline( state );
+                               buffer_add_char( state->sql, ')' );
+                       }
+                       break;
+               case EXP_FUNCTION :
+                       buildFunction( state, expr );
+                       break;
+               case EXP_IN :
+                       if( expr->left_operand ) {
+                               buildExpression( state, expr->left_operand );
+                               if( !state->error ) {
+                                       if( expr->negate )
+                                               buffer_add( state->sql, "NOT " );
+                                       buffer_add( state->sql, " IN (" );
+
+                                       if( expr->subquery ) {
+                                               incr_indent( state );
+                                               build_Query( state, expr->subquery );
+                                               if( state->error )
+                                                       sqlAddMsg( state, "Unable to build subquery for IN condition" );
+                                               else {
+                                                       decr_indent( state );
+                                                       add_newline( state );
+                                                       buffer_add_char( state->sql, ')' );
+                                               }
+                                       } else {
+                                               buildSeries( state, expr->subexp_list, NULL );
+                                               if( state->error )
+                                                       sqlAddMsg( state, "Unable to build IN list" );
+                                               else
+                                                       buffer_add_char( state->sql, ')' );
+                                       }
+                               }
+                       }
+                       break;
+               case EXP_ISNULL :
+                       if( expr->left_operand ) {
+                               buildExpression( state, expr->left_operand );
+                               if( state->error ) {
+                                       sqlAddMsg( state, "Unable to emit left operand in IS NULL expression # %d",
+                                               expr->id );
+                                       break;
+                               }
+                       }
+
+                       if( expr->negate )
+                               buffer_add( state->sql, " IS NOT NULL" );
+                       else
+                               buffer_add( state->sql, " IS NULL" );
+                       break;
+               case EXP_NULL :
+                       if( expr->negate )
+                               buffer_add( state->sql, "NOT " );
+
+                       buffer_add( state->sql, "NULL" );
+                       break;
+               case EXP_NUMBER :                    // Numeric literal
+                       if( !expr->literal ) {
+                               osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state,
+                                       "Internal error: No numeric value in string expression # %d", expr->id ));
+                               state->error = 1;
+                       } else {
+                               buffer_add( state->sql, expr->literal );
+                       }
+                       break;
+               case EXP_OPERATOR :
+                       if( expr->negate )
+                               buffer_add( state->sql, "NOT (" );
+
+                       if( expr->left_operand ) {
+                               buildExpression( state, expr->left_operand );
+                               if( state->error ) {
+                                       sqlAddMsg( state, "Unable to emit left operand in expression # %d",
+                                               expr->id );
+                                       break;
+                               }
+                       }
+                       buffer_add_char( state->sql, ' ' );
+                       buffer_add( state->sql, expr->op );
+                       buffer_add_char( state->sql, ' ' );
+                       if( expr->right_operand ) {
+                               buildExpression( state, expr->right_operand );
+                               if( state->error ) {
+                                       sqlAddMsg( state, "Unable to emit right operand in expression # %d",
+                                                          expr->id );
+                                       break;
+                               }
+                       }
+
+                       if( expr->negate )
+                               buffer_add_char( state->sql, ')' );
+
+                       break;
+               case EXP_SERIES :
+                       if( expr->negate )
+                               buffer_add( state->sql, "NOT (" );
+
+                       buildSeries( state, expr->subexp_list, expr->op );
+                       if( state->error ) {
+                               sqlAddMsg( state, "Unable to build series expression using operator \"%s\"",
+                                       expr->op ? expr->op : "," );
+                       }
+                       if( expr->negate )
+                               buffer_add_char( state->sql, ')' );
+
+                       break;
+               case EXP_STRING :                     // String literal
+                       if( !expr->literal ) {
+                               osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state,
+                                       "Internal error: No string value in string expression # %d", expr->id ));
+                                       state->error = 1;
+                       } else {
+                               char* str = strdup( expr->literal );
+                               dbi_conn_quote_string( state->dbhandle, &str );
+                               if( str ) {
+                                       buffer_add( state->sql, str );
+                                       free( str );
+                               } else {
+                                       osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state,
+                                               "Unable to format string literal \"%s\" for expression # %d",
+                                                       expr->literal, expr->id ));
+                                       state->error = 1;
+                               }
+                       }
+                       break;
+               case EXP_SUBQUERY :
+                       if( expr->negate )
+                               buffer_add( state->sql, "NOT " );
+
+                       if( expr->subquery ) {
+                               buffer_add_char( state->sql, '(' );
+                               incr_indent( state );
+                               build_Query( state, expr->subquery );
+                               decr_indent( state );
+                               add_newline( state );
+                               buffer_add_char( state->sql, ')' );
+                       } else {
+                               osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state,
+                                       "Internal error: No subquery in subquery expression # %d", expr->id ));
+                               state->error = 1;
+                       }
+                       break;
+       }
+
+       if( expr->parenthesize )
+               buffer_add_char( state->sql, ')' );
+}
+
+/**
+       @brief Build a CASE expression.
+       @param state Pointer to the query-building context.
+       @param exp Pointer to an Expression representing a CASE expression.
+*/
+static void buildCase( BuildSQLState* state, const Expression* expr ) {
+       // Sanity checks
+       if( ! expr->left_operand ) {
+               sqlAddMsg( state, "CASE expression # %d has no left operand", expr->id );
+               state->error  = 1;
+               return;
+       } else if( ! expr->branch_list ) {
+               sqlAddMsg( state, "CASE expression # %d has no branches", expr->id );
+               state->error  = 1;
+               return;
+       }
+
+       if( expr->negate )
+               buffer_add( state->sql, "NOT (" );
+
+       // left_operand is the expression on which we shall branch
+       buffer_add( state->sql, "CASE " );
+       buildExpression( state, expr->left_operand );
+       if( state->error ) {
+               sqlAddMsg( state, "Unable to build operand of CASE expression # %d", expr->id );
+               return;
+       }
+
+       incr_indent( state );
+
+       // Emit each branch in turn
+       CaseBranch* branch = expr->branch_list;
+       while( branch ) {
+               add_newline( state );
+
+               if( branch->condition ) {
+                       // Emit a WHEN condition
+                       buffer_add( state->sql, "WHEN " );
+                       buildExpression( state, branch->condition );
+                       incr_indent( state );
+                       add_newline( state );
+                       buffer_add( state->sql, "THEN " );
+               } else {
+                       // Emit ELSE
+                       buffer_add( state->sql, "ELSE " );
+                       incr_indent( state );
+                       add_newline( state );
+               }
+
+               // Emit the THEN expression
+               buildExpression( state, branch->result );
+               decr_indent( state );
+
+               branch = branch->next;
+       }
+
+       decr_indent( state );
+       add_newline( state );
+       buffer_add( state->sql, "END" );
+
+       if( expr->negate )
+               buffer_add( state->sql, ")" );
+}
+
+/**
+       @brief Build a function call, with a subfield if specified.
+       @param state Pointer to the query-building context.
+       @param exp Pointer to an Expression representing a function call.
+*/
+static void buildFunction( BuildSQLState* state, const Expression* expr ) {
+       if( expr->negate )
+               buffer_add( state->sql, "NOT " );
+
+       // If a subfield is specified, the function call
+       // needs an extra layer of parentheses
+       if( expr->column_name )
+               buffer_add_char( state->sql, '(' );
+
+       // First, check for some specific functions with peculiar syntax, and treat them
+       // as special exceptions.  We rely on the input side to ensure that the function
+       // name is available.
+       if( !strcasecmp( expr->function_name, "EXTRACT" ))
+               buildExtract( state, expr );
+       else if( !strcasecmp( expr->function_name, "CURRENT_DATE" ) && ! expr->subexp_list )
+               buffer_add( state->sql, "CURRENT_DATE " );
+       else if( !strcasecmp( expr->function_name, "CURRENT_TIME" ) && ! expr->subexp_list )
+               buffer_add( state->sql, "CURRENT_TIME " );
+       else if( !strcasecmp( expr->function_name, "CURRENT_TIMESTAMP" ) && ! expr->subexp_list )
+               buffer_add( state->sql, "CURRENT_TIMESTAMP " );
+       else if( !strcasecmp( expr->function_name, "LOCALTIME" ) && ! expr->subexp_list )
+               buffer_add( state->sql, "LOCALTIME " );
+       else if( !strcasecmp( expr->function_name, "LOCALTIMESTAMP" ) && ! expr->subexp_list )
+               buffer_add( state->sql, "LOCALTIMESTAMP " );
+       else if( !strcasecmp( expr->function_name, "TRIM" )) {
+               int arg_count = subexp_count( expr );
+
+               if( (arg_count != 2 && arg_count != 3 ) || expr->subexp_list->type != EXP_STRING )
+                       buildTypicalFunction( state, expr );
+               else {
+                       sqlAddMsg( state,
+                               "TRIM function not supported in expr # %d; use ltrim() and/or rtrim()",
+                               expr->id );
+                       state->error = 1;
+                       return;
+               }
+       } else
+               buildTypicalFunction( state, expr );     // Not a special exception.
+
+       if( expr->column_name ) {
+               // Add the name of the subfield
+               buffer_add( state->sql, ").\"" );
+               buffer_add( state->sql, expr->column_name );
+               buffer_add_char( state->sql, '\"' );
+       }
+}
+
+/**
+       @brief Count the number of subexpressions attached to a given Expression.
+       @param expr Pointer to the Expression whose subexpressions are to be counted.
+       @return The number of subexpressions.
+*/
+static int subexp_count( const Expression* expr ) {
+       if( !expr )
+               return 0;
+
+       int count = 0;
+       const Expression* sub = expr->subexp_list;
+       while( sub ) {
+               ++count;
+               sub = sub->next;
+       }
+       return count;
+}
+
+/**
+       @brief Build an ordinary function call, i.e. one with no special syntax,
+       @param state Pointer to the query-building context.
+       @param exp Pointer to an Expression representing a function call.
+
+       Emit the parameters as a comma-separated list of expressions.
+*/
+static void buildTypicalFunction( BuildSQLState* state, const Expression* expr ) {
+       buffer_add( state->sql, expr->function_name );
+       buffer_add_char( state->sql, '(' );
+
+       // Add the parameters, if any
+       buildSeries( state, expr->subexp_list, NULL );
+
+       buffer_add_char( state->sql, ')' );
+}
+
+/**
+       @brief Build a call to the EXTRACT function, with its peculiar syntax.
+       @param state Pointer to the query-building context.
+       @param exp Pointer to an Expression representing an EXTRACT call.
+
+       If there are not exactly two parameters, or if the first parameter is not a string,
+       then assume it is an ordinary function overloading on the same name.  We don't try to
+       check the type of the second parameter.  Hence it is possible for a legitimately
+       overloaded function to be uncallable.
+
+       The first parameter of EXTRACT() must be one of a short list of names for some fragment
+       of a date or time.  Here we accept that parameter in the form of a string.  We don't
+       surround it with quotes in the output, although PostgreSQL wouldn't mind if we did.
+*/
+static void buildExtract( BuildSQLState* state, const Expression* expr ) {
+
+       const Expression* arg = expr->subexp_list;
+
+       // See if this is the special form of EXTRACT(), so far as we can tell
+       if( subexp_count( expr ) != 2 || arg->type != EXP_STRING ) {
+               buildTypicalFunction( state, expr );
+               return;
+       } else {
+               // check the first argument against a list of valid values
+               if(    strcasecmp( arg->literal, "century" )
+                       && strcasecmp( arg->literal, "day" )
+                       && strcasecmp( arg->literal, "decade" )
+                       && strcasecmp( arg->literal, "dow" )
+                       && strcasecmp( arg->literal, "doy" )
+                       && strcasecmp( arg->literal, "epoch" )
+                       && strcasecmp( arg->literal, "hour" )
+                       && strcasecmp( arg->literal, "isodow" )
+                       && strcasecmp( arg->literal, "isoyear" )
+                       && strcasecmp( arg->literal, "microseconds" )
+                       && strcasecmp( arg->literal, "millennium" )
+                       && strcasecmp( arg->literal, "milliseconds" )
+                       && strcasecmp( arg->literal, "minute" )
+                       && strcasecmp( arg->literal, "month" )
+                       && strcasecmp( arg->literal, "quarter" )
+                       && strcasecmp( arg->literal, "second" )
+                       && strcasecmp( arg->literal, "timezone" )
+                       && strcasecmp( arg->literal, "timezone_hour" )
+                       && strcasecmp( arg->literal, "timezone_minute" )
+                       && strcasecmp( arg->literal, "week" )
+                       && strcasecmp( arg->literal, "year" )) {
+                       // This *could* be an ordinary function, overloading on the name.  However it's
+                       // more likely that the user misspelled one of the names expected by EXTRACT().
+                       sqlAddMsg( state,
+                               "Invalid name \"%s\" as EXTRACT argument in expression # %d",
+                               expr->literal, expr->id );
+                       state->error = 1;
+               }
+       }
+
+       buffer_add( state->sql, "EXTRACT(" );
+       buffer_add( state->sql, arg->literal );
+       buffer_add( state->sql, " FROM " );
+
+       arg = arg->next;
+       if( !arg ) {
+               sqlAddMsg( state,
+                       "Only one argument supplied to EXTRACT function in expression # %d", expr->id );
+               state->error = 1;
+               return;
+       }
+
+       // The second parameter must be of type timestamp, time, or interval.  We don't have
+       // a good way of checking it here, so we rely on PostgreSQL to complain if necessary.
+       buildExpression( state, arg );
+       buffer_add_char( state->sql, ')' );
+}
+
+/**
+       @brief Build a series of expressions separated by a specified operator, or by commas.
+       @param state Pointer to the query-building context.
+       @param subexp_list Pointer to the first Expression in a linked list.
+       @param op Pointer to the operator, or NULL for commas.
+
+       If the operator is AND or OR (in upper, lower, or mixed case), the second and all
+       subsequent operators will begin on a new line.
+*/
+static void buildSeries( BuildSQLState* state, const Expression* subexp_list, const char* op ) {
+
+       if( !subexp_list)
+               return;                // List is empty
+
+       int comma = 0;             // Boolean; true if separator is a comma
+       int newline_needed = 0;    // Boolean; true if operator is AND or OR
+
+       if( !op ) {
+               op = ",";
+               comma = 1;
+       } else if( !strcmp( op, "," ))
+               comma = 1;
+       else if( !strcasecmp( op, "AND" ) || !strcasecmp( op, "OR" ))
+               newline_needed = 1;
+
+       int first = 1;               // Boolean; true for first item in list
+       while( subexp_list ) {
+               if( first )
+                       first = 0;   // No separator needed yet
+               else {
+                       // Insert a separator
+                       if( comma )
+                               buffer_add( state->sql, ", " );
+                       else {
+                               if( newline_needed )
+                                       add_newline( state );
+                               else
+                                       buffer_add_char( state->sql, ' ' );
+
+                               buffer_add( state->sql, op );
+                               buffer_add_char( state->sql, ' ' );
+                       }
+               }
+
+               buildExpression( state, subexp_list );
+               subexp_list = subexp_list->next;
+       }
+}
+
+/**
+       @brief Add the value of a bind variable to an SQL statement.
+       @param state Pointer to the query-building context.
+       @param bind Pointer to the bind variable whose value is to be added to the SQL.
+
+       The value may be a null, a scalar, or an array of nulls and/or scalars, depending on
+       the type of the bind variable.
+*/
+static void buildBindVar( BuildSQLState* state, const BindVar* bind ) {
+
+       // Decide where to get the value, if any
+       const jsonObject* value = NULL;
+       if( bind->actual_value )
+               value = bind->actual_value;
+       else if( bind->default_value ) {
+               if( state->defaults_usable )
+                       value = bind->default_value;
+               else {
+                       sqlAddMsg( state, "No confirmed value available for bind variable \"%s\"",
+                               bind->name );
+                       state->error = 1;
+                       return;
+               }
+       } else if( state->values_required ) {
+               sqlAddMsg( state, "No value available for bind variable \"%s\"", bind->name );
+               state->error = 1;
+               return;
+       } else {
+               // No value available, and that's okay.  Emit the name of the bind variable.
+               buffer_add_char( state->sql, ':' );
+               buffer_add( state->sql, bind->name );
+               return;
+       }
+
+       // If we get to this point, we know that a value is available.  Carry on.
+
+       int numeric = 0;       // Boolean
+       if( BIND_NUM == bind->type || BIND_NUM_LIST == bind->type )
+               numeric = 1;
+
+       // Emit the value
+       switch( bind->type ) {
+               case BIND_STR :
+               case BIND_NUM :
+                       buildScalar( state, numeric, value );
+                       break;
+               case BIND_STR_LIST :
+               case BIND_NUM_LIST :
+                       if( JSON_ARRAY == value->type ) {
+                               // Iterate over array, emit each value
+                               int first = 1;   // Boolean
+                               unsigned long max = value->size;
+                               unsigned long i = 0;
+                               while( i < max ) {
+                                       if( first )
+                                               first = 0;
+                                       else
+                                               buffer_add( state->sql, ", " );
+
+                                       buildScalar( state, numeric, jsonObjectGetIndex( value, i ));
+                                       ++i;
+                               }
+                       } else {
+                               osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                                       "Invalid value for bind variable; expected a list of values" ));
+                               state->error = 1;
+                       }
+                       break;
+               default :
+                       osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                               "Internal error: invalid type for bind variable" ));
+                       state->error = 1;
+                       break;
+       }
+
+       if( state->error )
+               osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state,
+                       "Unable to emit value of bind variable \"%s\"", bind->name ));
+}
+
+/**
+       @brief Add a number or quoted string to an SQL statement.
+       @param state Pointer to the query-building context.
+       @param numeric Boolean; true if the value is expected to be a number
+       @param obj Pointer to the jsonObject whose value is to be added to the SQL.
+*/
+static void buildScalar( BuildSQLState* state, int numeric, const jsonObject* obj ) {
+       switch( obj->type ) {
+               case JSON_HASH :
+                       osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                               "Internal error: hash value for bind variable" ));
+                       state->error = 1;
+                       break;
+               case JSON_ARRAY :
+                       osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                               "Internal error: array value for bind variable" ));
+                       state->error = 1;
+                       break;
+               case JSON_STRING :
+                       if( numeric ) {
+                               sqlAddMsg( state,
+                                       "Invalid value for bind variable: expected a string, found a number" );
+                               state->error = 1;
+                       } else {
+                               char* str = jsonObjectToSimpleString( obj );
+                               dbi_conn_quote_string( state->dbhandle, &str );
+                               if( str ) {
+                                       buffer_add( state->sql, str );
+                                       free( str );
+                               } else {
+                                       osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state,
+                                               "Unable to format string literal \"%s\" for bind variable",
+                                               jsonObjectGetString( obj )));
+                                       state->error = 1;
+                               }
+                       }
+                       break;
+               case JSON_NUMBER :
+                       if( numeric ) {
+                               buffer_add( state->sql, jsonObjectGetString( obj ));
+                       } else {
+                               sqlAddMsg( state,
+                                       "Invalid value for bind variable: expected a number, found a string" );
+                               state->error = 1;
+                       }
+                       break;
+               case JSON_NULL :
+                       buffer_add( state->sql, "NULL" );
+                       break;
+               case JSON_BOOL :
+                       osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                               "Internal error: boolean value for bind variable" ));
+                       state->error = 1;
+                       break;
+               default :
+                       osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+                               "Internal error: corrupted value for bind variable" ));
+                       state->error = 1;
+                       break;
+       }
+}
+
+/**
+       @brief Start a new line in the output, with the current level of indentation.
+       @param state Pointer to the query-building context.
+*/
+static void add_newline( BuildSQLState* state ) {
+       buffer_add_char( state->sql, '\n' );
+
+       // Add indentation
+       static const char blanks[] = "                                ";   // 32 blanks
+       static const size_t maxlen = sizeof( blanks ) - 1;
+       const int blanks_per_level = 3;
+       int n = state->indent * blanks_per_level;
+       while( n > 0 ) {
+               size_t len = n >= maxlen ? maxlen : n;
+               buffer_add_n( state->sql, blanks, len );
+               n -= len;
+       }
+}
+
+/**
+       @brief Increase the degree of indentation.
+       @param state Pointer to the query-building context.
+*/
+static inline void incr_indent( BuildSQLState* state ) {
+       ++state->indent;
+}
+
+/**
+       @brief Reduce the degree of indentation.
+       @param state Pointer to the query-building context.
+*/
+static inline void decr_indent( BuildSQLState* state ) {
+       if( state->indent )
+               --state->indent;
+}
diff --git a/src/c/dump_idl.c b/src/c/dump_idl.c
new file mode 100644 (file)
index 0000000..efeee45
--- /dev/null
@@ -0,0 +1,414 @@
+/*
+*  C Implementation: dump_idl
+*
+* Description: 
+*
+*
+* Author: Scott McKellar <scott@esilibrary.com>, (C) 2009
+*
+* Copyright: See COPYING file that comes with this distribution
+*
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <opensrf/string_array.h>
+#include <opensrf/osrf_hash.h>
+#include <openils/oils_idl.h>
+
+static void dump_idl( osrfHash* IDLHash );
+static void dump_class( osrfHash* class_hash, const char* class_name );
+static void dump_fields( osrfHash* field_hash );
+static void dump_one_field( osrfHash* field_hash, const char* field_name );
+static void dump_links( osrfHash* links_hash );
+static void dump_one_link( osrfHash* link_hash, const char* link_name );
+static void dump_permacrud( osrfHash* pcrud_hash );
+static void dump_action( osrfHash* action_hash, const char* action_name );
+static void dump_foreign_context( osrfHash* fc_hash );
+static void dump_fc_class( osrfHash* fc_class_hash, const char* class_name );
+static void dump_string_array( 
+       osrfStringArray* sarr, const char* name, const char* indent );
+
+int main( int argc, char* argv[] ) {
+       int rc = 0;
+       
+       // Suppress informational messages
+       osrfLogSetLevel( OSRF_LOG_WARNING );
+
+       // Get name of IDL file, if specified on command line
+       const char* IDL_filename = NULL;
+       int filename_expected = 0;              // boolean
+       int i;
+       for( i = 1; i < argc; ++i ) {
+               const char* arg = argv[ i ];
+               printf( "%s\n", arg );
+               if( filename_expected ) {
+                       IDL_filename = arg;
+                       filename_expected = 0;
+               } else {
+                       if( '-' == arg[ 0 ] && 'f' == arg[1] ) {
+                               if( IDL_filename ) {
+                                       fprintf( stderr, "Only one IDL file may be specified\n" );
+                                       return 1;
+                               } else {
+                                       if( arg[ 2 ] )
+                                               IDL_filename = arg + 2;
+                                       else
+                                               filename_expected = 1;
+                               }
+                       }
+                       else
+                               break;
+               }
+       }
+       
+       if( filename_expected ) {
+               fprintf( stderr, "IDL filename expected on command line, not found\n" );
+               return 1;
+       }
+
+       // No filename?  Look in the environment
+       if( !IDL_filename )
+               IDL_filename = getenv( "OILS_IDL_FILENAME" );
+
+       // Still no filename?  Apply a default
+       if( !IDL_filename )
+               IDL_filename = "/openils/conf/fm_IDL.xml";
+       
+       printf( "IDL filename: %s\n", IDL_filename );
+       
+       osrfHash* IDL = oilsIDLInit( IDL_filename );
+       if( NULL == IDL ) {
+               fputs( "Failed to build IDL\n", stderr );
+               rc = 1;
+       }
+
+       if( i >= argc )
+               // No classes specified?  Dump them all
+               dump_idl( IDL );
+       else do {
+               // Dump the requested classes
+               dump_class( osrfHashGet( IDL, argv[ i ] ), argv[ i ] );
+               ++i;
+       } while( i < argc );
+               
+       return rc;
+}
+
+static void dump_idl( osrfHash* IDLHash ) {
+       if( NULL == IDLHash )
+               return;
+
+       if( 0 == osrfHashGetCount( IDLHash ) )
+               return;
+
+       osrfHashIterator* iter = osrfNewHashIterator( IDLHash );
+       osrfHash* class_hash = NULL;
+       
+       // Dump each class
+       for( ;; ) {
+               class_hash = osrfHashIteratorNext( iter );
+               if( class_hash )
+                       dump_class( class_hash, osrfHashIteratorKey( iter ) );
+               else
+                       break;
+       }
+
+       osrfHashIteratorFree( iter );
+}
+
+static void dump_class( osrfHash* class_hash, const char* class_name )
+{
+       if( !class_hash || !class_name )
+               return;
+       
+       if( 0 == osrfHashGetCount( class_hash ) )
+               return;
+
+       printf( "Class %s\n", class_name );
+       const char* indent = "    ";
+       
+       osrfHashIterator* iter = osrfNewHashIterator( class_hash );
+       
+       // Dump each attribute, etc. of the class hash
+       for( ;; ) {
+               void* class_attr = osrfHashIteratorNext( iter );
+               if( class_attr ) {
+                       const char* attr_name = osrfHashIteratorKey( iter );
+                       if( !strcmp( attr_name, "classname" ) )
+                               printf( "%s%s: %s\n", indent, attr_name, (char*) class_attr );
+                       else if( !strcmp( attr_name, "fieldmapper" ) )
+                               printf( "%s%s: %s\n", indent, attr_name, (char*) class_attr );
+                       else if( !strcmp( attr_name, "tablename" ) )
+                               printf( "%s%s: %s\n", indent, attr_name, (char*) class_attr );
+                       else if( !strcmp( attr_name, "restrict_primary" ) )
+                               printf( "%s%s: %s\n", indent, attr_name, (char*) class_attr );
+                       else if( !strcmp( attr_name, "virtual" ) )
+                               printf( "%s%s: %s\n", indent, attr_name, (char*) class_attr );
+                       else if( !strcmp( attr_name, "controller" ) )
+                               dump_string_array( (osrfStringArray*) class_attr, attr_name, indent );
+                       else if( !strcmp( attr_name, "fields" ) )
+                               dump_fields( (osrfHash*) class_attr );
+                       else if( !strcmp( attr_name, "links" ) )
+                               dump_links( (osrfHash*) class_attr );
+                       else if( !strcmp( attr_name, "primarykey" ) )
+                               printf( "%s%s: %s\n", indent, attr_name, (char*) class_attr );
+                       else if( !strcmp( attr_name, "sequence" ) )
+                               printf( "%s%s: %s\n", indent, attr_name, (char*) class_attr );
+                       else if( !strcmp( attr_name, "permacrud" ) )
+                               dump_permacrud( (osrfHash*) class_attr );
+                       else if( !strcmp( attr_name, "source_definition" ) )
+                               printf( "%s%s:\n%s\n", indent, attr_name, (char*) class_attr );
+                       else
+                               printf( "%s%s (unknown)\n", indent, attr_name );
+               } else
+                       break;
+       }
+}
+
+static void dump_fields( osrfHash* fields_hash ) {
+       if( NULL == fields_hash )
+               return;
+
+       if( 0 == osrfHashGetCount( fields_hash ) )
+               return;
+
+       fputs( "    fields\n", stdout );
+       
+       osrfHashIterator* iter = osrfNewHashIterator( fields_hash );
+       osrfHash* fields_attr = NULL;
+       
+       // Dump each field
+       for( ;; ) {
+               fields_attr = osrfHashIteratorNext( iter );
+               if( fields_attr )
+                       dump_one_field( fields_attr, osrfHashIteratorKey( iter ) );
+               else
+                       break;
+       }
+
+       osrfHashIteratorFree( iter );
+}
+
+static void dump_one_field( osrfHash* field_hash, const char* field_name ) {
+       if( !field_hash || !field_name )
+               return;
+       
+       if( 0 == osrfHashGetCount( field_hash ) )
+               return;
+
+       printf( "        %s\n", field_name  );
+       
+       osrfHashIterator* iter = osrfNewHashIterator( field_hash );
+       const char* field_attr = NULL;
+       const char* indent = "            ";
+       
+       // Dump each field attribute
+       for( ;; ) {
+               field_attr = osrfHashIteratorNext( iter );
+               if( field_attr )
+                       printf( "%s%s: %s\n", indent, osrfHashIteratorKey( iter ), field_attr );
+               else
+                       break;
+       }
+
+       osrfHashIteratorFree( iter );
+}
+
+static void dump_links( osrfHash* links_hash ) {
+       if( NULL == links_hash )
+               return;
+
+       if( 0 == osrfHashGetCount( links_hash ) )
+               return;
+
+       fputs( "    links\n", stdout );
+       
+       osrfHashIterator* iter = osrfNewHashIterator( links_hash );
+       osrfHash* links_attr = NULL;
+       
+       // Dump each link
+       for( ;; ) {
+               links_attr = osrfHashIteratorNext( iter );
+               if( links_attr )
+                       dump_one_link( links_attr, osrfHashIteratorKey( iter ) );
+               else
+                       break;
+       }
+
+       osrfHashIteratorFree( iter );
+}
+
+static void dump_one_link( osrfHash* link_hash, const char* link_name ) {
+       if( !link_hash || !link_name )
+               return;
+       
+       if( 0 == osrfHashGetCount( link_hash ) )
+               return;
+
+       printf( "        %s\n", link_name  );
+       
+       osrfHashIterator* iter = osrfNewHashIterator( link_hash );
+       const void* link_attr = NULL;
+       const char* indent = "            ";
+       
+       // Dump each link attribute
+       for( ;; ) {
+               link_attr = osrfHashIteratorNext( iter );
+               if( link_attr ) {
+                       const char* link_attr_name = osrfHashIteratorKey( iter );
+                       if( !strcmp( link_attr_name, "reltype" ) )
+                               printf( "%s%s: %s\n", indent, link_attr_name, (char*) link_attr );
+                       else if( !strcmp( link_attr_name, "key" ) )
+                               printf( "%s%s: %s\n", indent, link_attr_name, (char*) link_attr );
+                       else if( !strcmp( link_attr_name, "class" ) )
+                               printf( "%s%s: %s\n", indent, link_attr_name, (char*) link_attr );
+                       else if( !strcmp( link_attr_name, "map" ) ) 
+                               dump_string_array( (osrfStringArray*) link_attr, link_attr_name, indent );
+                       else if( !strcmp( link_attr_name, "field" ) )
+                               printf( "%s%s: %s\n", indent, link_attr_name, (char*) link_attr );
+                       else
+                               printf( "%s%s (unknown)\n", indent, link_attr_name );
+               } else
+                       break;
+       }
+
+       osrfHashIteratorFree( iter );
+}
+
+static void dump_permacrud( osrfHash* pcrud_hash ) {
+       if( NULL == pcrud_hash )
+               return;
+
+       if( 0 == osrfHashGetCount( pcrud_hash ) )
+               return;
+
+       fputs( "    permacrud\n", stdout );
+       
+       osrfHashIterator* iter = osrfNewHashIterator( pcrud_hash );
+       osrfHash* pcrud_attr = NULL;
+
+       // Dump each action
+       for( ;; ) {
+               pcrud_attr = osrfHashIteratorNext( iter );
+               if( pcrud_attr )
+                       dump_action( pcrud_attr, osrfHashIteratorKey( iter ) );
+               else
+                       break;
+       }
+
+       osrfHashIteratorFree( iter );
+}
+
+static void dump_action( osrfHash* action_hash, const char* action_name ) {
+       if( !action_hash || !action_name )
+               return;
+
+       if( 0 == osrfHashGetCount( action_hash ) )
+               return;
+
+       printf( "        %s\n", action_name );
+
+       osrfHashIterator* iter = osrfNewHashIterator( action_hash );
+       void* action_attr = NULL;
+       const char* indent = "            ";
+
+       // Dump each attribute of the action
+       for( ;; ) {
+               action_attr = osrfHashIteratorNext( iter );
+               if( action_attr ) {
+                       const char* attr_name = osrfHashIteratorKey( iter );
+                       if( !strcmp( attr_name, "permission" ) )
+                               dump_string_array( action_attr, attr_name, indent );
+                       else if( !strcmp( attr_name, "global_required" ) )
+                               printf( "%s%s: %s\n", indent, attr_name, (char*) action_attr );
+                       else if( !strcmp( attr_name, "local_context" ) )
+                               dump_string_array( action_attr, attr_name, indent );
+                       else if( !strcmp( attr_name, "foreign_context" ) )
+                               dump_foreign_context( action_attr );
+                       else
+                               printf( "%s%s (unknown)\n", indent, attr_name );
+               } else
+                       break;
+       }
+
+       osrfHashIteratorFree( iter );
+}
+
+static void dump_foreign_context( osrfHash* fc_hash ) {
+       if( !fc_hash )
+               return;
+       
+       if( 0 == osrfHashGetCount( fc_hash ) )
+               return;
+
+       fputs( "            foreign_context\n", stdout );
+
+       osrfHashIterator* iter = osrfNewHashIterator( fc_hash );
+       osrfHash* fc_attr = NULL;
+
+       // Dump each foreign context attribute
+       for( ;; ) {
+               fc_attr = osrfHashIteratorNext( iter );
+               if( fc_attr )
+                       dump_fc_class( (osrfHash*) fc_attr, osrfHashIteratorKey( iter ) );
+               else
+                       break;
+       }
+
+       osrfHashIteratorFree( iter );
+}
+
+static void dump_fc_class( osrfHash* fc_class_hash, const char* class_name )
+{
+       if( ! fc_class_hash )
+               return;
+       
+       if( 0 == osrfHashGetCount( fc_class_hash ) )
+               return;
+       
+       printf( "                %s\n", class_name );
+
+       osrfHashIterator* iter = osrfNewHashIterator( fc_class_hash );
+       void* fc_class_attr = NULL;
+       const char* indent = "                    ";
+
+       // Dump each foreign context attribute
+       for( ;; ) {
+               fc_class_attr = osrfHashIteratorNext( iter );
+               if( fc_class_attr ) {
+                       const char* fc_class_attr_name = osrfHashIteratorKey( iter );
+                       if( !strcmp( fc_class_attr_name, "field" ) )
+                               printf( "%s%s: %s\n", indent, fc_class_attr_name, (const char*) fc_class_attr );
+                       else if( !strcmp( fc_class_attr_name, "fkey" ) )
+                               printf( "%s%s: %s\n", indent, fc_class_attr_name, (const char*) fc_class_attr );
+                       else if( !strcmp( fc_class_attr_name, "jump" ) )
+                               dump_string_array( (osrfStringArray*) fc_class_attr, fc_class_attr_name, indent );
+                       else if( !strcmp( fc_class_attr_name, "context" ) )
+                               dump_string_array( (osrfStringArray*) fc_class_attr, fc_class_attr_name, indent );
+                       else
+                               printf( "%s%s\n", indent, fc_class_attr_name );
+               } else
+                       break;
+       }
+
+       osrfHashIteratorFree( iter );
+}
+
+static void dump_string_array( 
+       osrfStringArray* sarr, const char* name, const char* indent ) {
+       if( !sarr || !name || !indent )
+               return;
+
+       int size = sarr->size;
+
+       // Ignore an empty array
+       if( 0 == size )
+               return;
+
+       printf( "%s%s (string array)\n", indent, name );
+
+       int i;
+       for( i = 0; i < size; ++i )
+               printf( "%s\t%s\n", indent, osrfStringArrayGetString( sarr, i ) );
+}
diff --git a/src/c/idlval.c b/src/c/idlval.c
new file mode 100644 (file)
index 0000000..eeceaa2
--- /dev/null
@@ -0,0 +1,1726 @@
+/**
+       @file idlval.c
+       @brief Validator for IDL files.
+*/
+
+/*
+Copyright (C) 2009  Georgia Public Library Service
+Scott McKellar <scott@esilibrary.com>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include <libxml/globals.h>
+#include <libxml/xmlerror.h>
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include <libxml/debugXML.h>
+#include <libxml/xmlmemory.h>
+
+#include <opensrf/utils.h>
+#include <opensrf/osrf_hash.h>
+
+/* Represents the command line */
+struct Opts {
+       int new_argc;
+       char ** new_argv;
+
+       char * idl_file_name;
+       int idl_file_name_found;
+       int warning;
+};
+typedef struct Opts Opts;
+
+/* datatype attribute of <field> element */
+typedef enum {
+       DT_NONE,
+       DT_BOOL,
+       DT_FLOAT,
+       DT_ID,
+       DT_INT,
+       DT_INTERVAL,
+       DT_LINK,
+       DT_MONEY,
+       DT_NUMBER,
+       DT_ORG_UNIT,
+       DT_TEXT,
+       DT_TIMESTAMP,
+       DT_INVALID
+} Datatype;
+
+/* Represents a <Field> aggregate */
+struct Field_struct {
+       struct Field_struct* next;
+       xmlChar* name;
+       int is_virtual;     // boolean
+       xmlChar* label;
+       Datatype datatype;
+};
+typedef struct Field_struct Field;
+
+/* reltype attribute of <link> element */
+typedef enum {
+       RT_NONE,
+       RT_HAS_A,
+       RT_MIGHT_HAVE,
+       RT_HAS_MANY,
+       RT_INVALID
+} Reltype;
+
+/* Represents a <link> element */
+struct Link_struct {
+       struct Link_struct* next;
+       xmlChar* field;
+       Reltype reltype;
+       xmlChar* key;
+       xmlChar* classref;
+};
+typedef struct Link_struct Link;
+
+/* Represents a <class> aggregate */
+typedef struct {
+       xmlNodePtr node;
+       int loaded;        // boolean
+       int is_virtual;    // boolean
+       xmlChar* primary;  // name of primary key column
+       Field* fields;     // linked list
+       Link* links;       // linked list
+} Class;
+
+static int get_Opts( int argc, char * argv[], Opts * pOpts );;
+static int val_idl( void );
+static int cross_validate_classes( Class* class, const char* id );
+static int cross_validate_linkage( Class* class, const char*id, Link* link );
+static int val_class( Class* class, const char* id );
+static int val_class_attributes( Class* class, const char* id );
+static int check_labels( const Class* class, const char* id );
+static int val_fields_attributes( Class* class, const char* id, xmlNodePtr fields );
+static int val_links_to_fields( const Class* class, const char* id );
+static int compareFieldAndLink( const Class* class, const char* id,
+               const Field* field, const Link* link );
+static int val_fields_to_links( const Class* class, const char* id );
+static const Field* searchFieldByName( const Class* class, const xmlChar* field_name );
+static int val_fields( Class* class, const char* id, xmlNodePtr fields );
+static int val_one_field( Class* class, const char* id, xmlNodePtr field );
+static Datatype translate_datatype( const xmlChar* value );
+static int val_links( Class* class, const char* id, xmlNodePtr links );
+static int val_one_link( Class* class, const char* id, xmlNodePtr link );
+static Reltype translate_reltype( const xmlChar* value );
+static int scan_idl( xmlDocPtr doc );
+static int register_class( xmlNodePtr child );
+static int addField( Class* class, const char* id, Field* new_field );
+static int addLink( Class* class, const char* id, Link* new_link );
+static Class* newClass( xmlNodePtr node );
+static void freeClass( char* key, void* p );
+static Field* newField( xmlChar* name );
+static void freeField( Field* field );
+static Link* newLink( xmlChar* field );
+static void freeLink( Link* link );
+
+/* Stores an in-memory representation of the IDL */
+static osrfHash* classes = NULL;
+
+static int warn = 0;       // boolean; true if -w present on command line
+
+int main( int argc, char* argv[] ) {
+
+       // Examine command line
+       Opts opts;
+       if( get_Opts( argc, argv, &opts ) )
+               return 1;
+
+       const char* IDL_filename = NULL;
+       if( opts.idl_file_name_found )
+               IDL_filename = opts.idl_file_name;
+       else {
+               IDL_filename = getenv( "OILS_IDL_FILENAME" );
+               if( ! IDL_filename )
+                       IDL_filename = "/openils/conf/fm_IDL.xml";
+       }
+
+       if( opts.warning )
+               warn = 1;
+
+       int rc = 0;
+
+       xmlLineNumbersDefault(1);
+       xmlDocPtr doc = xmlReadFile( IDL_filename, NULL, XML_PARSE_XINCLUDE );
+       if ( ! doc ) {
+               fprintf( stderr, "Could not load or parse the IDL XML file %s\n", IDL_filename );
+               rc = 1;
+       } else {
+               printf( "Validating: %s\n", IDL_filename );
+               classes = osrfNewHash();
+               osrfHashSetCallback( classes, freeClass );
+
+               // Load the IDL
+               if( scan_idl( doc ) )
+                       rc = 1;
+
+               if( opts.new_argc < 2 ) {
+
+                       // No classes specified: validate all classes
+                       if( val_idl() )
+                               rc = 1;
+               } else {
+
+                       // Validate one or more specified classes
+                       int i = 1;
+                       while( i < opts.new_argc ) {
+                               const char* classname = opts.new_argv[ i ];
+                               Class* class = osrfHashGet( classes, classname );
+                               if( ! class ) {
+                                       printf( "Class \"%s\" does not exist\n", classname );
+                                       rc = 1;
+                               } else {
+                                       // Validate the class in isolation
+                                       if( val_class( class, classname ) )
+                                               rc = 1;
+                                       // Cross-validate with linked classes
+                                       if( cross_validate_classes( class, classname ) )
+                                               rc = 1;
+                               }
+                               ++i;
+                       }
+               }
+               osrfHashFree( classes );
+               xmlFreeDoc( doc );
+       }
+
+       return rc;
+}
+
+/**
+       @brief Examine the command line
+       @param argc Number of entries in argv[]
+       @param argv Array of pointers to command line strings
+       @param pOpts Pointer to structure to be populated
+       @return 0 upon success, or 1 if the command line is invalid
+*/
+static int get_Opts( int argc, char * argv[], Opts * pOpts ) {
+       int rc = 0; /* return code */
+       int opt;
+
+       /* Define valid option characters */
+
+       const char optstring[] = ":f:w";
+
+       /* Initialize members of struct */
+
+       pOpts->new_argc = 0;
+       pOpts->new_argv = NULL;
+
+       pOpts->idl_file_name_found = 0;
+       pOpts->idl_file_name = NULL;
+       pOpts->warning = 0;
+
+       /* Suppress error messages from getopt() */
+
+       opterr = 0;
+
+       /* Examine command line options */
+
+       while( ( opt = getopt( argc, argv, optstring ) ) != -1 ) {
+               switch( opt ) {
+                       case 'f' :   /* Get idl_file_name */
+                               if( pOpts->idl_file_name_found ) {
+                                       fprintf( stderr, "Only one occurrence of -f option allowed\n" );
+                                       rc = 1;
+                                       break;
+                               }
+                               pOpts->idl_file_name_found = 1;
+
+                               pOpts->idl_file_name = optarg;
+                               break;
+                               case 'w' :   /* Get warning */
+                                       pOpts->warning = 1;
+                                       break;
+                                       case ':' : /* Missing argument */
+                                               fprintf( stderr, "Required argument missing on -%c option\n",
+                                                                (char) optopt );
+                                               rc = 1;
+                                               break;
+                                               case '?' : /* Invalid option */
+                                                       fprintf( stderr, "Invalid option '-%c' on command line\n",
+                                                                        (char) optopt );
+                                                       rc = 1;
+                                                       break;
+                                                       default :  /* Programmer error */
+                                                               fprintf( stderr, "Internal error: unexpected value '-%c'"
+                                                                               "for optopt", (char) optopt );
+                                                               rc = 1;
+                                                               break;
+               } /* end switch */
+       } /* end while */
+
+       if( optind > argc ) {
+               /* This should never happen! */
+
+               fprintf( stderr, "Program error: found more arguments than expected\n" );
+               rc = 1;
+       } else {
+               /* Calculate new_argcv and new_argc to reflect */
+               /* the number of arguments consumed */
+
+               pOpts->new_argc = argc - optind + 1;
+               pOpts->new_argv = argv + optind - 1;
+       }
+
+       return rc;
+}
+
+/**
+       @brief Validate all classes.
+       @return 1 if errors found, or 0 if not.
+
+       Traverse the class list and validate each class in turn.
+*/
+static int val_idl( void ) {
+       int rc = 0;
+       osrfHashIterator* itr = osrfNewHashIterator( classes );
+       Class* class = NULL;
+
+       // For each class
+       while( (class = osrfHashIteratorNext( itr )) ) {
+               const char* id = osrfHashIteratorKey( itr );
+               if( val_class( class, id ) )               // validate class separately
+                       rc = 1;
+               if( cross_validate_classes( class, id ) )  // cross-validate with linked classes
+                       rc = 1;
+       }
+
+       osrfHashIteratorFree( itr );
+       return rc;
+}
+
+/**
+       @brief Make sure that every linkage appropriately matches the linked class.
+       @param class Pointer to the current Class.
+       @param id Class id.
+       @return 1 if errors found, or 0 if not.
+*/
+static int cross_validate_classes( Class* class, const char* id ) {
+       int rc = 0;
+       Link* link = class->links;
+       while( link ) {
+               if( cross_validate_linkage( class, id, link ) )
+                       rc = 1;
+               link = link->next;
+       }
+
+       return rc;
+}
+
+/**
+       @brief Make sure that a linkage appropriately matches the linked class.
+       @param class Pointer to the current class.
+       @param id Class id.
+       @param link Pointer to the link being validated.
+       @return 1 if errors found, or 0 if not.
+
+       Rules:
+       - The linked class must exist.
+       - The field to which the linkage points must exist.
+       - If the linked class has a corresponding link back to the current class, then exactly
+       one end of the linkage must have a reltype of "has_many".
+
+       It is not an error if the linkage is not reciprocated.
+*/
+static int cross_validate_linkage( Class* class, const char*id, Link* link ) {
+       int rc = 0;
+       Class* other_class = osrfHashGet( classes, (char*) link->classref );
+       if( ! other_class ) {
+               printf( "In class \"%s\": class \"%s\", referenced by \"%s\" field, does not exist\n",
+                               id, (char*) link->classref, (char*) link->field );
+               rc = 1;
+       } else {
+               // Make sure the other class is loaded before we look at it further
+               if( val_class( other_class, (char*) link->classref ) )
+                       rc = 1;
+
+               // Now see if the other class links back to this one
+               Link* other_link = other_class->links;
+               while( other_link ) {
+                       if( !strcmp( id, (char*) other_link->classref )                    // class to class
+                               && !strcmp( (char*) link->key,   (char*) other_link->field )   // key to field
+                               && !strcmp( (char*) link->field, (char*) other_link->key ) ) { // field to key
+                               break;
+                       }
+                       other_link = other_link->next;
+               }
+
+               if( ! other_link ) {
+                       // Link is not reciprocated?  That's okay, as long as
+                       // the referenced field exists in the referenced class.
+                       if( !searchFieldByName( other_class, link->key ) ) {
+                               printf( "In class \"%s\": field \"%s\" links to field \"%s\" of class \"%s\", "
+                                       "but that field doesn't exist\n", id, (char*) link->field,
+                                       (char*) link->key, (char*) link->classref );
+                               rc = 1;
+                       }
+               } else {
+                       // The link is reciprocated.  Make sure that exactly one of the links
+                       // has a reltype of "has_many"
+                       int many_count = 0;
+                       if( RT_HAS_MANY == link->reltype )
+                               ++many_count;
+                       if( RT_HAS_MANY == other_link->reltype )
+                               ++many_count;
+
+                       if( 0 == many_count ) {
+                               printf( "Classes \"%s\" and \"%s\" link to each other, but neither has a reltype "
+                                               "of \"has_many\"\n", id, (char*) link->classref );
+                               rc = 1;
+                       } else if( 2 == many_count ) {
+                               printf( "Classes \"%s\" and \"%s\" link to each other, but both have a reltype "
+                                               "of \"has_many\"\n", id, (char*) link->classref );
+                               rc = 1;
+                       }
+               }
+       }
+
+       return rc;
+}
+
+/**
+       @brief Validate a single class.
+       @param id Class id.
+       @param class Pointer to the XML node for the class element.
+       @return 1 if errors found, or 0 if not.
+
+       We have already validated the id.
+
+       Rules:
+       - Allowed elements are "fields", "links", "permacrud", and "source_definition".
+       - None of these elements may occur more than once in the same class.
+       - The "fields" element is required.
+       - No text allowed, other than white space.
+       - Comments are allowed (and ignored).
+*/
+static int val_class( Class* class, const char* id ) {
+       if( !class )
+               return 1;
+       else if( class->loaded )
+               return 0;         // We've already validated this one locally
+
+       int rc = 0;
+
+       if( val_class_attributes( class, id ) )
+               rc = 1;
+
+       xmlNodePtr fields = NULL;
+       xmlNodePtr links = NULL;
+       xmlNodePtr permacrud = NULL;
+       xmlNodePtr src_def = NULL;
+
+       // Examine every child element of the <class> element.
+       xmlNodePtr child = class->node->children;
+       while( child ) {
+               const char* child_name = (char*) child->name;
+               if( xmlNodeIsText( child ) ) {
+                       if( ! xmlIsBlankNode( child ) ) {
+                               // Found unexpected text.  After removing leading and
+                               // trailing white space, complain about it.
+                               xmlChar* content = xmlNodeGetContent( child );
+
+                               xmlChar* begin = content;
+                               while( *begin && isspace( *begin ) )
+                                       ++begin;
+                               if( *begin ) {
+                                       xmlChar* end = begin + strlen( (char*) begin ) - 1;
+                                       while( (isspace( *end ) ) )
+                                               --end;
+                                       end[ 1 ] = '\0';
+                               }
+
+                               printf( "Unexpected text in class \"%s\": \"%s\"\n", id,
+                                       (char*) begin );
+                               xmlFree( content );
+                       }
+               } else if( !strcmp( child_name, "fields" ) ) {
+                       if( fields ) {
+                               printf( "Multiple <fields> elements in class \"%s\"\n", id );
+                               rc = 1;
+                       } else {
+                               fields = child;
+                               // Identify the primary key, if any
+                               class->primary = xmlGetProp( fields, (xmlChar*) "primary" );
+                               if( val_fields( class, id, fields ) )
+                                       rc = 1;
+                       }
+               } else if( !strcmp( child_name, "links" ) ) {
+                       if( links ) {
+                               printf( "Multiple <links> elements in class \"%s\"\n", id );
+                               rc = 1;
+                       } else {
+                               links = child;
+                               if( val_links( class, id, links ) )
+                                       rc = 1;
+                       }
+               } else if( !strcmp( child_name, "permacrud" ) ) {
+                       if( permacrud ) {
+                               printf( "Multiple <permacrud> elements in class \"%s\"\n", id );
+                               rc = 1;
+                       } else {
+                               permacrud = child;
+                       }
+               } else if( !strcmp( child_name, "source_definition" ) ) {
+                       if( src_def ) {
+                               printf( "Multiple <source_definition> elements in class \"%s\"\n", id );
+                               rc = 1;
+                       } else {
+                               // To do: verify that there is nothing in <source_definition> except text and
+                               // comments, and that the text is non-empty.
+                               src_def = child;
+                       }
+               } else if( !strcmp( child_name, "comment" ) )
+                       ;  // ignore comment
+               else {
+                       printf( "Line %ld: Unexpected <%s> element in class \"%s\"\n",
+                               xmlGetLineNo( child ), child_name, id );
+                       rc = 1;
+               }
+               child = child->next;
+       }
+
+       if( fields ) {
+               if( check_labels( class, id ) )
+                       rc = 1;
+               if( val_fields_attributes( class, id, fields ) )
+                       rc = 1;
+       } else {
+               printf( "No <fields> element in class \"%s\"\n", id );
+               rc = 1;
+       }
+
+       if( val_links_to_fields( class, id ) )
+               rc = 1;
+
+       if( val_fields_to_links( class, id ) )
+               rc = 1;
+
+       class->loaded = 1;
+       return rc;
+}
+
+/**
+       @brief Validate the class attributes.
+       @param class Pointer to the current Class.
+       @param id Class id.
+       @return if errors found, or 0 if not.
+
+       Rules:
+       - Only the following attributes are valid: controller, core, field_safe, field_mapper,
+       id, label, readonly, restrict_primary, tablename, and virtual.
+       - The controller and fieldmapper attributes are required (as is the id attribute, but
+       that's checked elsewhere).
+       - Every attribute value must be non-empty.
+       - The values of attributes core, field_safe, reaadonly, and virtual must be either
+       "true" or "false".
+       - A virtual class must not have a tablename attribute.
+*/
+static int val_class_attributes( Class* class, const char* id ) {
+       int rc = 0;
+
+       int controller_found = 0;     // boolean
+       int fieldmapper_found = 0;    // boolean
+       int tablename_found = 0;      // boolean
+
+       xmlAttrPtr attr = class->node->properties;
+       while( attr ) {
+               const char* attr_name = (char*) attr->name;
+               if( !strcmp( (char*) attr_name, "id" ) ) {
+                       ;  // ignore; we already grabbed this one
+               } else if( !strcmp( (char*) attr_name, "controller" ) ) {
+                       controller_found = 1;
+                       xmlChar* value = xmlGetProp( class->node, (xmlChar*) "controller" );
+                       if( '\0' == *value ) {
+                               printf( "Line %ld: Value of controller attribute is empty in class \"%s\"\n",
+                                       xmlGetLineNo( class->node ), id );
+                               rc = 1;
+                       }
+                       xmlFree( value );
+               } else if( !strcmp( (char*) attr_name, "fieldmapper" ) ) {
+                       fieldmapper_found = 1;
+                       xmlChar* value = xmlGetProp( class->node, (xmlChar*) "fieldmapper" );
+                       if( '\0' == *value ) {
+                               printf( "Line %ld: Value of fieldmapper attribute is empty in class \"%s\"\n",
+                                               xmlGetLineNo( class->node ), id );
+                               rc = 1;
+                       }
+                       xmlFree( value );
+               } else if( !strcmp( (char*) attr_name, "label" ) ) {
+                       xmlChar* value = xmlGetProp( class->node, (xmlChar*) "label" );
+                       if( '\0' == *value ) {
+                               printf( "Line %ld: Value of label attribute is empty in class \"%s\"\n",
+                                               xmlGetLineNo( class->node ), id );
+                               rc = 1;
+                       }
+                       xmlFree( value );
+               } else if( !strcmp( (char*) attr_name, "tablename" ) ) {
+                       tablename_found = 1;
+                       xmlChar* value = xmlGetProp( class->node, (xmlChar*) "tablename" );
+                       if( '\0' == *value ) {
+                               printf( "Line %ld: Value of tablename attribute is empty in class \"%s\"\n",
+                                               xmlGetLineNo( class->node ), id );
+                               rc = 1;
+                       }
+                       xmlFree( value );
+               } else if( !strcmp( (char*) attr_name, "virtual" ) ) {
+                       xmlChar* virtual_str = xmlGetProp( class->node, (xmlChar*) "virtual" );
+                       if( virtual_str ) {
+                               if( !strcmp( (char*) virtual_str, "true" ) ) {
+                                       class->is_virtual = 1;
+                               } else if( strcmp( (char*) virtual_str, "false" ) ) {
+                                       printf(
+                                               "Line %ld: Invalid value \"%s\" for virtual attribute of class\"%s\"\n",
+                                               xmlGetLineNo( class->node ), (char*) virtual_str, id );
+                                       rc = 1;
+                               }
+                               xmlFree( virtual_str );
+                       }
+               } else if( !strcmp( (char*) attr_name, "readonly" ) ) {
+                       xmlChar* readonly = xmlGetProp( class->node, (xmlChar*) "readonly" );
+                       if( readonly ) {
+                               if(    strcmp( (char*) readonly, "true" )
+                                       && strcmp( (char*) readonly, "false" ) ) {
+                                       printf(
+                                               "Line %ld: Invalid value \"%s\" for readonly attribute of class\"%s\"\n",
+                                               xmlGetLineNo( class->node ), (char*) readonly, id );
+                                       rc = 1;
+                               }
+                               xmlFree( readonly );
+                       }
+               } else if( !strcmp( (char*) attr_name, "restrict_primary" ) ) {
+                       xmlChar* value = xmlGetProp( class->node, (xmlChar*) "restrict_primary" );
+                       if( '\0' == *value ) {
+                               printf( "Line %ld: Value of restrict_primary attribute is empty in class \"%s\"\n",
+                                               xmlGetLineNo( class->node ), id );
+                               rc = 1;
+                       }
+                       xmlFree( value );
+               } else if( !strcmp( (char*) attr_name, "core" ) ) {
+                       xmlChar* core = xmlGetProp( class->node, (xmlChar*) "core" );
+                       if( core ) {
+                               if(    strcmp( (char*) core, "true" )
+                                       && strcmp( (char*) core, "false" ) ) {
+                                       printf(
+                                          "Line %ld: Invalid value \"%s\" for core attribute of class\"%s\"\n",
+                                               xmlGetLineNo( class->node ), (char*) core, id );
+                                       rc = 1;
+                               }
+                               xmlFree( core );
+                       }
+               } else if( !strcmp( (char*) attr_name, "field_safe" ) ) {
+                       xmlChar* field_safe = xmlGetProp( class->node, (xmlChar*) "field_safe" );
+                       if( field_safe ) {
+                               if(    strcmp( (char*) field_safe, "true" )
+                                       && strcmp( (char*) field_safe, "false" ) ) {
+                                       printf(
+                                               "Line %ld: Invalid value \"%s\" for field_safe attribute of class\"%s\"\n",
+                                               xmlGetLineNo( class->node ), (char*) field_safe, id );
+                                       rc = 1;
+                               }
+                               xmlFree( field_safe );
+                       }
+               } else {
+                       printf( "Line %ld: Unrecognized class attribute \"%s\" in class \"%s\"\n",
+                               xmlGetLineNo( class->node ), attr_name, id );
+                       rc = 1;
+               }
+               attr = attr->next;
+       } // end while
+
+       if( ! controller_found ) {
+               printf( "Line %ld: No controller attribute for class \"%s\"\n",
+                       xmlGetLineNo( class->node ), id );
+               rc = 1;
+       }
+
+       if( ! fieldmapper_found ) {
+               printf( "Line %ld: No fieldmapper attribute for class \"\%s\"\n",
+                       xmlGetLineNo( class->node ), id );
+               rc = 1;
+       }
+
+       if( class->is_virtual && tablename_found ) {
+               printf( "Line %ld: Virtual class \"%s\" shouldn't have a tablename",
+                       xmlGetLineNo( class->node ), id );
+               rc = 1;
+       }
+
+       return rc;
+}
+
+/**
+       @brief Determine whether fields are either all labeled or all unlabeled.
+       @param class Pointer to the current Class.
+       @param id Class id.
+       @return 1 if errors found, or 0 if not.
+
+       Rule:
+       - The fields for a given class must either all be labeled or all unlabeled.
+
+       For purposes of this validation, a field is considered labeled even if the label is an
+       empty string.  Empty labels are reported elsewhere.
+*/
+static int check_labels( const Class* class, const char* id ) {
+       int rc = 0;
+
+       int label_found = 0;    // boolean
+       int unlabel_found = 0;  // boolean
+
+       Field* field = class->fields;
+       while( field ) {
+               if( field->label )
+                       label_found = 1;
+               else
+                       unlabel_found = 1;
+               field = field->next;
+       }
+
+       if( label_found && unlabel_found ) {
+               printf( "Class \"%s\" has a mixture of labeled and unlabeled fields\n", id );
+               rc = 1;
+       }
+
+       return rc;
+}
+
+/**
+       @brief Validate the fields attributes.
+       @param class Pointer to the current Class.
+       @param id Class id.
+       @param fields Pointer to the XML node for the fields element.
+       @return if errors found, or 0 if not.
+
+       Rules:
+       - The only valid attributes for the fields element are "primary" and "sequence".
+       - Neither attribute may have an empty string for a value.
+       - If there is a sequence attribute, there must also be a primary attribute.
+       - If there is a primary attribute, the field identified must exist.
+       - If the primary key field has the datatype "id", there must be a sequence attribute.
+       - If the datatype of the primary key is not "id" or "int", there must @em not be a
+       sequence attribute.
+*/
+static int val_fields_attributes( Class* class, const char* id, xmlNodePtr fields ) {
+       int rc = 0;
+
+       xmlChar* sequence = NULL;
+       xmlChar* primary  = NULL;
+
+       // Traverse the attributes
+       xmlAttrPtr attr = fields->properties;
+       while( attr ) {
+               const char* attr_name = (char*) attr->name;
+               if( !strcmp( attr_name, "primary" ) ) {
+                       primary = xmlGetProp( fields, (xmlChar*) "primary" );
+                       if( '\0' == primary[0] ) {
+                               printf(
+                                       "Line %ld: value of primary attribute is an empty string for class \"%s\"\n",
+                                       xmlGetLineNo( fields ), id );
+                               rc = 1;
+                       }
+               } else if( !strcmp( attr_name, "sequence" )) {
+                       sequence = xmlGetProp( fields, (xmlChar*) "sequence" );
+                       if( '\0' == sequence[0] ) {
+                               printf(
+                                       "Line %ld: value of sequence attribute is an empty string for class \"%s\"\n",
+                                       xmlGetLineNo( fields ), id );
+                               rc = 1;
+                       } else if( !strchr( (const char*) sequence, '.' )) {
+                               printf(
+                                       "Line %ld: name of sequence for class \"%s\" is not qualified by schema\n",
+                                       xmlGetLineNo( fields ), id );
+                               rc = 1;
+                       }
+               } else {
+                       printf( "Line %ld: Unexpected fields attribute \"%s\" in class \"%s\"\n",
+                               xmlGetLineNo( fields ), attr_name, id );
+                       rc = 1;
+               }
+
+               attr = attr->next;
+       }
+
+       if( sequence && ! primary ) {
+               printf( "Line %ld: class \"%s\" has a sequence identified but no primary key\n",
+                       xmlGetLineNo( fields ), id );
+               rc = 1;
+       }
+
+       if( primary ) {
+               // look for the primary key
+               Field* field = class->fields;
+               while( field ) {
+                       if( !strcmp( (char*) field->name, (char*) primary ) )
+                               break;
+                       field = field->next;
+               }
+               if( !field ) {
+                       printf( "Primary key field \"%s\" does not exist for class \"%s\"\n",
+                               (char*) primary, id );
+                       rc = 1;
+               } else if( DT_ID == field->datatype && ! sequence && ! class->is_virtual ) {
+                       printf(
+                               "Line %ld: Primary key is an id; class \"%s\" may need a sequence attribute\n",
+                               xmlGetLineNo( fields ), id );
+                       rc = 1;
+               } else if(    DT_ID != field->datatype
+                                  && DT_INT != field->datatype
+                                  && DT_ORG_UNIT != field->datatype
+                                  && sequence ) {
+                       printf(
+                               "Line %ld: Datatype of key for class \"%s\" does not allow a sequence attribute\n",
+                               xmlGetLineNo( fields ), id );
+                       rc = 1;
+               }
+       }
+
+       xmlFree( primary );
+       xmlFree( sequence );
+       return rc;
+}
+
+/**
+       @brief Verify that every Link has a matching Field for a given Class.
+       @param class Pointer to the current class.
+       @param id Class id.
+       @return 1 if errors found, or 0 if not.
+
+       Rules:
+       - For every link element, there must be a matching field element in the same class.
+       - If the link's reltype is "has_many", the field must be a virtual field of type link
+       or org_unit.
+       - If the link's reltype is "has_a" or "might_have", the field must be a non-virtual link
+       of type link or org_unit.
+*/
+static int val_links_to_fields( const Class* class, const char* id ) {
+       if( !class )
+               return 1;
+
+       int rc = 0;
+
+       const Link* link = class->links;
+       while( link ) {
+               if( link->field && *link->field ) {
+                       const Field* field = searchFieldByName( class, link->field );
+                       if( field ) {
+                               if( compareFieldAndLink( class, id, field, link ) )
+                                       rc = 1;
+                       } else {
+                               printf( "\"%s\" class has no <field> corresponding to <link> for \"%s\"\n",
+                                       id, (char*) link->field );
+                               rc = 1;
+                       }
+               }
+               link = link->next;
+       }
+
+       return rc;
+}
+
+/**
+       @brief Compare matching field and link elements to see if they are compatible
+       @param class Pointer to the current Class.
+       @param id Class id.
+       @param field Pointer to the Field to be compared to the Link.
+       @param link Pointer to the Link to be compared to the Field.
+       @return 0 if they are compatible, or 1 if not.
+
+       Rules:
+       - If the reltype is "has_many", the field must be virtual.
+       - If a field corresponds to a link, and is not the primary key, then it must have a
+       datatype "link" or "org_unit".
+       - If the datatype is "org_unit", the linkage must be to the class "aou".
+
+       Warnings:
+       - If the reltype is "has_a" or "might_have", the the field should probably @em not
+       be virtual, but there are legitimate exceptions.
+       - If the linkage is to the class "aou", then the datatype should probably be "org_unit".
+*/
+static int compareFieldAndLink( const Class* class, const char* id,
+               const Field* field, const Link* link ) {
+       int rc = 0;
+
+       Datatype datatype = field->datatype;
+       const char* classref = (const char*) link->classref;
+
+       // Validate the virtuality of the field
+       if( RT_HAS_A == link->reltype || RT_MIGHT_HAVE == link->reltype ) {
+               if( warn && field->is_virtual ) {
+                       // This is the child class; field should usually be non-virtual,
+                       // but there are legitimate exceptions.
+                       printf( "WARNING: In class \"%s\": field \"%s\" is tied to a \"has_a\" or "
+                               "\"might_have\" link; perhaps should not be virtual\n",
+                               id, (char*) field->name );
+               }
+       } else if ( RT_HAS_MANY == link->reltype ) {
+               if( ! field->is_virtual ) {
+                       printf( "In class \"%s\": field \"%s\" is tied to a \"has_many\" link "
+                                       "and therefore should be virtual\n", id, (char*) field->name );
+                       rc = 1;
+               }
+       }
+
+       // Validate the datatype of the field
+       if( class->primary && !strcmp( (char*) class->primary, (char*) field->name ) ) {
+               ; // For the primary key field, the datatype can be anything
+       } else if( DT_NONE == datatype || DT_INVALID == datatype ) {
+               printf( "In class \"%s\": \"%s\" field should have a datatype for linkage\n",
+                               id, (char*) field->name );
+               rc = 1;
+       } else if( DT_ORG_UNIT == datatype ) {
+               if( strcmp( classref, "aou" ) ) {
+                       printf( "In class \"%s\": \"%s\" field should have a datatype "
+                                       "\"link\", not \"org_unit\"\n", id, field->name );
+                       rc = 1;
+               }
+       } else if( DT_LINK == datatype ) {
+               if( warn && !strcmp( classref, "aou" ) ) {
+                       printf( "WARNING: In class \"%s\", field \"%s\": Consider changing datatype "
+                                       "to \"org_unit\"\n", id, (char*) field->name );
+               }
+       } else {
+               // Datatype should be "link", or maybe "org_unit"
+               if( !strcmp( classref, "aou" ) ) {
+                       printf( "In class \"%s\": \"%s\" field should have a datatype "
+                                       "\"org_unit\" or \"link\"\n",
+                                       id, (char*) field->name );
+                       rc = 1;
+               } else {
+                       printf( "In class \"%s\": \"%s\" field should have a datatype \"link\"\n",
+                                       id, (char*) field->name );
+                       rc = 1;
+               }
+       }
+
+       return rc;
+}
+
+/**
+       @brief See if every linked field has a counterpart in the links aggregate.
+       @param class Pointer to the current class.
+       @param id Class id.
+       @return 1 if errors found, or 0 if not.
+
+       Rules:
+       - If a field has a datatype of "link" or "org_unit, there must be a corresponding
+       entry in the links aggregate.
+*/
+static int val_fields_to_links( const Class* class, const char* id ) {
+       int rc = 0;
+       const Field* field = class->fields;
+       while( field ) {
+               if( DT_LINK != field->datatype && DT_ORG_UNIT != field->datatype ) {
+                       field = field->next;
+                       continue;  // not a link?  skip it
+               }
+               // See if there's a matching entry in the <links> aggregate
+               const Link* link = class->links;
+               while( link ) {
+                       if( !strcmp( (char*) field->name, (char*) link->field ) )
+                               break;
+                       link = link->next;
+               }
+
+               if( !link ) {
+                       if( !strcmp( (char*) field->name, "id" ) && !strcmp( id, "aou" ) ) {
+                               // Special exception: primary key of "aou" is of
+                               // datatype "org_unit", but it's not a foreign key.
+                               ;
+                       } else {
+                               printf( "In class \"%s\": Linked field \"%s\" has no matching <link>\n",
+                                               id, (char*) field->name );
+                               rc = 1;
+                       }
+               }
+               field = field->next;
+       }
+       return rc;
+}
+
+/**
+       @brief Search a given Class for a Field with a given name.
+       @param class Pointer to the class in which to search.
+       @param field_name The field name for which to search.
+       @return Pointer to the Field if found, or NULL if not.
+*/
+static const Field* searchFieldByName( const Class* class, const xmlChar* field_name ) {
+       if( ! class || ! field_name || ! *field_name )
+               return NULL;
+
+       const char* name = (const char*) field_name;
+       const Field* field = class->fields;
+       while( field ) {
+               if( field->name && !strcmp( (char*) field->name, name ) )
+                       return field;
+               field = field->next;
+       }
+
+       return NULL;
+}
+
+/**
+       @brief Validate a fields element.
+       @param class Pointer to the current Class.
+       @param id Id of the current Class.
+       @param fields Pointer to the XML node for the fields element.
+       @return 1 if errors found, or 0 if not.
+
+       Rules:
+       - There must be at least one field element.
+       - No other elements are allowed.
+       - Text is not allowed, other than white space.
+       - Comments are allowed (and ignored).
+*/
+static int val_fields( Class* class, const char* id, xmlNodePtr fields ) {
+       int rc = 0;
+       int field_found = 0;    // boolean
+
+       xmlNodePtr child = fields->children;
+       while( child ) {
+               const char* child_name = (char*) child->name;
+               if( xmlNodeIsText( child ) ) {
+                       if( ! xmlIsBlankNode( child ) ) {
+                               // Found unexpected text.  After removing leading and
+                               // trailing white space, complain about it.
+                               xmlChar* content = xmlNodeGetContent( child );
+
+                               xmlChar* begin = content;
+                               while( *begin && isspace( *begin ) )
+                                       ++begin;
+                               if( *begin ) {
+                                       xmlChar* end = begin + strlen( (char*) begin ) - 1;
+                                       while( (isspace( *end ) ) )
+                                               --end;
+                                       end[ 1 ] = '\0';
+                               }
+
+                               printf( "Unexpected text in <fields> element of class \"%s\": \"%s\"\n", id,
+                                       (char*) begin );
+                               xmlFree( content );
+                       }
+               } else if( ! strcmp( child_name, "field" ) ) {
+                       field_found = 1;
+                       if( val_one_field( class, id, child ) )
+                               rc = 1;
+               } else if( !strcmp( child_name, "comment" ) )
+                       ;  // ignore comment
+               else {
+                       printf( "Line %ld: Unexpected <%s> element in <fields> of class \"%s\"\n",
+                               xmlGetLineNo( child ), child_name, id );
+                       rc = 1;
+               }
+               child = child->next;
+       }
+
+       if( !field_found ) {
+               printf( "No <field> element in class \"%s\"\n", id );
+               rc = 1;
+       }
+
+       return rc;
+}
+
+/**
+       @brief Validate a field element within a fields element.
+       @param class Pointer to the current Class.
+       @param id Class id.
+       @param field Pointer to the XML node for the field element.
+       @return 1 if errors found, or 0 if not.
+
+       Rules:
+       - attribute names are limited to: "name", "virtual", "label", "datatype", "array_position",
+       "selector", "i18n", "primitive".
+       - "name" attribute is required.
+       - label attribute, if present, must have a non-empty value.
+       - virtual and i18n attributes, if present, must have a value of "true" or "false".
+       - if the datatype attribute is present, its value must be one of: "bool", "float", "id",
+       "int", "interval", "link", "money", "number", "org_unit", "text", "timestamp".
+
+       Warnings:
+       - A non-virtual field should have a datatype attribute.
+       - Attribute "array_position" is deprecated.
+*/
+static int val_one_field( Class* class, const char* id, xmlNodePtr field ) {
+       int rc = 0;
+       xmlChar* label = NULL;
+       xmlChar* field_name = NULL;
+       int is_virtual = 0;
+       Datatype datatype = DT_NONE;
+
+       // Traverse the attributes
+       xmlAttrPtr attr = field->properties;
+       while( attr ) {
+               const char* attr_name = (char*) attr->name;
+               if( !strcmp( attr_name, "name" ) ) {
+                       field_name = xmlGetProp( field, (xmlChar*) "name" );
+               } else if( !strcmp( attr_name, "virtual" ) ) {
+                       xmlChar* virt = xmlGetProp( field, (xmlChar*) "virtual" );
+                       if( !strcmp( (char*) virt, "true" ) )
+                               is_virtual = 1;
+                       else if( strcmp( (char*) virt, "false" ) ) {
+                               printf( "Line %ld: Invalid value for virtual attribute: \"%s\"\n",
+                                       xmlGetLineNo( field ), (char*) virt );
+                               rc = 1;
+                       }
+                       xmlFree( virt );
+                       // To do: verify that the namespace is oils_persist
+               } else if( !strcmp( attr_name, "label" ) ) {
+                       label = xmlGetProp( field, (xmlChar*) "label" );
+                       if( '\0' == *label ) {
+                               printf( "Line %ld: Empty value for label attribute for class \"%s\"\n",
+                                       xmlGetLineNo( field ), id );
+                               xmlFree( label );
+                               label = NULL;
+                               rc = 1;
+                       }
+                       // To do: verify that the namespace is reporter
+               } else if( !strcmp( attr_name, "datatype" ) ) {
+                       xmlChar* dt_str = xmlGetProp( field, (xmlChar*) "datatype" );
+                       datatype = translate_datatype( dt_str );
+                       if( DT_INVALID == datatype ) {
+                               printf( "Line %ld: Invalid datatype \"%s\" in class \"%s\"\n",
+                                       xmlGetLineNo( field ), (char*) dt_str, id );
+                               rc = 1;
+                       }
+                       xmlFree( dt_str );
+                       // To do: make sure that the namespace is reporter
+               } else if( !strcmp( attr_name, "array_position" ) ) {
+                       printf( "Line %ld: WARNING: Deprecated array_position attribute "
+                                       "for field \"%s\" in class \"%s\"\n",
+                                       xmlGetLineNo( field ), ((char*) field_name ? : ""), id );
+               } else if( !strcmp( attr_name, "selector" ) ) {
+                       ;  // Ignore for now
+               } else if( !strcmp( attr_name, "i18n" ) ) {
+                       xmlChar* i18n = xmlGetProp( field, (xmlChar*) "i18n" );
+                       if( strcmp( (char*) i18n, "true" ) && strcmp( (char*) i18n, "false" ) ) {
+                               printf( "Line %ld: Invalid value for i18n attribute: \"%s\"\n",
+                                       xmlGetLineNo( field ), (char*) i18n );
+                               rc = 1;
+                       }
+                       xmlFree( i18n );
+                       // To do: verify that the namespace is oils_persist
+               } else if( !strcmp( attr_name, "primitive" ) ) {
+                       xmlChar* primitive = xmlGetProp( field, (xmlChar*) "primitive" );
+                       if( strcmp( (char*) primitive, "string" ) && strcmp( (char*) primitive, "number" ) ) {
+                               printf( "Line %ld: Invalid value for primitive attribute: \"%s\"\n",
+                                       xmlGetLineNo( field ), (char*) primitive );
+                               rc = 1;
+                       }
+                       xmlFree( primitive );
+               } else if( !strcmp( attr_name, "validate" )) {
+                       xmlChar* validate = xmlGetProp( field, (xmlChar*) "validate" );
+                       if( !*validate ) {
+                               // Value should be a regular expression to define a validation rule
+                               printf( "Line %ld: Empty value for \"validate\" attribute "
+                                       "for field \"%s\" in class \"%s\"\n",
+                                       xmlGetLineNo( field ), (char*) field_name ? : "", id );
+                               rc = 1;
+                       }
+                       xmlFree( validate );
+                       // To do: verify that the namespace is oils_obj
+               } else if( !strcmp( attr_name, "required" )) {
+                       xmlChar* required = xmlGetProp( field, (xmlChar*) "required" );
+                       if( strcmp( (char*) required, "true" ) && strcmp( (char*) required, "false" )) {
+                               printf( 
+                                       "Line %ld: Invalid value \"%s\" for \"required\" attribute "
+                                       "for field \"%s\" in class \"%s\"\n",
+                                       xmlGetLineNo( field ), (char*) required,
+                                       (char*) field_name ? : "", id );
+                               rc = 1;
+                       }
+                       xmlFree( required );
+                       // To do: verify that the namespace is oils_obj
+               } else {
+                       printf( "Line %ld: Unexpected field attribute \"%s\" in class \"%s\"\n",
+                               xmlGetLineNo( field ), attr_name, id );
+                       rc = 1;
+               }
+
+               attr = attr->next;
+       }
+
+       if( warn && (!is_virtual) && DT_NONE == datatype ) {
+               printf( "Line %ld: WARNING: No datatype attribute for field \"%s\" in class \"%s\"\n",
+                       xmlGetLineNo( field ), ((char*) field_name ? : ""), id );
+       }
+
+       if( ! field_name ) {
+               printf( "Line %ld: No name attribute for <field> element in class \"%s\"\n",
+                       xmlGetLineNo( field ), id );
+               rc = 1;
+       } else if( '\0' == *field_name ) {
+               printf( "Line %ld: Field name is empty for <field> element in class \"%s\"\n",
+                       xmlGetLineNo( field ), id );
+               rc = 1;
+       } else {
+               // Add to the class's field list
+               Field* new_field = newField( field_name );
+               new_field->is_virtual = is_virtual;
+               new_field->label = label;
+               new_field->datatype = datatype;
+               if( addField( class, id, new_field ) )
+                       rc = 1;
+       }
+
+       return rc;
+}
+
+/**
+       @brief Translate a datatype string into a Dataype (an enum).
+       @param value The value of a datatype attribute.
+       @return The datatype in the form of an enum.
+*/
+static Datatype translate_datatype( const xmlChar* value ) {
+       const char* val = (const char*) value;
+       Datatype type;
+
+       if( !value || !*value )
+               type = DT_NONE;
+       else if( !strcmp( val, "bool" ) )
+               type = DT_BOOL;
+       else if( !strcmp( val, "float" ) )
+               type = DT_FLOAT;
+       else if( !strcmp( val, "id" ) )
+               type = DT_ID;
+       else if( !strcmp( val, "int" ) )
+               type = DT_INT;
+       else if( !strcmp( val, "interval" ) )
+               type = DT_INTERVAL;
+       else if( !strcmp( val, "link" ) )
+               type = DT_LINK;
+       else if( !strcmp( val, "money" ) )
+               type = DT_MONEY;
+       else if( !strcmp( val, "number" ) )
+               type = DT_NUMBER;
+       else if( !strcmp( val, "org_unit" ) )
+               type = DT_ORG_UNIT;
+       else if( !strcmp( val, "text" ) )
+               type = DT_TEXT;
+       else if( !strcmp( val, "timestamp" ) )
+               type = DT_TIMESTAMP;
+       else
+               type = DT_INVALID;
+
+       return type;
+}
+
+/**
+       @brief Validate a links element.
+       @param class Pointer to the current Class.
+       @param id Id of the current Class.
+       @param links Pointer to the XML node for the links element.
+       @return 1 if errors found, or 0 if not.
+
+       Rules:
+       - No elements other than "link" are allowed.
+       - Text is not allowed, other than white space.
+       - Comments are allowed (and ignored).
+
+       Warnings:
+       - There is usually at least one link element.
+*/
+static int val_links( Class* class, const char* id, xmlNodePtr links ) {
+       int rc = 0;
+       int link_found = 0;    // boolean
+
+       xmlNodePtr child = links->children;
+       while( child ) {
+               const char* child_name = (char*) child->name;
+               if( xmlNodeIsText( child ) ) {
+                       if( ! xmlIsBlankNode( child ) ) {
+                               // Found unexpected text.  After removing leading and
+                               // trailing white space, complain about it.
+                               xmlChar* content = xmlNodeGetContent( child );
+
+                               xmlChar* begin = content;
+                               while( *begin && isspace( *begin ) )
+                                       ++begin;
+                               if( *begin ) {
+                                       xmlChar* end = begin + strlen( (char*) begin ) - 1;
+                                       while( (isspace( *end ) ) )
+                                               --end;
+                                       end[ 1 ] = '\0';
+                               }
+
+                               printf( "Unexpected text in <links> element of class \"%s\": \"%s\"\n", id,
+                                       (char*) begin );
+                               xmlFree( content );
+                       }
+               } else if( ! strcmp( child_name, "link" ) ) {
+                       link_found = 1;
+                       if( val_one_link( class, id, child ) )
+                               rc = 1;
+               } else if( !strcmp( child_name, "comment" ) )
+                       ;  // ignore comment
+               else {
+                       printf( "Line %ld: Unexpected <%s> element in <link> of class \"%s\"\n",
+                               xmlGetLineNo( child ), child_name, id );
+                               rc = 1;
+               }
+               child = child->next;
+       }
+
+       if( warn && !link_found ) {
+               printf( "WARNING: No <link> element in class \"%s\"\n", id );
+       }
+
+       return rc;
+}
+
+/**
+               @brief Validate one link element.
+               @param class Pointer to the current Class.
+               @param id Id of the current Class.
+               @param link Pointer to the XML node for the link element.
+               @return 1 if errors found, or 0 if not.
+
+       Rules:
+       - The only allowed attributes are "field", "reltype", "key", "map", and "class".
+       - Except for map, every attribute is required.
+       - Except for map, every attribute must have a non-empty value.
+       - The value of the reltype attribute must be one of "has_a", "might_have", or "has_many".
+*/
+static int val_one_link( Class* class, const char* id, xmlNodePtr link ) {
+       int rc = 0;
+       xmlChar* field_name = NULL;
+       Reltype reltype = RT_NONE;
+       xmlChar* key = NULL;
+       xmlChar* classref = NULL;
+
+       // Traverse the attributes
+       xmlAttrPtr attr = link->properties;
+       while( attr ) {
+               const char* attr_name = (const char*) attr->name;
+               if( !strcmp( attr_name, "field" ) ) {
+                       field_name = xmlGetProp( link, (xmlChar*) "field" );
+               } else if (!strcmp( attr_name, "reltype" ) ) {
+                       ;
+                       xmlChar* rt = xmlGetProp( link, (xmlChar*) "reltype" );
+                       if( *rt ) {
+                               reltype = translate_reltype( rt );
+                               if( RT_INVALID == reltype ) {
+                                       printf(
+                                               "Line %ld: Invalid value \"%s\" for reltype attribute in class \"%s\"\n",
+                                               xmlGetLineNo( link ), (char*) rt, id );
+                                       rc = 1;
+                               }
+                       } else {
+                               printf( "Line %ld: Empty value for reltype attribute in class \"%s\"\n",
+                                       xmlGetLineNo( link ), id );
+                               rc = 1;
+                       }
+                       xmlFree( rt );
+               } else if (!strcmp( attr_name, "key" ) ) {
+                       key = xmlGetProp( link, (xmlChar*) "key" );
+               } else if (!strcmp( attr_name, "map" ) ) {
+                       ;   // ignore for now
+               } else if (!strcmp( attr_name, "class" ) ) {
+                       classref = xmlGetProp( link, (xmlChar*) "class" );
+               } else {
+                       printf( "Line %ld: Unexpected attribute %s in links element of class \"%s\"\n",
+                               xmlGetLineNo( link ), attr_name, id );
+                       rc = 1;
+               }
+               attr = attr->next;
+       }
+
+       if( !field_name ) {
+               printf( "Line %ld: No field attribute found in <link> in class \"%s\"\n",
+                       xmlGetLineNo( link ), id );
+               rc = 1;
+       } else if( '\0' == *field_name ) {
+               printf( "Line %ld: Field name is empty for <link> element in class \"%s\"\n",
+                       xmlGetLineNo( link ), id );
+               rc = 1;
+       } else if( !reltype ) {
+               printf( "Line %ld: No reltype attribute found in <link> in class \"%s\"\n",
+                       xmlGetLineNo( link ), id );
+               rc = 1;
+       } else if( !key ) {
+               printf( "Line %ld: No key attribute found in <link> in class \"%s\"\n",
+                               xmlGetLineNo( link ), id );
+               rc = 1;
+       } else if( '\0' == *key ) {
+               printf( "Line %ld: key attribute is empty for <link> element in class \"%s\"\n",
+                       xmlGetLineNo( link ), id );
+               rc = 1;
+       } else if( !classref ) {
+               printf( "Line %ld: No class attribute found in <link> in class \"%s\"\n",
+                        xmlGetLineNo( link ), id );
+               rc = 1;
+       } else if( '\0' == *classref ) {
+               printf( "Line %ld: class attribute is empty for <link> element in class \"%s\"\n",
+                       xmlGetLineNo( link ), id );
+               rc = 1;
+       } else {
+               // Add to Link list
+               Link* new_link = newLink( field_name );
+               new_link->reltype = reltype;
+               new_link->key = key;
+               new_link->classref = classref;
+               if( addLink( class, id, new_link ) )
+                       rc = 1;
+       }
+
+       return rc;
+}
+
+/**
+       @brief Translate an attribute value into a Reltype (an enum).
+       @param value The value of a reltype attribute.
+       @return The value of the attribute translated into the enum Reltype.
+*/
+static Reltype translate_reltype( const xmlChar* value ) {
+       const char* val = (char*) value;
+       Reltype reltype;
+
+       if( !val || !*val )
+               reltype = RT_NONE;
+       else if( !strcmp( val, "has_a" ) )
+               reltype = RT_HAS_A;
+       else if( !strcmp( val, "might_have" ) )
+               reltype = RT_MIGHT_HAVE;
+       else if( !strcmp( val, "has_many" ) )
+               reltype = RT_HAS_MANY;
+       else
+               reltype = RT_INVALID;
+
+       return reltype;
+}
+
+/**
+       @brief Build a list of classes, while checking for several errors.
+       @param doc Pointer to the xmlDoc loaded from the IDL.
+       @return 1 if errors found, or 0 if not.
+
+       Rules:
+       - Every child element of the root must be of the element "class".
+       - No text is allowed, other than white space, between classes.
+       - Comments are allowed (and ignored) between classes.
+*/
+static int scan_idl( xmlDocPtr doc ) {
+       int rc = 0;
+
+       xmlNodePtr child = xmlDocGetRootElement( doc )->children;
+       while( child ) {
+               char* child_name = (char*) child->name;
+               if( xmlNodeIsText( child ) ) {
+                       if( ! xmlIsBlankNode( child ) ) {
+                               // Found unexpected text.  After removing leading and
+                               // trailing white space, complain about it.
+                               xmlChar* content = xmlNodeGetContent( child );
+
+                               xmlChar* begin = content;
+                               while( *begin && isspace( *begin ) )
+                                       ++begin;
+                               if( *begin ) {
+                                       xmlChar* end = begin + strlen( (char*) begin ) - 1;
+                                       while( (isspace( *end ) ) )
+                                               --end;
+                                       end[ 1 ] = '\0';
+                               }
+
+                               printf( "Unexpected text between class elements: \"%s\"\n",
+                                       (char*) begin );
+                               xmlFree( content );
+                       }
+               } else if( !strcmp( child_name, "class" ) ) {
+                       if( register_class( child ) )
+                               rc = 1;
+               } else if( !strcmp( child_name, "comment" ) )
+                       ;  // ignore comment
+               else {
+                       printf( "Line %ld: Unexpected <%s> element under root\n",
+                               xmlGetLineNo( child ), child_name );
+                       rc = 1;
+               }
+
+               child = child->next;
+       }
+       return rc;
+}
+
+/**
+       @brief Register a class.
+       @param class Pointer to the class node.
+       @return 1 if errors found, or 0 if not.
+
+       Rules:
+       - Every class element must have an "id" attribute.
+       - A class id must not be an empty string.
+       - Every class id must be unique.
+
+       Warnings:
+       - A class id normally consists entirely of lower case letters, digits and underscores.
+       - A class id longer than 12 characters is suspiciously long.
+*/
+static int register_class( xmlNodePtr class ) {
+       int rc = 0;
+       xmlChar* id = xmlGetProp( class, (xmlChar*) "id" );
+
+       if( ! id ) {
+               printf( "Line %ld: Class has no \"id\" attribute\n", xmlGetLineNo( class ) );
+               rc = 1;
+       } else if( ! *id ) {
+               printf( "Line %ld: Class id is an empty string\n", xmlGetLineNo( class ) );
+               rc = 1;
+       } else {
+
+               // In principle a class id could contain any arbitrary characters, but in practice
+               // anything but lower case, digits, and underscores is probably a mistake.
+               const xmlChar* p = id;
+               while( *p ) {
+                       if( islower( *p ) || isdigit( *p ) || '_' == *p )
+                               ++p;
+                       else if( warn ) {
+                               printf( "Line %ld: WARNING: Dubious class id \"%s\"; not all lower case, "
+                                               "digits, and underscores\n", xmlGetLineNo( class ), (char*) id );
+                               break;
+                       }
+               }
+
+               // Warn about a suspiciously long id
+               if( warn && strlen( (char*) id ) > 12 ) {
+                       printf( "Line %ld: WARNING: Class id is unusually long: \"%s\"\n",
+                               xmlGetLineNo( class ), (char*) id );
+               }
+
+               // Add the classname to the list of classes.  If the size of
+               // the list doesn't change, then we must have a duplicate.
+               Class* entry = newClass( class );
+               unsigned long class_count = osrfHashGetCount( classes );
+               osrfHashSet( classes, entry, (char*) id );
+               if( osrfHashGetCount( classes ) == class_count ) {
+                       printf( "Line %ld: Duplicate class name \"%s\"\n",
+                               xmlGetLineNo( class ), (char*) id );
+                       rc = 1;
+               }
+               xmlFree( id );
+       }
+       return rc;
+}
+
+/**
+       @brief Add a field to a class's field list (unless the id collides with an earlier entry).
+       @param class Pointer to the current class.
+       @param id The class id.
+       @param new_field Pointer to the Field to be added.
+       @return 0 if successful, or 1 if not (probably due to a duplicate key).
+
+       If the id collides with a previous entry, we free the new Field instead of adding it
+       to the list.  If the label collides with a previous entry, we complain, but we go
+       ahead and add the Field to the list.
+
+       RULES:
+       - Each field name should be unique within the fields element.
+       - Each label should be unique within the fields element.
+*/
+static int addField( Class* class, const char* id, Field* new_field ) {
+       if( ! class || ! new_field )
+               return 1;
+
+       int rc = 0;
+       int dup_name = 0;
+
+       // See if the class has any other fields with the same name or label.
+       const Field* old_field = class->fields;
+       while( old_field ) {
+
+               // Compare the ids
+               if( !strcmp( (char*) old_field->name, (char*) new_field->name ) ) {
+                       printf( "Duplicate field name \"%s\" in class \"%s\"\n",
+                               (char*) new_field->name, id );
+                       dup_name = 1;
+                       rc = 1;
+                       break;
+               }
+
+               // Compare the labels. if they're both non-empty
+               if( old_field->label && *old_field->label
+                && new_field->label && *new_field->label
+                && !strcmp( (char*) old_field->label, (char*) new_field->label )) {
+                       printf( "Duplicate labels \"%s\" in class \"%s\"\n",
+                               (char*) old_field->label, id );
+                       rc = 1;
+               }
+
+               old_field = old_field->next;
+       }
+
+       if( dup_name ) {
+               free( new_field );
+       } else {
+               new_field->next = class->fields;
+               class->fields = new_field;
+       }
+
+       return rc;
+}
+
+/**
+       @brief Add a Link to the Link list of a specified Class (unless it's a duplicate).
+       @param class Pointer to the Class to whose list to add the Link.
+       @param id Class id.
+       @param new_link Pointer to the Link to be added.
+       @return 0 if successful, or 1 if not (probably due to a duplicate).
+
+       If there's already a Link in the list with the same field name, free the new Link
+       instead of adding it.
+*/
+static int addLink( Class* class, const char* id, Link* new_link ) {
+       if( ! class || ! new_link )
+               return 1;
+
+       int rc = 0;
+       int dup_name = 0;
+
+       // See if the class has any other links with the same field
+       const Link* old_link = class->links;
+       while( old_link ) {
+
+               if( !strcmp( (char*) old_link->field, (char*) new_link->field ) ) {
+                       printf( "Duplicate field name \"%s\" in links of class \"%s\"\n",
+                               (char*) old_link->field, id );
+                       rc = 1;
+                       dup_name = 1;
+                       break;
+               }
+
+               old_link = old_link->next;
+       }
+
+       if( dup_name ) {
+               freeLink( new_link );
+       } else {
+               // Add to the linked list
+               new_link->next = class->links;
+               class->links = new_link;
+       }
+
+       return rc;
+}
+
+/**
+       @brief Create and initialize a new Class.
+       @param node Pointer to the XML node for a class element.
+       @return Pointer to the newly created Class.
+
+       The calling code is responsible for freeing the Class by calling freeClass().  In practice
+       this happens automagically when we free the osrfHash classes.
+*/
+static Class* newClass( xmlNodePtr node ) {
+       Class* class = safe_malloc( sizeof( Class ) );
+       class->node = node;
+       class->loaded = 0;
+       class->is_virtual = 0;
+       xmlFree( class->primary );
+       class->fields = NULL;
+       class->links = NULL;
+       return class;
+}
+
+/**
+       @brief Free a Class and everything it owns.
+       @param key The class id (not used).
+       @param p A pointer to the Class to be freed, cast to a void pointer.
+
+       This function is designed to be a freeItem callback for an osrfHash.
+*/
+static void freeClass( char* key, void* p ) {
+       Class* class = p;
+
+       // Free the linked list of Fields
+       Field* next_field = NULL;
+       Field* field = class->fields;
+       while( field ) {
+               next_field = field->next;
+               freeField( field );
+               field = next_field;
+       }
+
+       // Free the linked list of Links
+       Link* next_link = NULL;
+       Link* link = class->links;
+       while( link ) {
+               next_link = link->next;
+               freeLink( link );
+               link = next_link;
+       }
+
+       free( class );
+}
+
+/**
+       @brief Allocate and initialize a Field.
+       @param name Field name.
+       @return Pointer to a new Field.
+
+       It is the responsibility of the caller to free the Field by calling freeField().
+*/
+static Field* newField( xmlChar* name ) {
+       Field* field = safe_malloc( sizeof( Field ) );
+       field->next         = NULL;
+       field->name         = name;
+       field->is_virtual   = 0;
+       field->label        = NULL;
+       field->datatype     = DT_NONE;
+       return field;
+}
+
+/**
+       @brief Free a Field and everything in it.
+       @param field Pointer to the Field to be freed.
+*/
+static void freeField( Field* field ) {
+       if( field ) {
+               xmlFree( field->name );
+               if( field->label )
+                       xmlFree( field->label );
+               free( field );
+       }
+}
+
+/**
+       @brief Allocate and initialize a Link.
+       @param field Field name.
+       @return Pointer to a new Link.
+
+       It is the responsibility of the caller to free the Link by calling freeLink().
+*/
+static Link* newLink( xmlChar* field ) {
+       Link* link = safe_malloc( sizeof( Link ) );
+       link->next         = NULL;
+       link->field        = field;
+       link->reltype      = RT_NONE;
+       link->key          = NULL;
+       link->classref     = NULL;
+       return link;
+}
+
+/**
+       @brief Free a Link and everything it owns.
+       @param link Pointer to the Link to be freed.
+*/
+static void freeLink( Link* link ) {
+       if( link ) {
+               xmlFree( link->field );
+               xmlFree( link->key );
+               xmlFree( link->classref );
+               free( link );
+       }
+}
diff --git a/src/c/oils_cstore.c b/src/c/oils_cstore.c
new file mode 100644 (file)
index 0000000..5014605
--- /dev/null
@@ -0,0 +1,338 @@
+/**
+       @file oils_cstore.c
+       @brief As a server, perform database operations at the request of clients.
+*/
+
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <dbi/dbi.h>
+#include "opensrf/utils.h"
+#include "opensrf/log.h"
+#include "opensrf/osrf_application.h"
+#include "openils/oils_utils.h"
+#include "openils/oils_sql.h"
+
+static dbi_conn writehandle; /* our MASTER db connection */
+static dbi_conn dbhandle; /* our CURRENT db connection */
+//static osrfHash * readHandles;
+
+static const int enforce_pcrud = 0;     // Boolean
+static const char modulename[] = "open-ils.cstore";
+
+/**
+       @brief Disconnect from the database.
+
+       This function is called when the server drone is about to terminate.
+*/
+void osrfAppChildExit( void ) {
+       osrfLogDebug(OSRF_LOG_MARK, "Child is exiting, disconnecting from database...");
+
+       int same = 0;
+       if (writehandle == dbhandle)
+               same = 1;
+
+       if (writehandle) {
+               dbi_conn_query(writehandle, "ROLLBACK;");
+               dbi_conn_close(writehandle);
+               writehandle = NULL;
+       }
+       if (dbhandle && !same)
+               dbi_conn_close(dbhandle);
+
+       // XXX add cleanup of readHandles whenever that gets used
+
+       return;
+}
+
+/**
+       @brief Initialize the application.
+       @return Zero if successful, or non-zero if not.
+
+       Load the IDL file into an internal data structure for future reference.  Each non-virtual
+       class in the IDL corresponds to a table or view in the database, or to a subquery defined
+       in the IDL.  Ignore all virtual tables and virtual fields.
+
+       Register a number of methods, some of them general-purpose and others specific for
+       particular classes.
+
+       The name of the application is given by the MODULENAME macro, whose value depends on
+       conditional compilation.  The method names also incorporate MODULENAME, followed by a
+       dot, as a prefix.
+
+       The general-purpose methods are as follows (minus their MODULENAME prefixes):
+
+       - json_query
+       - transaction.begin
+       - transaction.commit
+       - transaction.rollback
+       - savepoint.set
+       - savepoint.release
+       - savepoint.rollback
+       - set_audit_info
+
+       For each non-virtual class, create up to eight class-specific methods:
+
+       - create    (not for readonly classes)
+       - retrieve
+       - update    (not for readonly classes)
+       - delete    (not for readonly classes
+       - search    (atomic and non-atomic versions)
+       - id_list   (atomic and non-atomic versions)
+
+       The full method names follow the pattern "MODULENAME.direct.XXX.method_type", where XXX
+       is the fieldmapper name from the IDL, with every run of one or more consecutive colons
+       replaced by a period.  In addition, the names of atomic methods have a suffix of ".atomic".
+
+       This function is called when the registering the application, and is executed by the
+       listener before spawning the drones.
+*/
+int osrfAppInitialize( void ) {
+
+       osrfLogInfo(OSRF_LOG_MARK, "Initializing the CStore Server...");
+       osrfLogInfo(OSRF_LOG_MARK, "Finding XML file...");
+
+       // Load the IDL into memory
+       if ( !oilsIDLInit( osrf_settings_host_value( "/IDL" )))
+               return 1; /* return non-zero to indicate error */
+
+       // Open the database temporarily.  Look up the datatypes of all
+       // the non-virtual fields and record them with the IDL data.
+       dbi_conn handle = oilsConnectDB( modulename );
+       if( !handle )
+               return -1;
+       else if( oilsExtendIDL( handle )) {
+               osrfLogError( OSRF_LOG_MARK, "Error extending the IDL" );
+               return -1;
+       }
+       dbi_conn_close( handle );
+
+       // Get the maximum flesh depth from the settings
+       char* md = osrf_settings_host_value(
+               "/apps/%s/app_settings/max_query_recursion", modulename );
+       int max_flesh_depth = 100;
+       if( md )
+               max_flesh_depth = atoi( md );
+       if( max_flesh_depth < 0 )
+               max_flesh_depth = 1;
+       else if( max_flesh_depth > 1000 )
+               max_flesh_depth = 1000;
+
+       oilsSetSQLOptions( modulename, enforce_pcrud, max_flesh_depth );
+
+       // Now register all the methods
+       growing_buffer* method_name = buffer_init(64);
+
+       // Generic search thingy
+       buffer_add( method_name, modulename );
+       buffer_add( method_name, ".json_query" );
+       osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
+               "doJSONSearch", "", 1, OSRF_METHOD_STREAMING );
+
+       // Next we register all the transaction and savepoint methods
+       buffer_reset(method_name);
+       OSRF_BUFFER_ADD(method_name, modulename );
+       OSRF_BUFFER_ADD(method_name, ".transaction.begin");
+       osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
+                       "beginTransaction", "", 0, 0 );
+
+       buffer_reset(method_name);
+       OSRF_BUFFER_ADD(method_name, modulename );
+       OSRF_BUFFER_ADD(method_name, ".transaction.commit");
+       osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+                       "commitTransaction", "", 0, 0 );
+
+       buffer_reset(method_name);
+       OSRF_BUFFER_ADD(method_name, modulename );
+       OSRF_BUFFER_ADD(method_name, ".transaction.rollback");
+       osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+                       "rollbackTransaction", "", 0, 0 );
+
+       buffer_reset(method_name);
+       OSRF_BUFFER_ADD(method_name, modulename );
+       OSRF_BUFFER_ADD(method_name, ".savepoint.set");
+       osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+                       "setSavepoint", "", 1, 0 );
+
+       buffer_reset(method_name);
+       OSRF_BUFFER_ADD(method_name, modulename );
+       OSRF_BUFFER_ADD(method_name, ".savepoint.release");
+       osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+                       "releaseSavepoint", "", 1, 0 );
+
+       buffer_reset(method_name);
+       OSRF_BUFFER_ADD(method_name, modulename );
+       OSRF_BUFFER_ADD(method_name, ".savepoint.rollback");
+       osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+                       "rollbackSavepoint", "", 1, 0 );
+
+       buffer_reset(method_name);
+       OSRF_BUFFER_ADD(method_name, modulename );
+       OSRF_BUFFER_ADD(method_name, ".set_audit_info");
+       osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+                       "setAuditInfo", "", 3, 0 );
+
+       static const char* global_method[] = {
+               "create",
+               "retrieve",
+               "update",
+               "delete",
+               "search",
+               "id_list"
+       };
+       const int global_method_count
+               = sizeof( global_method ) / sizeof ( global_method[0] );
+
+       unsigned long class_count = osrfHashGetCount( oilsIDL() );
+       osrfLogDebug(OSRF_LOG_MARK, "%lu classes loaded", class_count );
+       osrfLogDebug(OSRF_LOG_MARK,
+               "At most %lu methods will be generated",
+               (unsigned long) (class_count * global_method_count) );
+
+       osrfHashIterator* class_itr = osrfNewHashIterator( oilsIDL() );
+       osrfHash* idlClass = NULL;
+
+       // For each class in the IDL...
+       while( (idlClass = osrfHashIteratorNext( class_itr ) ) ) {
+
+               const char* classname = osrfHashIteratorKey( class_itr );
+               osrfLogInfo(OSRF_LOG_MARK, "Generating class methods for %s", classname);
+
+               if (!osrfStringArrayContains( osrfHashGet(idlClass, "controller"), modulename )) {
+                       osrfLogInfo(OSRF_LOG_MARK, "%s is not listed as a controller for %s, moving on",
+                                       modulename, classname);
+                       continue;
+               }
+
+               if ( str_is_true( osrfHashGet(idlClass, "virtual") ) ) {
+                       osrfLogDebug(OSRF_LOG_MARK, "Class %s is virtual, skipping", classname );
+                       continue;
+               }
+
+               // Look up some other attributes of the current class
+               const char* idlClass_fieldmapper = osrfHashGet(idlClass, "fieldmapper");
+               if( !idlClass_fieldmapper ) {
+                       osrfLogDebug( OSRF_LOG_MARK, "Skipping class \"%s\"; no fieldmapper in IDL",
+                                       classname );
+                       continue;
+               }
+
+               const char* readonly = osrfHashGet(idlClass, "readonly");
+
+               int i;
+               for( i = 0; i < global_method_count; ++i ) {  // for each global method
+                       const char* method_type = global_method[ i ];
+                       osrfLogDebug(OSRF_LOG_MARK,
+                               "Using files to build %s class methods for %s", method_type, classname);
+
+                       // No create, update, or delete methods for a readonly class
+                       if ( str_is_true( readonly )
+                               && ( *method_type == 'c' || *method_type == 'u' || *method_type == 'd') )
+                               continue;
+
+                       buffer_reset( method_name );
+
+                       // Build the method name: MODULENAME.MODULENAME.direct.XXX.method_type
+                       // where XXX is the fieldmapper name from the IDL, with every run of
+                       // one or more consecutive colons replaced by a period.
+                       char* st_tmp = NULL;
+                       char* part = NULL;
+                       char* _fm = strdup( idlClass_fieldmapper );
+                       part = strtok_r(_fm, ":", &st_tmp);
+
+                       buffer_fadd(method_name, "%s.direct.%s", modulename, part);
+
+                       while ((part = strtok_r(NULL, ":", &st_tmp))) {
+                               OSRF_BUFFER_ADD_CHAR(method_name, '.');
+                               OSRF_BUFFER_ADD(method_name, part);
+                       }
+                       OSRF_BUFFER_ADD_CHAR(method_name, '.');
+                       OSRF_BUFFER_ADD(method_name, method_type);
+                       free(_fm);
+
+                       // For an id_list or search method we specify the OSRF_METHOD_STREAMING option.
+                       // The consequence is that we implicitly create an atomic method in addition to
+                       // the usual non-atomic method.
+                       int flags = 0;
+                       if (*method_type == 'i' || *method_type == 's') {  // id_list or search
+                               flags = flags | OSRF_METHOD_STREAMING;
+                       }
+
+                       osrfHash* method_meta = osrfNewHash();
+                       osrfHashSet( method_meta, idlClass, "class");
+                       osrfHashSet( method_meta, buffer_data( method_name ), "methodname" );
+                       osrfHashSet( method_meta, strdup(method_type), "methodtype" );
+
+                       // Register the method, with a pointer to an osrfHash to tell the method
+                       // its name, type, and class.
+                       osrfAppRegisterExtendedMethod(
+                               modulename,
+                               OSRF_BUFFER_C_STR( method_name ),
+                               "dispatchCRUDMethod",
+                               "",
+                               1,
+                               flags,
+                               (void*)method_meta
+                       );
+
+               } // end for each global method
+       } // end for each class in IDL
+
+       buffer_free( method_name );
+       osrfHashIteratorFree( class_itr );
+
+       return 0;
+}
+
+/**
+       @brief Initialize a server drone.
+       @return Zero if successful, -1 if not.
+
+       Connect to the database.
+
+       This function is called by a server drone shortly after it is spawned by the listener.
+*/
+int osrfAppChildInit( void ) {
+
+       writehandle = oilsConnectDB( modulename );
+       if( !writehandle )
+               return -1;
+
+       oilsSetDBConnection( writehandle );
+
+       return 0;
+}
+
+/**
+       @brief Implement the class-specific methods.
+       @param ctx Pointer to the method context.
+       @return Zero if successful, or -1 if not.
+
+       Branch on the method type: create, retrieve, update, delete, search, or id_list.
+
+       The method parameters and the type of value returned to the client depend on the method
+       type.
+*/
+int dispatchCRUDMethod( osrfMethodContext* ctx ) {
+
+       // Get the method type, then can branch on it
+       osrfHash* method_meta = (osrfHash*) ctx->method->userData;
+       const char* methodtype = osrfHashGet( method_meta, "methodtype" );
+
+       if( !strcmp( methodtype, "create" ))
+               return doCreate( ctx );
+       else if( !strcmp(methodtype, "retrieve" ))
+               return doRetrieve( ctx );
+       else if( !strcmp(methodtype, "update" ))
+               return doUpdate( ctx );
+       else if( !strcmp(methodtype, "delete" ))
+               return doDelete( ctx );
+       else if( !strcmp(methodtype, "search" ))
+               return doSearch( ctx );
+       else if( !strcmp(methodtype, "id_list" ))
+               return doIdList( ctx );
+       else {
+               osrfAppRespondComplete( ctx, NULL );      // should be unreachable...
+               return 0;
+       }
+}
diff --git a/src/c/oils_event.c b/src/c/oils_event.c
new file mode 100644 (file)
index 0000000..a39b45b
--- /dev/null
@@ -0,0 +1,343 @@
+#include "openils/oils_event.h"
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include "opensrf/osrf_settings.h"
+
+const char default_lang[] = "en-US";
+
+static void _oilsEventParseEvents();
+static const char* lookup_desc( const char* lang, const char* code );
+
+// The following two osrfHashes are created when we
+// create the first osrfEvent, and are never freed.
+
+/**
+       @brief Lookup store mapping event names to event numbers.
+
+       - Key: textcode from the events config file.
+       - Data: numeric code (as a string) from the events config file.
+*/
+static osrfHash* _oilsEventEvents = NULL;
+
+/**
+       @brief Lookup store mapping event numbers to descriptive text.
+
+       - Key: numeric code (as a string) of the event.
+       - Data: another layer of lookup, as follows:
+               - Key: numeric code (as a string) of the event.
+               - Data: text message describing the event.
+*/
+static osrfHash* _oilsEventDescriptions = NULL;
+
+/**
+       @brief Allocate and initialize a new oilsEvent.
+       @param file The name of the source file where oilsNewEvent is called.
+       @param line The line number in the source code where oilsNewEvent is called.
+       @param event A name or label for the event.
+       @return Pointer to the newly allocated oilsEvent.
+
+       The first two parameters are normally passed as the OSRF_LOG_MARK macro.
+
+       The calling code is responsible for freeing the oilsEvent by calling oilsEventFree().
+*/
+oilsEvent* oilsNewEvent( const char* file, int line, const char* event ) {
+       if(!event) return NULL;
+
+       osrfLogInfo(OSRF_LOG_MARK, "Creating new event: %s", event);
+
+       if(!_oilsEventEvents)
+               _oilsEventParseEvents();
+
+       oilsEvent* evt = safe_malloc( sizeof(oilsEvent) );
+       evt->event = strdup(event);
+       evt->perm = NULL;
+       evt->permloc = -1;
+       evt->payload = NULL;
+       evt->json = NULL;
+
+       if(file)
+               evt->file = strdup(file);
+       else
+               evt->file = strdup( "" );
+
+       evt->line = line;
+       return evt;
+}
+
+/**
+       @brief Allocate and initialize a new oilsEvent with a payload.
+       @param file The name of the source file where oilsNewEvent is called.
+       @param line The line number in the source code where oilsNewEvent is called.
+       @param event A name or label for the event.
+       @param payload The payload, of which a copy will be incorporated into the oilsEvent.
+       @return Pointer to the newly allocated oilsEvent.
+
+       The first two parameters are normally passed as the OSRF_LOG_MARK macro.
+
+       The calling code is responsible for freeing the oilsEvent by calling oilsEventFree().
+*/
+oilsEvent* oilsNewEvent2( const char* file, int line, const char* event,
+               const jsonObject* payload ) {
+       oilsEvent* evt = oilsNewEvent(file, line, event);
+
+       if(payload)
+               evt->payload = jsonObjectClone(payload);
+
+       return evt;
+}
+
+/**
+       @brief Create a new oilsEvent with a permission and a permission location.
+       @param file The name of the source file where oilsNewEvent is called.
+       @param line The line number in the source code where oilsNewEvent is called.
+       @param event A name or label for the event.
+       @param perm The permission name.
+       @param permloc The permission location.
+       @return Pointer to the newly allocated oilsEvent.
+
+       The first two parameters are normally passed as the OSRF_LOG_MARK macro.
+
+       The calling code is responsible for freeing the oilsEvent by calling oilsEventFree().
+*/
+oilsEvent* oilsNewEvent3( const char* file, int line, const char* event,
+               const char* perm, int permloc ) {
+       oilsEvent* evt = oilsNewEvent(file, line, event);
+       if(perm) {
+               evt->perm = strdup(perm);
+               evt->permloc = permloc;
+       }
+       return evt;
+}
+
+/**
+       @brief Create a new oilsEvent with a permission and a permission location.
+       @param file The name of the source file where oilsNewEvent is called.
+       @param line The line number in the source code where oilsNewEvent is called.
+       @param event A name or label for the event.
+       @param perm The permission name.
+       @param permloc The permission location.
+       @param payload Pointer to the payload.
+       @return Pointer to the newly allocated oilsEvent.
+
+       The first two parameters are normally passed as the OSRF_LOG_MARK macro.
+
+       The calling code is responsible for freeing the oilsEvent by calling oilsEventFree().
+*/
+oilsEvent* oilsNewEvent4( const char* file, int line, const char* event,
+               const char* perm, int permloc, const jsonObject* payload ) {
+       oilsEvent* evt = oilsNewEvent3( file, line, event, perm, permloc );
+
+       if(payload)
+               evt->payload = jsonObjectClone(payload);
+
+       return evt;
+}
+
+/**
+       @brief Set the permission and permission location of an oilsEvent.
+       @param event Pointer the oilsEvent whose permission and permission location are to be set.
+       @param perm The permission name.
+       @param permloc The permission location.
+*/
+void oilsEventSetPermission( oilsEvent* event, const char* perm, int permloc ) {
+       if(!(event && perm)) return;
+
+       if(event->perm)
+               free(event->perm);
+
+       event->perm = strdup(perm);
+       event->permloc = permloc;
+}
+
+/**
+       @brief Install a payload in an oilsEvent.
+       @param event The oilsEvent in which the payload is to be installed.
+       @param payload The payload, a copy of which will be installed in the oilsEvent.
+
+       If @a payload is NULL, install a JSON_NULL as the payload.
+*/
+void oilsEventSetPayload( oilsEvent* event, const jsonObject* payload ) {
+       if(!(event && payload)) return;
+
+       if(event->payload)
+               jsonObjectFree(event->payload);
+
+       event->payload = jsonObjectClone(payload);
+}
+
+/**
+       @brief Free an OilsEvent.
+       @param event Pointer to the oilsEvent to be freed.
+*/
+void oilsEventFree( oilsEvent* event ) {
+       if(!event) return;
+       free(event->event);
+       free(event->perm);
+       free(event->file);
+
+       // If present, the jsonObject to which event->json will include a pointer to
+       // event->payload.  Hence we must avoid trying to free the payload twice.
+       if(event->json)
+               jsonObjectFree(event->json);
+       else
+               jsonObjectFree(event->payload);
+
+       free(event);
+}
+
+/**
+       @brief Package the contents of an oilsEvent into a jsonObject.
+       @param event Pointer to the oilsEvent whose contents are to be packaged.
+       @return Pointer to the newly created jsonObject if successful, or NULL if not.
+
+       The jsonObject will include a textual description of the event, as loaded from the
+       events file.  Although the events file may include text in multiple languages,
+       oilsEventToJSON() uses only those marked as "en-US".
+
+       A pointer to the resulting jsonObject will be stored in the oilsEvent.  Hence the calling
+       code should @em not free the returned jsonObject directly.  It will be freed by
+       oilsEventFree().
+*/
+jsonObject* oilsEventToJSON( oilsEvent* event ) {
+       if(!event) return NULL;
+
+       char* code = osrfHashGet( _oilsEventEvents, event->event );
+       if(!code) {
+               osrfLogError(OSRF_LOG_MARK, "No such event name: %s", event->event );
+               return NULL;
+       }
+
+       // Look up the text message corresponding the code, preferably in the right language.
+       const char* lang = osrf_message_get_last_locale();
+       const char* desc = lookup_desc( lang, code );
+       if( !desc && strcmp( lang, default_lang ) )    // No luck?
+               desc = lookup_desc( default_lang, code );  // Try the default language
+
+       if( !desc )
+               desc = "";  // Not found?  Default to an empty string.
+
+       jsonObject* json = jsonNewObject(NULL);
+       jsonObjectSetKey( json, "ilsevent", jsonNewNumberObject(atoi(code)) );
+       jsonObjectSetKey( json, "textcode", jsonNewObject(event->event) );
+       jsonObjectSetKey( json, "desc", jsonNewObject(desc) );
+       jsonObjectSetKey( json, "pid", jsonNewNumberObject(getpid()) );
+
+       char buf[256] = "";
+       snprintf(buf, sizeof(buf), "%s:%d", event->file, event->line);
+       jsonObjectSetKey( json, "stacktrace", jsonNewObject(buf) );
+
+       if(event->perm)
+               jsonObjectSetKey( json, "ilsperm", jsonNewObject(event->perm) );
+
+       if(event->permloc != -1)
+               jsonObjectSetKey( json, "ilspermloc", jsonNewNumberObject(event->permloc) );
+
+       if(event->payload)
+               jsonObjectSetKey( json, "payload", event->payload );
+
+       if(event->json)
+               jsonObjectFree(event->json);
+
+       event->json = json;
+       return json;
+}
+
+/**
+       @brief Lookup up the descriptive text, in a given language, for a given event code.
+       @param lang The language (a.k.a. locale) of the desired message.
+       @param code The numeric code for the event, as a string.
+       return The corresponding descriptive text if found, or NULL if not.
+
+       The lookup has two stages.  First we look up the language, and then within that
+       language we look up the code.
+*/
+static const char* lookup_desc( const char* lang, const char* code ) {
+       // Search for the right language
+       const char* desc = NULL;
+       osrfHash* lang_hash = osrfHashGet( _oilsEventDescriptions, lang );
+       if( lang_hash ) {
+               // Within that language, search for the right message
+               osrfLogDebug( OSRF_LOG_MARK, "Loaded event lang hash for %s", lang );
+               desc = osrfHashGet( lang_hash, code );
+       }
+
+       if( desc )
+               osrfLogDebug( OSRF_LOG_MARK, "Found event description %s", desc );
+       else
+               osrfLogDebug( OSRF_LOG_MARK, "Event description not found for code %s", code );
+
+       return desc;
+}
+
+/**
+       @brief Parse and load the events file.
+
+       Get the name of the events file from previously loaded settings.  Open it and load
+       it into an xmlDoc.  Based on the contents of the xmlDoc, build two osrfHashes: one to
+       map event names to event numbers, and another to map event numbers to descriptive
+       text messages (actually one such hash for each supported language).
+*/
+static void _oilsEventParseEvents() {
+
+       char* xml = osrf_settings_host_value("/ils_events");
+
+       if(!xml) {
+               osrfLogError(OSRF_LOG_MARK, "Unable to find ILS Events file: %s", xml);
+               return;
+       }
+
+       xmlDocPtr doc = xmlParseFile(xml);
+       free(xml);
+       int success = 0;
+       _oilsEventEvents = osrfNewHash();
+       _oilsEventDescriptions = osrfNewHash();
+
+       if( doc ) {
+               xmlNodePtr root = xmlDocGetRootElement(doc);
+               if( root ) {
+                       xmlNodePtr child = root->children;
+                       while( child ) {
+                               if( !strcmp((char*) child->name, "event") ) {
+                                       xmlChar* code = xmlGetProp( child, BAD_CAST "code");
+                                       xmlChar* textcode = xmlGetProp( child, BAD_CAST "textcode");
+                                       if( code && textcode ) {
+                                               osrfHashSet( _oilsEventEvents, code, (char*) textcode );
+                                               success = 1;
+                                       }
+
+                                       /* here we collect all of the <desc> nodes on the event
+                                        * element and store them based on the xml:lang attribute
+                                        */
+                                       xmlNodePtr desc = child->children;
+                                       while(desc) {
+                                               if( !strcmp((char*) desc->name, "desc") ) {
+                                                       xmlChar* lang = xmlGetProp( desc, BAD_CAST "lang");
+                                                       if(lang) {
+                                                               osrfLogInternal( OSRF_LOG_MARK,
+                                                                       "Loaded event lang: %s", (char*) lang );
+                                                               osrfHash* langHash = osrfHashGet(
+                                                                       _oilsEventDescriptions, (char*) lang);
+                                                               if(!langHash) {
+                                                                       langHash = osrfNewHash();
+                                                                       osrfHashSet(_oilsEventDescriptions, langHash, (char*) lang);
+                                                               }
+                                                               char* content;
+                                                               if( desc->children
+                                                                       && (content = (char*) desc->children->content) ) {
+                                                                       osrfLogInternal( OSRF_LOG_MARK,
+                                                                               "Loaded event desc: %s", content);
+                                                                       osrfHashSet( langHash, content, (char*) code );
+                                                               }
+                                                       }
+                                               }
+                                               desc = desc->next;
+                                       }
+                               }
+                               child = child->next;
+                       }
+               }
+       }
+
+       if(!success)
+               osrfLogError(OSRF_LOG_MARK, " ! Unable to parse events file: %s", xml );
+}
diff --git a/src/c/oils_idl-core.c b/src/c/oils_idl-core.c
new file mode 100644 (file)
index 0000000..5d432ab
--- /dev/null
@@ -0,0 +1,550 @@
+#include "openils/oils_idl.h"
+/*
+ * vim:noet:ts=4:
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <libxml/globals.h>
+#include <libxml/xmlerror.h>
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include <libxml/debugXML.h>
+#include <libxml/xmlmemory.h>
+
+#define PERSIST_NS "http://open-ils.org/spec/opensrf/IDL/persistence/v1"
+#define OBJECT_NS "http://open-ils.org/spec/opensrf/IDL/objects/v1"
+#define BASE_NS "http://opensrf.org/spec/IDL/base/v1"
+#define REPORTER_NS "http://open-ils.org/spec/opensrf/IDL/reporter/v1"
+#define PERM_NS "http://open-ils.org/spec/opensrf/IDL/permacrud/v1"
+
+static xmlDocPtr idlDoc = NULL; // parse and store the IDL here
+
+/* parse and store the IDL here */
+static osrfHash* idlHash;
+
+static void add_std_fld( osrfHash* fields_hash, const char* field_name, unsigned pos );
+osrfHash* oilsIDL(void) { return idlHash; }
+osrfHash* oilsIDLInit( const char* idl_filename ) {
+
+       if (idlHash) return idlHash;
+
+       char* prop_str = NULL;
+
+       idlHash = osrfNewHash();
+       osrfHash* class_def_hash = NULL;
+
+       osrfLogInfo(OSRF_LOG_MARK, "Parsing the IDL XML...");
+       idlDoc = xmlReadFile( idl_filename, NULL, XML_PARSE_XINCLUDE );
+       
+       if (!idlDoc) {
+               osrfLogError(OSRF_LOG_MARK, "Could not load or parse the IDL XML file!");
+               return NULL;
+       }
+
+       osrfLogDebug(OSRF_LOG_MARK, "Initializing the Fieldmapper IDL...");
+
+       xmlNodePtr docRoot = xmlDocGetRootElement(idlDoc);
+       xmlNodePtr kid = docRoot->children;
+       while (kid) {
+               if (!strcmp( (char*)kid->name, "class" )) {
+
+                       class_def_hash = osrfNewHash();
+                       char* current_class_name = (char*) xmlGetProp(kid, BAD_CAST "id");
+                       
+                       osrfHashSet( class_def_hash, current_class_name, "classname" );
+                       osrfHashSet( class_def_hash, xmlGetNsProp(kid, BAD_CAST "fieldmapper", BAD_CAST OBJECT_NS), "fieldmapper" );
+                       osrfHashSet( class_def_hash, xmlGetNsProp(kid, BAD_CAST "readonly", BAD_CAST PERSIST_NS), "readonly" );
+
+                       osrfHashSet( idlHash, class_def_hash, current_class_name );
+
+                       if ((prop_str = (char*)xmlGetNsProp(kid, BAD_CAST "tablename", BAD_CAST PERSIST_NS))) {
+                               osrfLogDebug(OSRF_LOG_MARK, "Using table '%s' for class %s", prop_str, current_class_name );
+                               osrfHashSet(
+                                       class_def_hash,
+                                       prop_str,
+                                       "tablename"
+                               );
+                       }
+
+                       if ((prop_str = (char*)xmlGetNsProp(kid, BAD_CAST "restrict_primary", BAD_CAST PERSIST_NS))) {
+                               osrfLogDebug(OSRF_LOG_MARK, "Delete restriction policy set at '%s' for pkey of class %s", prop_str, current_class_name );
+                               osrfHashSet(
+                                       class_def_hash,
+                                       prop_str,
+                                       "restrict_primary"
+                               );
+                       }
+
+                       if ((prop_str = (char*)xmlGetNsProp(kid, BAD_CAST "virtual", BAD_CAST PERSIST_NS))) {
+                               osrfHashSet(
+                                       class_def_hash,
+                                       prop_str,
+                                       "virtual"
+                               );
+                       }
+
+                       // Tokenize controller attribute into an osrfStringArray
+                       prop_str = (char*) xmlGetProp(kid, BAD_CAST "controller");
+                       if( prop_str )
+                               osrfLogDebug(OSRF_LOG_MARK, "Controller list is %s", prop_str );
+                       osrfStringArray* controller = osrfStringArrayTokenize( prop_str, ' ' );
+                       xmlFree( prop_str );
+                       osrfHashSet( class_def_hash, controller, "controller");
+
+                       osrfHash* current_links_hash = osrfNewHash();
+                       osrfHash* current_fields_hash = osrfNewHash();
+
+                       osrfHashSet( class_def_hash, current_fields_hash, "fields" );
+                       osrfHashSet( class_def_hash, current_links_hash, "links" );
+
+                       xmlNodePtr _cur = kid->children;
+
+                       while (_cur) {
+
+                               if (!strcmp( (char*)_cur->name, "fields" )) {
+
+                                       if( (prop_str = (char*)xmlGetNsProp(_cur, BAD_CAST "primary", BAD_CAST PERSIST_NS)) ) {
+                                               osrfHashSet(
+                                                       class_def_hash,
+                                                       prop_str,
+                                                       "primarykey"
+                                               );
+                                       }
+
+                                       if( (prop_str = (char*)xmlGetNsProp(_cur, BAD_CAST "sequence", BAD_CAST PERSIST_NS)) ) {
+                                               osrfHashSet(
+                                                       class_def_hash,
+                                                       prop_str,
+                                                       "sequence"
+                                               );
+                                       }
+
+                                       unsigned int array_pos = 0;
+                                       char array_pos_buf[ 7 ];  // For up to 1,000,000 fields per class
+
+                                       xmlNodePtr _f = _cur->children;
+                                       while(_f) {
+                                               if (strcmp( (char*)_f->name, "field" )) {
+                                                       _f = _f->next;
+                                                       continue;
+                                               }
+
+                                               // Get the field name.  If it's one of the three standard
+                                               // fields that we always generate, ignore it.
+                                               char* field_name = (char*)xmlGetProp(_f, BAD_CAST "name");
+                                               if( field_name ) {
+                                                       osrfLogDebug(OSRF_LOG_MARK, 
+                                                                       "Found field %s for class %s", field_name, current_class_name );
+                                                       if(    !strcmp( field_name, "isnew" )
+                                                               || !strcmp( field_name, "ischanged" )
+                                                               || !strcmp( field_name, "isdeleted" ) ) {
+                                                               free( field_name );
+                                                               _f = _f->next;
+                                                               continue;
+                                                       }
+                                               } else {
+                                                       osrfLogDebug(OSRF_LOG_MARK,
+                                                                       "Found field with no name for class %s", current_class_name );
+                                                       _f = _f->next;
+                                                       continue;
+                                               }
+                                               osrfHash* field_def_hash = osrfNewHash();
+
+                                               // Insert array_position
+                                               snprintf( array_pos_buf, sizeof( array_pos_buf ), "%u", array_pos++ );
+                                               osrfHashSet( field_def_hash, strdup( array_pos_buf ), "array_position" );
+
+                                               // Tokenize suppress_controller attribute into an osrfStringArray
+                                               if( (prop_str = (char*)xmlGetProp(_f, BAD_CAST "suppress_controller")) ) {
+                                                       osrfLogDebug(OSRF_LOG_MARK, "Controller suppression list is %s", prop_str );
+                                                       osrfStringArray* controller = osrfStringArrayTokenize( prop_str, ' ' );
+                                                       osrfHashSet( field_def_hash, controller, "suppress_controller");
+                                               }
+
+                                               if( (prop_str = (char*)xmlGetNsProp(_f, BAD_CAST "i18n", BAD_CAST PERSIST_NS)) ) {
+                                                       osrfHashSet(
+                                                               field_def_hash,
+                                                               prop_str,
+                                                               "i18n"
+                                                       );
+                                               }
+
+                                               if( (prop_str = (char*)xmlGetNsProp(_f, BAD_CAST "virtual", BAD_CAST PERSIST_NS)) ) {
+                                                       osrfHashSet(
+                                                               field_def_hash,
+                                                               prop_str,
+                                                               "virtual"
+                                                       );
+                                               } else {   // default to virtual
+                                                       osrfHashSet(
+                                                               field_def_hash,
+                                                               "false",
+                                                               "virtual"
+                                                       );
+                                               }
+
+                                               if( (prop_str = (char*)xmlGetNsProp(_f, BAD_CAST "primitive", BAD_CAST PERSIST_NS)) ) {
+                                                       osrfHashSet(
+                                                               field_def_hash,
+                                                               prop_str,
+                                                               "primitive"
+                                                       );
+                                               }
+
+                                               osrfHashSet( field_def_hash, field_name, "name" );
+                                               osrfHashSet(
+                                                       current_fields_hash,
+                                                       field_def_hash,
+                                                       field_name
+                                               );
+                                               _f = _f->next;
+                                       }
+
+                                       // Create three standard, stereotyped virtual fields for every class
+                                       add_std_fld( current_fields_hash, "isnew",     array_pos++ );
+                                       add_std_fld( current_fields_hash, "ischanged", array_pos++ );
+                                       add_std_fld( current_fields_hash, "isdeleted", array_pos   );
+
+                               }
+
+                               if (!strcmp( (char*)_cur->name, "links" )) {
+                                       xmlNodePtr _l = _cur->children;
+
+                                       while(_l) {
+                                               if (strcmp( (char*)_l->name, "link" )) {
+                                                       _l = _l->next;
+                                                       continue;
+                                               }
+
+                                               osrfHash* link_def_hash = osrfNewHash();
+
+                                               if( (prop_str = (char*)xmlGetProp(_l, BAD_CAST "reltype")) ) {
+                                                       osrfHashSet(
+                                                               link_def_hash,
+                                                               prop_str,
+                                                               "reltype"
+                                                       );
+                                                       osrfLogDebug(OSRF_LOG_MARK, "Adding link with reltype %s", prop_str );
+                                               } else
+                                                       osrfLogDebug(OSRF_LOG_MARK, "Adding link with no reltype" );
+
+                                               if( (prop_str = (char*)xmlGetProp(_l, BAD_CAST "key")) ) {
+                                                       osrfHashSet(
+                                                               link_def_hash,
+                                                               prop_str,
+                                                               "key"
+                                                       );
+                                                       osrfLogDebug(OSRF_LOG_MARK, "Link fkey is %s", prop_str );
+                                               } else
+                                                       osrfLogDebug(OSRF_LOG_MARK, "Link with no fkey" );
+
+                                               if( (prop_str = (char*)xmlGetProp(_l, BAD_CAST "class")) ) {
+                                                       osrfHashSet(
+                                                               link_def_hash,
+                                                               prop_str,
+                                                               "class"
+                                                       );
+                                                       osrfLogDebug(OSRF_LOG_MARK, "Link fclass is %s", prop_str );
+                                               } else
+                                                       osrfLogDebug(OSRF_LOG_MARK, "Link with no fclass" );
+
+                                               // Tokenize map attribute into an osrfStringArray
+                                               prop_str = (char*) xmlGetProp(_l, BAD_CAST "map");
+                                               if( prop_str )
+                                                       osrfLogDebug(OSRF_LOG_MARK, "Link mapping list is %s", prop_str );
+                                               osrfStringArray* map = osrfStringArrayTokenize( prop_str, ' ' );
+                                               osrfHashSet( link_def_hash, map, "map");
+                                               xmlFree( prop_str );
+
+                                               if( (prop_str = (char*)xmlGetProp(_l, BAD_CAST "field")) ) {
+                                                       osrfHashSet(
+                                                               link_def_hash,
+                                                               prop_str,
+                                                               "field"
+                                                       );
+                                                       osrfLogDebug(OSRF_LOG_MARK, "Link fclass is %s", prop_str );
+                                               } else
+                                                       osrfLogDebug(OSRF_LOG_MARK, "Link with no fclass" );
+
+                                               osrfHashSet(
+                                                       current_links_hash,
+                                                       link_def_hash,
+                                                       prop_str
+                                               );
+
+                                               _l = _l->next;
+                                       }
+                               }
+/**** Structure of permacrud in memory ****
+
+{ create :
+    { permission : [ x, y, z ],
+      global_required : "true", -- anything else, or missing, is false
+      local_context : [ f1, f2 ],
+      foreign_context : { class1 : { fkey : local_class_key, field : class1_field, context : [ a, b, c ] }, ...}
+    },
+  retrieve : null, -- no perm check, or structure similar to the others
+  update : -- like create
+    ...
+  delete : -- like create
+    ...
+}   
+
+**** Structure of permacrud in memory ****/
+
+                               if (!strcmp( (char*)_cur->name, "permacrud" )) {
+                                       osrfHash* pcrud = osrfNewHash();
+                                       osrfHashSet( class_def_hash, pcrud, "permacrud" );
+                                       xmlNodePtr _l = _cur->children;
+
+                                       while(_l) {
+                                               if (strcmp( (char*)_l->name, "actions" )) {
+                                                       _l = _l->next;
+                                                       continue;
+                                               }
+
+                                               xmlNodePtr _a = _l->children;
+
+                                               while(_a) {
+                                                       const char* action_name = (const char*) _a->name;
+                                                       if (
+                                                               strcmp( action_name, "create" ) &&
+                                                               strcmp( action_name, "retrieve" ) &&
+                                                               strcmp( action_name, "update" ) &&
+                                                               strcmp( action_name, "delete" )
+                                                       ) {
+                                                               _a = _a->next;
+                                                               continue;
+                                                       }
+
+                                                       osrfLogDebug(OSRF_LOG_MARK, "Found Permacrud action %s for class %s",
+                                                               action_name, current_class_name );
+
+                                                       osrfHash* action_def_hash = osrfNewHash();
+                                                       osrfHashSet( pcrud, action_def_hash, action_name );
+
+                                                       // Tokenize permission attribute into an osrfStringArray
+                                                       prop_str = (char*) xmlGetProp(_a, BAD_CAST "permission");
+                                                       if( prop_str )
+                                                               osrfLogDebug(OSRF_LOG_MARK,
+                                                                       "Permacrud permission list is %s", prop_str );
+                                                       osrfStringArray* map = osrfStringArrayTokenize( prop_str, ' ' );
+                                                       osrfHashSet( action_def_hash, map, "permission");
+                                                       xmlFree( prop_str );
+
+                                               osrfHashSet( action_def_hash,
+                                                               (char*)xmlGetNoNsProp(_a, BAD_CAST "global_required"), "global_required");
+
+                                                       // Tokenize context_field attribute into an osrfStringArray
+                                                       prop_str = (char*) xmlGetProp(_a, BAD_CAST "context_field");
+                                                       if( prop_str )
+                                                               osrfLogDebug(OSRF_LOG_MARK,
+                                                                       "Permacrud context_field list is %s", prop_str );
+                                                       map = osrfStringArrayTokenize( prop_str, ' ' );
+                                                       osrfHashSet( action_def_hash, map, "local_context");
+                                                       xmlFree( prop_str );
+
+                                                       osrfHash* foreign_context = osrfNewHash();
+                                                       osrfHashSet( action_def_hash, foreign_context, "foreign_context");
+
+                                                       xmlNodePtr _f = _a->children;
+
+                                                       while(_f) {
+                                                               if ( strcmp( (char*)_f->name, "context" ) ) {
+                                                                       _f = _f->next;
+                                                                       continue;
+                                                               }
+
+                                                               if( (prop_str = (char*)xmlGetNoNsProp(_f, BAD_CAST "link")) ) {
+                                                                       osrfLogDebug(OSRF_LOG_MARK,
+                                                                               "Permacrud context link definition is %s", prop_str );
+
+                                                                       osrfHash* _tmp_fcontext = osrfNewHash();
+
+                                                                       // Store pointers to elements already stored
+                                                                       // from the <link> aggregate
+                                                                       osrfHash* _flink = osrfHashGet( current_links_hash, prop_str );
+                                                                       osrfHashSet( _tmp_fcontext, osrfHashGet(_flink, "field"), "fkey" );
+                                                                       osrfHashSet( _tmp_fcontext, osrfHashGet(_flink, "key"), "field" );
+                                                                       xmlFree( prop_str );
+
+                                                                   if( (prop_str = (char*)xmlGetNoNsProp(_f, BAD_CAST "jump")) )
+                                                                           osrfHashSet( _tmp_fcontext, osrfStringArrayTokenize( prop_str, '.' ), "jump" );
+                                                                       xmlFree( prop_str );
+
+                                                                       // Tokenize field attribute into an osrfStringArray
+                                                                       char * field_list = (char*) xmlGetProp(_f, BAD_CAST "field");
+                                                                       if( field_list )
+                                                                               osrfLogDebug(OSRF_LOG_MARK,
+                                                                                       "Permacrud foreign context field list is %s", field_list );
+                                                                       map = osrfStringArrayTokenize( field_list, ' ' );
+                                                                       osrfHashSet( _tmp_fcontext, map, "context");
+                                                                       xmlFree( field_list );
+
+                                                                       // Insert the new hash into a hash attached to the parent node
+                                                                       osrfHashSet( foreign_context, _tmp_fcontext, osrfHashGet( _flink, "class" ) );
+
+                                                               } else {
+
+                                                                       if( (prop_str = (char*)xmlGetNoNsProp(_f, BAD_CAST "field") )) {
+                                                                               char* map_list = prop_str;
+                                                                               osrfLogDebug(OSRF_LOG_MARK,
+                                                                                       "Permacrud local context field list is %s", prop_str );
+                       
+                                                                               if (strlen( map_list ) > 0) {
+                                                                                       char* st_tmp = NULL;
+                                                                                       char* _map_class = strtok_r(map_list, " ", &st_tmp);
+                                                                                       osrfStringArrayAdd(
+                                                                                               osrfHashGet( action_def_hash, "local_context"), _map_class);
+                                                                       
+                                                                                       while ((_map_class = strtok_r(NULL, " ", &st_tmp))) {
+                                                                                               osrfStringArrayAdd(
+                                                                                                       osrfHashGet( action_def_hash, "local_context"), _map_class);
+                                                                                       }
+                                                                               }
+                                                                               xmlFree(map_list);
+                                                                       }
+
+                                                               }
+                                                               _f = _f->next;
+                                                       }
+                                                       _a = _a->next;
+                                               }
+                                               _l = _l->next;
+                                       }
+                               }
+
+                               if (!strcmp( (char*)_cur->name, "source_definition" )) {
+                                       char* content_str;
+                                       if( (content_str = (char*)xmlNodeGetContent(_cur)) ) {
+                                               osrfLogDebug(OSRF_LOG_MARK, "Using source definition '%s' for class %s",
+                                                       content_str, current_class_name );
+                                               osrfHashSet(
+                                                       class_def_hash,
+                                                       content_str,
+                                                       "source_definition"
+                                               );
+                                       }
+
+                               }
+
+                               _cur = _cur->next;
+                       } // end while
+               }
+
+               kid = kid->next;
+       } // end while
+
+       osrfLogInfo(OSRF_LOG_MARK, "...IDL XML parsed");
+
+       return idlHash;
+}
+
+// Adds a standard virtual field to a fields hash
+static void add_std_fld( osrfHash* fields_hash, const char* field_name, unsigned pos ) {
+       char array_pos_buf[ 7 ];
+       osrfHash* std_fld_hash = osrfNewHash();
+
+       snprintf( array_pos_buf, sizeof( array_pos_buf ), "%u", pos );
+       osrfHashSet( std_fld_hash, strdup( array_pos_buf ), "array_position" );
+       osrfHashSet( std_fld_hash, "true", "virtual" );
+       osrfHashSet( std_fld_hash, strdup( field_name ), "name" );
+       osrfHashSet( fields_hash, std_fld_hash, field_name );
+}
+
+
+osrfHash* oilsIDLFindPath( const char* path, ... ) {
+       if(!path || strlen(path) < 1) return NULL;
+
+       osrfHash* obj = idlHash;
+
+       VA_LIST_TO_STRING(path);
+       char* buf = VA_BUF;
+
+       char* token = NULL;
+       char* t = buf;
+       char* tt;
+
+       token = strtok_r(t, "/", &tt);
+       if(!token) return NULL;
+
+       do {
+               obj = osrfHashGet(obj, token);
+       } while( (token = strtok_r(NULL, "/", &tt)) && obj);
+
+       return obj;
+}
+
+static osrfHash* findClassDef( const char* classname ) {
+       if( !classname || !idlHash )
+               return NULL;
+       else
+               return osrfHashGet( idlHash, classname );
+}
+
+osrfHash* oilsIDL_links( const char* classname ) {
+       osrfHash* classdef = findClassDef( classname );
+       if( classdef )
+               return osrfHashGet( classdef, "links" );
+       else
+               return NULL;
+}
+
+osrfHash* oilsIDL_fields( const char* classname ) {
+       osrfHash* classdef = findClassDef( classname );
+       if( classdef )
+               return osrfHashGet( classdef, "fields" );
+       else
+               return NULL;
+}
+
+int oilsIDL_classIsFieldmapper ( const char* classname ) {
+       if( findClassDef( classname ) )
+               return 1;
+       else
+               return 0;
+}
+
+// For a given class: return the array_position associated with a 
+// specified field. (or -1 if it doesn't exist)
+int oilsIDL_ntop (const char* classname, const char* fieldname) {
+       osrfHash* fields_hash = oilsIDL_fields( classname );
+       if( !fields_hash )
+               return -1;     // No such class, or no fields for it
+
+       osrfHash* field_def_hash = osrfHashGet( fields_hash, fieldname );
+       if( !field_def_hash )
+               return -1;                      // No such field
+
+       const char* pos_attr = osrfHashGet( field_def_hash, "array_position" );
+       if( !pos_attr )
+               return -1;                      // No array_position attribute
+
+       return atoi( pos_attr );        // Return position as int
+}
+
+// For a given class: return a copy of the name of the field 
+// at a specified array_position (or NULL if there is none)
+char * oilsIDL_pton (const char* classname, int pos) {
+       osrfHash* fields_hash = oilsIDL_fields( classname );
+       if( !fields_hash )
+               return NULL;     // No such class, or no fields for it
+
+       char* ret = NULL;
+       osrfHash* field_def_hash = NULL;
+       osrfHashIterator* iter = osrfNewHashIterator( fields_hash );
+
+       while ( ( field_def_hash = osrfHashIteratorNext( iter ) ) ) {
+               if ( atoi( osrfHashGet( field_def_hash, "array_position" ) ) == pos ) {
+                       ret = strdup( osrfHashIteratorKey( iter ) );
+                       break;
+               }
+       }
+
+       osrfHashIteratorFree( iter );
+
+       return ret;
+}
+
diff --git a/src/c/oils_pcrud.c b/src/c/oils_pcrud.c
new file mode 100644 (file)
index 0000000..18fadfc
--- /dev/null
@@ -0,0 +1,337 @@
+/**
+       @file oils_pcrud.c
+       @brief As a server, perform database operations at the request of clients.
+
+       This server is similar to the cstore and reporter-store servers,
+       except that it enforces a permissions scheme.
+*/
+
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <dbi/dbi.h>
+#include "opensrf/utils.h"
+#include "opensrf/log.h"
+#include "opensrf/osrf_application.h"
+#include "openils/oils_utils.h"
+#include "openils/oils_sql.h"
+
+static dbi_conn writehandle; /* our MASTER db connection */
+static dbi_conn dbhandle; /* our CURRENT db connection */
+//static osrfHash * readHandles;
+
+static const int enforce_pcrud = 1;     // Boolean
+static const char modulename[] = "open-ils.pcrud";
+
+/**
+       @brief Disconnect from the database.
+
+       This function is called when the server drone is about to terminate.
+*/
+void osrfAppChildExit( void ) {
+       osrfLogDebug(OSRF_LOG_MARK, "Child is exiting, disconnecting from database...");
+
+       int same = 0;
+       if (writehandle == dbhandle)
+               same = 1;
+
+       if (writehandle) {
+               dbi_conn_query(writehandle, "ROLLBACK;");
+               dbi_conn_close(writehandle);
+               writehandle = NULL;
+       }
+       if (dbhandle && !same)
+               dbi_conn_close(dbhandle);
+
+       // XXX add cleanup of readHandles whenever that gets used
+
+       return;
+}
+
+/**
+       @brief Initialize the application.
+       @return Zero if successful, or non-zero if not.
+
+       Load the IDL file into an internal data structure for future reference.  Each non-virtual
+       class in the IDL corresponds to a table or view in the database, or to a subquery defined
+       in the IDL.  Ignore all virtual tables and virtual fields.
+
+       Register a number of methods, some of them general-purpose and others specific for
+       particular classes.
+
+       The name of the application is given by the MODULENAME macro, whose value depends on
+       conditional compilation.  The method names also incorporate MODULENAME, followed by a
+       dot, as a prefix.  Some methods are registered or not registered depending on whether
+       the IDL includes permacrud entries for the class and method.
+
+       The general-purpose methods are as follows (minus their MODULENAME prefixes):
+
+       - transaction.begin
+       - transaction.commit
+       - transaction.rollback
+       - savepoint.set
+       - savepoint.release
+       - savepoint.rollback
+       - set_audit_info
+
+       For each non-virtual class, create up to eight class-specific methods:
+
+       - create    (not for readonly classes)
+       - retrieve
+       - update    (not for readonly classes)
+       - delete    (not for readonly classes
+       - search    (atomic and non-atomic versions)
+       - id_list   (atomic and non-atomic versions)
+
+       The full method names follow the pattern "MODULENAME.method_type.classname".
+       In addition, the names of atomic methods have a suffix of ".atomic".
+
+       This function is called when the registering the application, and is executed by the
+       listener before spawning the drones.
+*/
+int osrfAppInitialize( void ) {
+
+       osrfLogInfo(OSRF_LOG_MARK, "Initializing the PCRUD Server...");
+       osrfLogInfo(OSRF_LOG_MARK, "Finding XML file...");
+
+       if ( !oilsIDLInit( osrf_settings_host_value( "/IDL" )))
+               return 1; /* return non-zero to indicate error */
+
+       // Open the database temporarily.  Look up the datatypes of all
+       // the non-virtual fields and record them with the IDL data.
+       dbi_conn handle = oilsConnectDB( modulename );
+       if( !handle )
+               return -1;
+       else if( oilsExtendIDL( handle )) {
+               osrfLogError( OSRF_LOG_MARK, "Error extending the IDL" );
+               return -1;
+       }
+       dbi_conn_close( handle );
+
+       // Get the maximum flesh depth from the settings
+       char* md = osrf_settings_host_value(
+               "/apps/%s/app_settings/max_query_recursion", modulename );
+       int max_flesh_depth = 100;
+       if( md )
+               max_flesh_depth = atoi( md );
+       if( max_flesh_depth < 0 )
+               max_flesh_depth = 1;
+       else if( max_flesh_depth > 1000 )
+               max_flesh_depth = 1000;
+
+       oilsSetSQLOptions( modulename, enforce_pcrud, max_flesh_depth );
+
+       // Now register all the methods
+       growing_buffer* method_name = buffer_init(64);
+
+       // first we register all the transaction and savepoint methods
+       buffer_reset(method_name);
+       OSRF_BUFFER_ADD(method_name, modulename );
+       OSRF_BUFFER_ADD(method_name, ".transaction.begin");
+       osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
+                       "beginTransaction", "", 0, 0 );
+
+       buffer_reset(method_name);
+       OSRF_BUFFER_ADD(method_name, modulename );
+       OSRF_BUFFER_ADD(method_name, ".transaction.commit");
+       osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+                       "commitTransaction", "", 0, 0 );
+
+       buffer_reset(method_name);
+       OSRF_BUFFER_ADD(method_name, modulename );
+       OSRF_BUFFER_ADD(method_name, ".transaction.rollback");
+       osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+                       "rollbackTransaction", "", 0, 0 );
+
+       buffer_reset(method_name);
+       OSRF_BUFFER_ADD(method_name, modulename );
+       OSRF_BUFFER_ADD(method_name, ".savepoint.set");
+       osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+                       "setSavepoint", "", 1, 0 );
+
+       buffer_reset(method_name);
+       OSRF_BUFFER_ADD(method_name, modulename );
+       OSRF_BUFFER_ADD(method_name, ".savepoint.release");
+       osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+                       "releaseSavepoint", "", 1, 0 );
+
+       buffer_reset(method_name);
+       OSRF_BUFFER_ADD(method_name, modulename );
+       OSRF_BUFFER_ADD(method_name, ".savepoint.rollback");
+       osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+                       "rollbackSavepoint", "", 1, 0 );
+
+       buffer_reset(method_name);
+       OSRF_BUFFER_ADD(method_name, modulename );
+       OSRF_BUFFER_ADD(method_name, ".set_audit_info");
+       osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+                       "setAuditInfo", "", 3, 0 );
+
+       static const char* global_method[] = {
+               "create",
+               "retrieve",
+               "update",
+               "delete",
+               "search",
+               "id_list"
+       };
+       const int global_method_count
+               = sizeof( global_method ) / sizeof ( global_method[0] );
+
+       unsigned long class_count = osrfHashGetCount( oilsIDL() );
+       osrfLogDebug(OSRF_LOG_MARK, "%lu classes loaded", class_count );
+       osrfLogDebug(OSRF_LOG_MARK,
+               "At most %lu methods will be generated",
+               (unsigned long) (class_count * global_method_count) );
+
+       osrfHashIterator* class_itr = osrfNewHashIterator( oilsIDL() );
+       osrfHash* idlClass = NULL;
+
+       // For each class in the IDL...
+       while( (idlClass = osrfHashIteratorNext( class_itr ) ) ) {
+
+               const char* classname = osrfHashIteratorKey( class_itr );
+               osrfLogInfo(OSRF_LOG_MARK, "Generating class methods for %s", classname);
+
+               if (!osrfStringArrayContains( osrfHashGet(idlClass, "controller"), modulename )) {
+                       osrfLogInfo(OSRF_LOG_MARK, "%s is not listed as a controller for %s, moving on",
+                                       modulename, classname);
+                       continue;
+               }
+
+               if ( str_is_true( osrfHashGet(idlClass, "virtual") ) ) {
+                       osrfLogDebug(OSRF_LOG_MARK, "Class %s is virtual, skipping", classname );
+                       continue;
+               }
+
+               // Look up some other attributes of the current class
+               const char* idlClass_fieldmapper = osrfHashGet(idlClass, "fieldmapper");
+               if( !idlClass_fieldmapper ) {
+                       osrfLogDebug( OSRF_LOG_MARK, "Skipping class \"%s\"; no fieldmapper in IDL",
+                                       classname );
+                       continue;
+               }
+
+               osrfHash* idlClass_permacrud = NULL;
+
+               // Ignore classes with no permacrud section
+               idlClass_permacrud = osrfHashGet( idlClass, "permacrud" );
+               if( !idlClass_permacrud ) {
+                       osrfLogDebug( OSRF_LOG_MARK,
+                               "Skipping class \"%s\"; no permacrud in IDL", classname );
+                       continue;
+               }
+
+               const char* readonly = osrfHashGet( idlClass, "readonly" );
+
+               int i;
+               for( i = 0; i < global_method_count; ++i ) {  // for each global method
+                       const char* method_type = global_method[ i ];
+                       osrfLogDebug(OSRF_LOG_MARK,
+                               "Using files to build %s class methods for %s", method_type, classname);
+
+                       // Treat "id_list" or "search" as forms of "retrieve"
+                       const char* tmp_method = method_type;
+                       if ( *tmp_method == 'i' || *tmp_method == 's') {  // "id_list" or "search"
+                               tmp_method = "retrieve";
+                       }
+                       // Skip this method if there is no permacrud entry for it
+                       if (!osrfHashGet( idlClass_permacrud, tmp_method ))
+                               continue;
+
+                       // No create, update, or delete methods for a readonly class
+                       if ( str_is_true( readonly )
+                               && ( *method_type == 'c' || *method_type == 'u' || *method_type == 'd') )
+                               continue;
+
+                       buffer_reset( method_name );
+
+                       // Build the method name: MODULENAME.method_type.classname
+                       buffer_fadd(method_name, "%s.%s.%s", modulename, method_type, classname);
+
+                       // For an id_list or search method we specify the OSRF_METHOD_STREAMING option.
+                       // The consequence is that we implicitly create an atomic method in addition to
+                       // the usual non-atomic method.
+                       int flags = 0;
+                       if (*method_type == 'i' || *method_type == 's') {  // id_list or search
+                               flags = flags | OSRF_METHOD_STREAMING;
+                       }
+
+                       osrfHash* method_meta = osrfNewHash();
+                       osrfHashSet( method_meta, idlClass, "class");
+                       osrfHashSet( method_meta, buffer_data( method_name ), "methodname" );
+                       osrfHashSet( method_meta, strdup(method_type), "methodtype" );
+
+                       // Register the method, with a pointer to an osrfHash to tell the method
+                       // its name, type, and class.
+                       osrfAppRegisterExtendedMethod(
+                               modulename,
+                               OSRF_BUFFER_C_STR( method_name ),
+                               "dispatchCRUDMethod",
+                               "",
+                               1,
+                               flags,
+                               (void*)method_meta
+                       );
+
+               } // end for each global method
+       } // end for each class in IDL
+
+       buffer_free( method_name );
+       osrfHashIteratorFree( class_itr );
+
+       return 0;
+}
+
+/**
+       @brief Initialize a server drone.
+       @return Zero if successful, -1 if not.
+
+       Connect to the database.
+
+       This function is called by a server drone shortly after it is spawned by the listener.
+*/
+int osrfAppChildInit( void ) {
+
+       writehandle = oilsConnectDB( modulename );
+       if( !writehandle )
+               return -1;
+
+       oilsSetDBConnection( writehandle );
+
+       return 0;
+}
+
+/**
+       @brief Implement the class-specific methods.
+       @param ctx Pointer to the method context.
+       @return Zero if successful, or -1 if not.
+
+       Branch on the method type: create, retrieve, update, delete, search, or id_list.
+
+       The method parameters and the type of value returned to the client depend on the method
+       type.  However, for all PCRUD methods, the first method parameter is an authkey.
+*/
+int dispatchCRUDMethod( osrfMethodContext* ctx ) {
+
+       // Get the method type, then can branch on it
+       osrfHash* method_meta = (osrfHash*) ctx->method->userData;
+       const char* methodtype = osrfHashGet( method_meta, "methodtype" );
+
+       if( !strcmp( methodtype, "create" ))
+               return doCreate( ctx );
+       else if( !strcmp(methodtype, "retrieve" ))
+               return doRetrieve( ctx );
+       else if( !strcmp(methodtype, "update" ))
+               return doUpdate( ctx );
+       else if( !strcmp(methodtype, "delete" ))
+               return doDelete( ctx );
+       else if( !strcmp(methodtype, "search" ))
+               return doSearch( ctx );
+       else if( !strcmp(methodtype, "id_list" ))
+               return doIdList( ctx );
+       else {
+               osrfAppRespondComplete( ctx, NULL );      // should be unreachable...
+               return 0;
+       }
+}
diff --git a/src/c/oils_sql.c b/src/c/oils_sql.c
new file mode 100644 (file)
index 0000000..c67362b
--- /dev/null
@@ -0,0 +1,7337 @@
+/**
+       @file oils_sql.c
+       @brief Utility routines for translating JSON into SQL.
+*/
+
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <ctype.h>
+#include <dbi/dbi.h>
+#include "opensrf/utils.h"
+#include "opensrf/log.h"
+#include "opensrf/osrf_application.h"
+#include "openils/oils_utils.h"
+#include "openils/oils_sql.h"
+
+// The next four macros are OR'd together as needed to form a set
+// of bitflags.  SUBCOMBO enables an extra pair of parentheses when
+// nesting one UNION, INTERSECT or EXCEPT inside another.
+// SUBSELECT tells us we're in a subquery, so don't add the
+// terminal semicolon yet.
+#define SUBCOMBO    8
+#define SUBSELECT   4
+#define DISABLE_I18N    2
+#define SELECT_DISTINCT 1
+
+#define AND_OP_JOIN     0
+#define OR_OP_JOIN      1
+
+struct ClassInfoStruct;
+typedef struct ClassInfoStruct ClassInfo;
+
+#define ALIAS_STORE_SIZE 16
+#define CLASS_NAME_STORE_SIZE 16
+
+struct ClassInfoStruct {
+       char* alias;
+       char* class_name;
+       char* source_def;
+       osrfHash* class_def;      // Points into IDL
+       osrfHash* fields;         // Points into IDL
+       osrfHash* links;          // Points into IDL
+
+       // The remaining members are private and internal.  Client code should not
+       // access them directly.
+
+       ClassInfo* next;          // Supports linked list of joined classes
+       int in_use;               // boolean
+
+       // We usually store the alias and class name in the following arrays, and
+       // point the corresponding pointers at them.  When the string is too big
+       // for the array (which will probably never happen in practice), we strdup it.
+
+       char alias_store[ ALIAS_STORE_SIZE + 1 ];
+       char class_name_store[ CLASS_NAME_STORE_SIZE + 1 ];
+};
+
+struct QueryFrameStruct;
+typedef struct QueryFrameStruct QueryFrame;
+
+struct QueryFrameStruct {
+       ClassInfo core;
+       ClassInfo* join_list;  // linked list of classes joined to the core class
+       QueryFrame* next;      // implements stack as linked list
+       int in_use;            // boolean
+};
+
+static int timeout_needs_resetting;
+static time_t time_next_reset;
+
+static int verifyObjectClass ( osrfMethodContext*, const jsonObject* );
+
+static void setXactId( osrfMethodContext* ctx );
+static inline const char* getXactId( osrfMethodContext* ctx );
+static inline void clearXactId( osrfMethodContext* ctx );
+
+static jsonObject* doFieldmapperSearch ( osrfMethodContext* ctx, osrfHash* class_meta,
+               jsonObject* where_hash, jsonObject* query_hash, int* err );
+static jsonObject* oilsMakeFieldmapperFromResult( dbi_result, osrfHash* );
+static jsonObject* oilsMakeJSONFromResult( dbi_result );
+
+static char* searchSimplePredicate ( const char* op, const char* class_alias,
+                               osrfHash* field, const jsonObject* node );
+static char* searchFunctionPredicate ( const char*, osrfHash*, const jsonObject*, const char* );
+static char* searchFieldTransform ( const char*, osrfHash*, const jsonObject* );
+static char* searchFieldTransformPredicate ( const ClassInfo*, osrfHash*, const jsonObject*,
+               const char* );
+static char* searchBETWEENPredicate ( const char*, osrfHash*, const jsonObject* );
+static char* searchINPredicate ( const char*, osrfHash*,
+                                                                jsonObject*, const char*, osrfMethodContext* );
+static char* searchPredicate ( const ClassInfo*, osrfHash*, jsonObject*, osrfMethodContext* );
+static char* searchJOIN ( const jsonObject*, const ClassInfo* left_info );
+static char* searchWHERE ( const jsonObject* search_hash, const ClassInfo*, int, osrfMethodContext* );
+static char* buildSELECT( const jsonObject*, jsonObject* rest_of_query,
+       osrfHash* meta, osrfMethodContext* ctx );
+static char* buildOrderByFromArray( osrfMethodContext* ctx, const jsonObject* order_array );
+
+char* buildQuery( osrfMethodContext* ctx, jsonObject* query, int flags );
+
+char* SELECT ( osrfMethodContext*, jsonObject*, const jsonObject*, const jsonObject*,
+       const jsonObject*, const jsonObject*, const jsonObject*, const jsonObject*, int );
+
+static osrfStringArray* getPermLocationCache( osrfMethodContext*, const char* );
+static void setPermLocationCache( osrfMethodContext*, const char*, osrfStringArray* );
+
+void userDataFree( void* );
+static void sessionDataFree( char*, void* );
+static void pcacheFree( char*, void* );
+static int obj_is_true( const jsonObject* obj );
+static const char* json_type( int code );
+static const char* get_primitive( osrfHash* field );
+static const char* get_datatype( osrfHash* field );
+static void pop_query_frame( void );
+static void push_query_frame( void );
+static int add_query_core( const char* alias, const char* class_name );
+static inline ClassInfo* search_alias( const char* target );
+static ClassInfo* search_all_alias( const char* target );
+static ClassInfo* add_joined_class( const char* alias, const char* classname );
+static void clear_query_stack( void );
+
+static const jsonObject* verifyUserPCRUD( osrfMethodContext* );
+static int verifyObjectPCRUD( osrfMethodContext*, osrfHash*, const jsonObject*, int );
+static const char* org_tree_root( osrfMethodContext* ctx );
+static jsonObject* single_hash( const char* key, const char* value );
+
+static int child_initialized = 0;   /* boolean */
+
+static dbi_conn writehandle; /* our MASTER db connection */
+static dbi_conn dbhandle; /* our CURRENT db connection */
+//static osrfHash * readHandles;
+
+// The following points to the top of a stack of QueryFrames.  It's a little
+// confusing because the top level of the query is at the bottom of the stack.
+static QueryFrame* curr_query = NULL;
+
+static dbi_conn writehandle; /* our MASTER db connection */
+static dbi_conn dbhandle; /* our CURRENT db connection */
+//static osrfHash * readHandles;
+
+static int max_flesh_depth = 100;
+
+static int perm_at_threshold = 5;
+static int enforce_pcrud = 0;     // Boolean
+static char* modulename = NULL;
+
+int writeAuditInfo( osrfMethodContext* ctx, const char* user_id, const char* ws_id);
+
+static char* _sanitize_savepoint_name( const char* sp );
+
+/**
+       @brief Connect to the database.
+       @return A database connection if successful, or NULL if not.
+*/
+dbi_conn oilsConnectDB( const char* mod_name ) {
+
+       osrfLogDebug( OSRF_LOG_MARK, "Attempting to initialize libdbi..." );
+       if( dbi_initialize( NULL ) == -1 ) {
+               osrfLogError( OSRF_LOG_MARK, "Unable to initialize libdbi" );
+               return NULL;
+       } else
+               osrfLogDebug( OSRF_LOG_MARK, "... libdbi initialized." );
+
+       char* driver = osrf_settings_host_value( "/apps/%s/app_settings/driver", mod_name );
+       char* user   = osrf_settings_host_value( "/apps/%s/app_settings/database/user", mod_name );
+       char* host   = osrf_settings_host_value( "/apps/%s/app_settings/database/host", mod_name );
+       char* port   = osrf_settings_host_value( "/apps/%s/app_settings/database/port", mod_name );
+       char* db     = osrf_settings_host_value( "/apps/%s/app_settings/database/db", mod_name );
+       char* pw     = osrf_settings_host_value( "/apps/%s/app_settings/database/pw", mod_name );
+
+       osrfLogDebug( OSRF_LOG_MARK, "Attempting to load the database driver [%s]...", driver );
+       dbi_conn handle = dbi_conn_new( driver );
+
+       if( !handle ) {
+               osrfLogError( OSRF_LOG_MARK, "Error loading database driver [%s]", driver );
+               return NULL;
+       }
+       osrfLogDebug( OSRF_LOG_MARK, "Database driver [%s] seems OK", driver );
+
+       osrfLogInfo(OSRF_LOG_MARK, "%s connecting to database.  host=%s, "
+               "port=%s, user=%s, db=%s", mod_name, host, port, user, db );
+
+       if( host ) dbi_conn_set_option( handle, "host", host );
+       if( port ) dbi_conn_set_option_numeric( handle, "port", atoi( port ));
+       if( user ) dbi_conn_set_option( handle, "username", user );
+       if( pw )   dbi_conn_set_option( handle, "password", pw );
+       if( db )   dbi_conn_set_option( handle, "dbname", db );
+
+       free( user );
+       free( host );
+       free( port );
+       free( db );
+       free( pw );
+
+       if( dbi_conn_connect( handle ) < 0 ) {
+               sleep( 1 );
+               if( dbi_conn_connect( handle ) < 0 ) {
+                       const char* msg;
+                       dbi_conn_error( handle, &msg );
+                       osrfLogError( OSRF_LOG_MARK, "Error connecting to database: %s",
+                               msg ? msg : "(No description available)" );
+                       return NULL;
+               }
+       }
+
+       osrfLogInfo( OSRF_LOG_MARK, "%s successfully connected to the database", mod_name );
+
+       return handle;
+}
+
+/**
+       @brief Select some options.
+       @param module_name: Name of the server.
+       @param do_pcrud: Boolean.  True if we are to enforce PCRUD permissions.
+
+       This source file is used (at this writing) to implement three different servers:
+       - open-ils.reporter-store
+       - open-ils.pcrud
+       - open-ils.cstore
+
+       These servers behave mostly the same, but they implement different combinations of
+       methods, and open-ils.pcrud enforces a permissions scheme that the other two don't.
+
+       Here we use the server name in messages to identify which kind of server issued them.
+       We use do_crud as a boolean to control whether or not to enforce the permissions scheme.
+*/
+void oilsSetSQLOptions( const char* module_name, int do_pcrud, int flesh_depth ) {
+       if( !module_name )
+               module_name = "open-ils.cstore";   // bulletproofing with a default
+
+       if( modulename )
+               free( modulename );
+
+       modulename = strdup( module_name );
+       enforce_pcrud = do_pcrud;
+       max_flesh_depth = flesh_depth;
+}
+
+/**
+       @brief Install a database connection.
+       @param conn Pointer to a database connection.
+
+       In some contexts, @a conn may merely provide a driver so that we can process strings
+       properly, without providing an open database connection.
+*/
+void oilsSetDBConnection( dbi_conn conn ) {
+       dbhandle = writehandle = conn;
+}
+
+/**
+       @brief Determine whether a database connection is alive.
+       @param handle Handle for a database connection.
+       @return 1 if the connection is alive, or zero if it isn't.
+*/
+int oilsIsDBConnected( dbi_conn handle ) {
+       // Do an innocuous SELECT.  If it succeeds, the database connection is still good.
+       dbi_result result = dbi_conn_query( handle, "SELECT 1;" );
+       if( result ) {
+               dbi_result_free( result );
+               return 1;
+       } else {
+               // This is a terrible, horrible, no good, very bad kludge.
+               // Sometimes the SELECT 1 query fails, not because the database connection is dead,
+               // but because (due to a previous error) the database is ignoring all commands,
+               // even innocuous SELECTs, until the current transaction is rolled back.  The only
+               // known way to detect this condition via the dbi library is by looking at the error
+               // message.  This approach will break if the language or wording of the message ever
+               // changes.
+               // Note: the dbi_conn_ping function purports to determine whether the database
+               // connection is live, but at this writing this function is unreliable and useless.
+               static const char* ok_msg = "ERROR:  current transaction is aborted, commands "
+                       "ignored until end of transaction block\n";
+               const char* msg;
+               dbi_conn_error( handle, &msg );
+               if( strcmp( msg, ok_msg )) {
+                       osrfLogError( OSRF_LOG_MARK, "Database connection isn't working" );
+                       return 0;
+               } else
+                       return 1;   // ignoring SELECT due to previous error; that's okay
+       }
+}
+
+/**
+       @brief Get a table name, view name, or subquery for use in a FROM clause.
+       @param class Pointer to the IDL class entry.
+       @return A table name, a view name, or a subquery in parentheses.
+
+       In some cases the IDL defines a class, not with a table name or a view name, but with
+       a SELECT statement, which may be used as a subquery.
+*/
+char* oilsGetRelation( osrfHash* classdef ) {
+
+       char* source_def = NULL;
+       const char* tabledef = osrfHashGet( classdef, "tablename" );
+
+       if( tabledef ) {
+               source_def = strdup( tabledef );   // Return the name of a table or view
+       } else {
+               tabledef = osrfHashGet( classdef, "source_definition" );
+               if( tabledef ) {
+                       // Return a subquery, enclosed in parentheses
+                       source_def = safe_malloc( strlen( tabledef ) + 3 );
+                       source_def[ 0 ] = '(';
+                       strcpy( source_def + 1, tabledef );
+                       strcat( source_def, ")" );
+               } else {
+                       // Not found: return an error
+                       const char* classname = osrfHashGet( classdef, "classname" );
+                       if( !classname )
+                               classname = "???";
+                       osrfLogError(
+                               OSRF_LOG_MARK,
+                               "%s ERROR No tablename or source_definition for class \"%s\"",
+                               modulename,
+                               classname
+                       );
+               }
+       }
+
+       return source_def;
+}
+
+/**
+       @brief Add datatypes from the database to the fields in the IDL.
+       @param handle Handle for a database connection
+       @return Zero if successful, or 1 upon error.
+
+       For each relevant class in the IDL: ask the database for the datatype of every field.
+       In particular, determine which fields are text fields and which fields are numeric
+       fields, so that we know whether to enclose their values in quotes.
+*/
+int oilsExtendIDL( dbi_conn handle ) {
+       osrfHashIterator* class_itr = osrfNewHashIterator( oilsIDL() );
+       osrfHash* class = NULL;
+       growing_buffer* query_buf = buffer_init( 64 );
+       int results_found = 0;   // boolean
+
+       // For each class in the IDL...
+       while( (class = osrfHashIteratorNext( class_itr ) ) ) {
+               const char* classname = osrfHashIteratorKey( class_itr );
+               osrfHash* fields = osrfHashGet( class, "fields" );
+
+               // If the class is virtual, ignore it
+               if( str_is_true( osrfHashGet(class, "virtual") ) ) {
+                       osrfLogDebug(OSRF_LOG_MARK, "Class %s is virtual, skipping", classname );
+                       continue;
+               }
+
+               char* tabledef = oilsGetRelation( class );
+               if( !tabledef )
+                       continue;   // No such relation -- a query of it would be doomed to failure
+
+               buffer_reset( query_buf );
+               buffer_fadd( query_buf, "SELECT * FROM %s AS x WHERE 1=0;", tabledef );
+
+               free(tabledef );
+
+               osrfLogDebug( OSRF_LOG_MARK, "%s Investigatory SQL = %s",
+                               modulename, OSRF_BUFFER_C_STR( query_buf ) );
+
+               dbi_result result = dbi_conn_query( handle, OSRF_BUFFER_C_STR( query_buf ) );
+               if( result ) {
+
+                       results_found = 1;
+                       int columnIndex = 1;
+                       const char* columnName;
+                       while( (columnName = dbi_result_get_field_name(result, columnIndex)) ) {
+
+                               osrfLogInternal( OSRF_LOG_MARK, "Looking for column named [%s]...",
+                                               columnName );
+
+                               /* fetch the fieldmapper index */
+                               osrfHash* _f = osrfHashGet(fields, columnName);
+                               if( _f ) {
+
+                                       osrfLogDebug(OSRF_LOG_MARK, "Found [%s] in IDL hash...", columnName);
+
+                                       /* determine the field type and storage attributes */
+
+                                       switch( dbi_result_get_field_type_idx( result, columnIndex )) {
+
+                                               case DBI_TYPE_INTEGER : {
+
+                                                       if( !osrfHashGet(_f, "primitive") )
+                                                               osrfHashSet(_f, "number", "primitive");
+
+                                                       int attr = dbi_result_get_field_attribs_idx( result, columnIndex );
+                                                       if( attr & DBI_INTEGER_SIZE8 )
+                                                               osrfHashSet( _f, "INT8", "datatype" );
+                                                       else
+                                                               osrfHashSet( _f, "INT", "datatype" );
+                                                       break;
+                                               }
+                                               case DBI_TYPE_DECIMAL :
+                                                       if( !osrfHashGet( _f, "primitive" ))
+                                                               osrfHashSet( _f, "number", "primitive" );
+
+                                                       osrfHashSet( _f, "NUMERIC", "datatype" );
+                                                       break;
+
+                                               case DBI_TYPE_STRING :
+                                                       if( !osrfHashGet( _f, "primitive" ))
+                                                               osrfHashSet( _f, "string", "primitive" );
+
+                                                       osrfHashSet( _f,"TEXT", "datatype" );
+                                                       break;
+
+                                               case DBI_TYPE_DATETIME :
+                                                       if( !osrfHashGet( _f, "primitive" ))
+                                                               osrfHashSet( _f, "string", "primitive" );
+
+                                                       osrfHashSet( _f, "TIMESTAMP", "datatype" );
+                                                       break;
+
+                                               case DBI_TYPE_BINARY :
+                                                       if( !osrfHashGet( _f, "primitive" ))
+                                                               osrfHashSet( _f, "string", "primitive" );
+
+                                                       osrfHashSet( _f, "BYTEA", "datatype" );
+                                       }
+
+                                       osrfLogDebug(
+                                               OSRF_LOG_MARK,
+                                               "Setting [%s] to primitive [%s] and datatype [%s]...",
+                                               columnName,
+                                               osrfHashGet( _f, "primitive" ),
+                                               osrfHashGet( _f, "datatype" )
+                                       );
+                               }
+                               ++columnIndex;
+                       } // end while loop for traversing columns of result
+                       dbi_result_free( result  );
+               } else {
+                       const char* msg;
+                       int errnum = dbi_conn_error( handle, &msg );
+                       osrfLogDebug( OSRF_LOG_MARK, "No data found for class [%s]: %d, %s", classname,
+                               errnum, msg ? msg : "(No description available)" );
+                       // We don't check the database connection here.  It's routine to get failures at
+                       // this point; we routinely try to query tables that don't exist, because they
+                       // are defined in the IDL but not in the database.
+               }
+       } // end for each class in IDL
+
+       buffer_free( query_buf );
+       osrfHashIteratorFree( class_itr );
+       child_initialized = 1;
+
+       if( !results_found ) {
+               osrfLogError( OSRF_LOG_MARK,
+                       "No results found for any class -- bad database connection?" );
+               return 1;
+       } else if( ! oilsIsDBConnected( handle )) {
+               osrfLogError( OSRF_LOG_MARK,
+                       "Unable to extend IDL: database connection isn't working" );
+               return 1;
+       }
+       else
+               return 0;
+}
+
+/**
+       @brief Free an osrfHash that stores a transaction ID.
+       @param blob A pointer to the osrfHash to be freed, cast to a void pointer.
+
+       This function is a callback, to be called by the application session when it ends.
+       The application session stores the osrfHash via an opaque pointer.
+
+       If the osrfHash contains an entry for the key "xact_id", it means that an
+       uncommitted transaction is pending.  Roll it back.
+*/
+void userDataFree( void* blob ) {
+       osrfHash* hash = (osrfHash*) blob;
+       if( osrfHashGet( hash, "xact_id" ) && writehandle ) {
+               if( !dbi_conn_query( writehandle, "ROLLBACK;" )) {
+                       const char* msg;
+                       int errnum = dbi_conn_error( writehandle, &msg );
+                       osrfLogWarning( OSRF_LOG_MARK, "Unable to perform rollback: %d %s",
+                               errnum, msg ? msg : "(No description available)" );
+               };
+       }
+       if( writehandle ) {
+               if( !dbi_conn_query( writehandle, "SELECT auditor.clear_audit_info();" ) ) {
+                       const char* msg;
+                       int errnum = dbi_conn_error( writehandle, &msg );
+                       osrfLogWarning( OSRF_LOG_MARK, "Unable to perform audit info clearing: %d %s",
+                               errnum, msg ? msg : "(No description available)" );
+               }
+       }
+
+       osrfHashFree( hash );
+}
+
+/**
+       @name Managing session data
+       @brief Maintain data stored via the userData pointer of the application session.
+
+       Currently, session-level data is stored in an osrfHash.  Other arrangements are
+       possible, and some would be more efficient.  The application session calls a
+       callback function to free userData before terminating.
+
+       Currently, the only data we store at the session level is the transaction id.  By this
+       means we can ensure that any pending transactions are rolled back before the application
+       session terminates.
+*/
+/*@{*/
+
+/**
+       @brief Free an item in the application session's userData.
+       @param key The name of a key for an osrfHash.
+       @param item An opaque pointer to the item associated with the key.
+
+       We store an osrfHash as userData with the application session, and arrange (by
+       installing userDataFree() as a different callback) for the session to free that
+       osrfHash before terminating.
+
+       This function is a callback for freeing items in the osrfHash.  Currently we store
+       two things:
+       - Transaction id of a pending transaction; a character string.  Key: "xact_id".
+       - Authkey; a character string.  Key: "authkey".
+       - User object from the authentication server; a jsonObject.  Key: "user_login".
+
+       If we ever store anything else in userData, we will need to revisit this function so
+       that it will free whatever else needs freeing.
+*/
+static void sessionDataFree( char* key, void* item ) {
+       if( !strcmp( key, "xact_id" ) || !strcmp( key, "authkey" ) || !strncmp( key, "rs_size_", 8) ) 
+               free( item );
+       else if( !strcmp( key, "user_login" ) )
+               jsonObjectFree( (jsonObject*) item );
+       else if( !strcmp( key, "pcache" ) )
+               osrfHashFree( (osrfHash*) item );
+}
+
+static void pcacheFree( char* key, void* item ) {
+       osrfStringArrayFree( (osrfStringArray*) item );
+}
+
+/**
+       @brief Initialize session cache.
+       @param ctx Pointer to the method context.
+
+       Create a cache for the session by making the session's userData member point
+       to an osrfHash instance.
+*/
+static osrfHash* initSessionCache( osrfMethodContext* ctx ) {
+       ctx->session->userData = osrfNewHash();
+       osrfHashSetCallback( (osrfHash*) ctx->session->userData, &sessionDataFree );
+       ctx->session->userDataFree = &userDataFree;
+       return ctx->session->userData;
+}
+
+/**
+       @brief Save a transaction id.
+       @param ctx Pointer to the method context.
+
+       Save the session_id of the current application session as a transaction id.
+*/
+static void setXactId( osrfMethodContext* ctx ) {
+       if( ctx && ctx->session ) {
+               osrfAppSession* session = ctx->session;
+
+               osrfHash* cache = session->userData;
+
+               // If the session doesn't already have a hash, create one.  Make sure
+               // that the application session frees the hash when it terminates.
+               if( NULL == cache )
+                       cache = initSessionCache( ctx );
+
+               // Save the transaction id in the hash, with the key "xact_id"
+               osrfHashSet( cache, strdup( session->session_id ), "xact_id" );
+       }
+}
+
+/**
+       @brief Get the transaction ID for the current transaction, if any.
+       @param ctx Pointer to the method context.
+       @return Pointer to the transaction ID.
+
+       The return value points to an internal buffer, and will become invalid upon issuing
+       a commit or rollback.
+*/
+static inline const char* getXactId( osrfMethodContext* ctx ) {
+       if( ctx && ctx->session && ctx->session->userData )
+               return osrfHashGet( (osrfHash*) ctx->session->userData, "xact_id" );
+       else
+               return NULL;
+}
+
+/**
+       @brief Clear the current transaction id.
+       @param ctx Pointer to the method context.
+*/
+static inline void clearXactId( osrfMethodContext* ctx ) {
+       if( ctx && ctx->session && ctx->session->userData )
+               osrfHashRemove( ctx->session->userData, "xact_id" );
+}
+/*@}*/
+
+/**
+       @brief Stash the location for a particular perm in the sessionData cache
+       @param ctx Pointer to the method context.
+       @param perm Name of the permission we're looking at
+       @param array StringArray of perm location ids
+*/
+static void setPermLocationCache( osrfMethodContext* ctx, const char* perm, osrfStringArray* locations ) {
+       if( ctx && ctx->session ) {
+               osrfAppSession* session = ctx->session;
+
+               osrfHash* cache = session->userData;
+
+               // If the session doesn't already have a hash, create one.  Make sure
+               // that the application session frees the hash when it terminates.
+               if( NULL == cache )
+                       cache = initSessionCache( ctx );
+
+               osrfHash* pcache = osrfHashGet(cache, "pcache");
+
+               if( NULL == pcache ) {
+                       pcache = osrfNewHash();
+                       osrfHashSetCallback( pcache, &pcacheFree );
+                       osrfHashSet( cache, pcache, "pcache" );
+               }
+
+               if( perm && locations )
+                       osrfHashSet( pcache, locations, strdup(perm) );
+       }
+}
+
+/**
+       @brief Grab stashed location for a particular perm in the sessionData cache
+       @param ctx Pointer to the method context.
+       @param perm Name of the permission we're looking at
+*/
+static osrfStringArray* getPermLocationCache( osrfMethodContext* ctx, const char* perm ) {
+       if( ctx && ctx->session ) {
+               osrfAppSession* session = ctx->session;
+               osrfHash* cache = session->userData;
+               if( cache ) {
+                       osrfHash* pcache = osrfHashGet(cache, "pcache");
+                       if( pcache ) {
+                               return osrfHashGet( pcache, perm );
+                       }
+               }
+       }
+
+       return NULL;
+}
+
+/**
+       @brief Save the user's login in the userData for the current application session.
+       @param ctx Pointer to the method context.
+       @param user_login Pointer to the user login object to be cached (we cache the original,
+       not a copy of it).
+
+       If @a user_login is NULL, remove the user login if one is already cached.
+*/
+static void setUserLogin( osrfMethodContext* ctx, jsonObject* user_login ) {
+       if( ctx && ctx->session ) {
+               osrfAppSession* session = ctx->session;
+
+               osrfHash* cache = session->userData;
+
+               // If the session doesn't already have a hash, create one.  Make sure
+               // that the application session frees the hash when it terminates.
+               if( NULL == cache )
+                       cache = initSessionCache( ctx );
+
+               if( user_login )
+                       osrfHashSet( cache, user_login, "user_login" );
+               else
+                       osrfHashRemove( cache, "user_login" );
+       }
+}
+
+/**
+       @brief Get the user login object for the current application session, if any.
+       @param ctx Pointer to the method context.
+       @return Pointer to the user login object if found; otherwise NULL.
+
+       The user login object was returned from the authentication server, and then cached so
+       we don't have to call the authentication server again for the same user.
+*/
+static const jsonObject* getUserLogin( osrfMethodContext* ctx ) {
+       if( ctx && ctx->session && ctx->session->userData )
+               return osrfHashGet( (osrfHash*) ctx->session->userData, "user_login" );
+       else
+               return NULL;
+}
+
+/**
+       @brief Save a copy of an authkey in the userData of the current application session.
+       @param ctx Pointer to the method context.
+       @param authkey The authkey to be saved.
+
+       If @a authkey is NULL, remove the authkey if one is already cached.
+*/
+static void setAuthkey( osrfMethodContext* ctx, const char* authkey ) {
+       if( ctx && ctx->session && authkey ) {
+               osrfAppSession* session = ctx->session;
+               osrfHash* cache = session->userData;
+
+               // If the session doesn't already have a hash, create one.  Make sure
+               // that the application session frees the hash when it terminates.
+               if( NULL == cache )
+                       cache = initSessionCache( ctx );
+
+               // Save the transaction id in the hash, with the key "xact_id"
+               if( authkey && *authkey )
+                       osrfHashSet( cache, strdup( authkey ), "authkey" );
+               else
+                       osrfHashRemove( cache, "authkey" );
+       }
+}
+
+/**
+       @brief Reset the login timeout.
+       @param authkey The authentication key for the current login session.
+       @param now The current time.
+       @return Zero if successful, or 1 if not.
+
+       Tell the authentication server to reset the timeout so that the login session won't
+       expire for a while longer.
+
+       We could dispense with the @a now parameter by calling time().  But we just called
+       time() in order to decide whether to reset the timeout, so we might as well reuse
+       the result instead of calling time() again.
+*/
+static int reset_timeout( const char* authkey, time_t now ) {
+       jsonObject* auth_object = jsonNewObject( authkey );
+
+       // Ask the authentication server to reset the timeout.  It returns an event
+       // indicating success or failure.
+       jsonObject* result = oilsUtilsQuickReq( "open-ils.auth",
+               "open-ils.auth.session.reset_timeout", auth_object );
+       jsonObjectFree( auth_object );
+
+       if( !result || result->type != JSON_HASH ) {
+               osrfLogError( OSRF_LOG_MARK,
+                        "Unexpected object type receieved from open-ils.auth.session.reset_timeout" );
+               jsonObjectFree( result );
+               return 1;       // Not the right sort of object returned
+       }
+
+       const jsonObject* ilsevent = jsonObjectGetKeyConst( result, "ilsevent" );
+       if( !ilsevent || ilsevent->type != JSON_NUMBER ) {
+               osrfLogError( OSRF_LOG_MARK, "ilsevent is absent or malformed" );
+               jsonObjectFree( result );
+               return 1;    // Return code from method not available
+       }
+
+       if( jsonObjectGetNumber( ilsevent ) != 0.0 ) {
+               const char* desc = jsonObjectGetString( jsonObjectGetKeyConst( result, "desc" ));
+               if( !desc )
+                       desc = "(No reason available)";    // failsafe; shouldn't happen
+               osrfLogInfo( OSRF_LOG_MARK, "Failure to reset timeout: %s", desc );
+               jsonObjectFree( result );
+               return 1;
+       }
+
+       // Revise our local proxy for the timeout deadline
+       // by a smallish fraction of the timeout interval
+       const char* timeout = jsonObjectGetString( jsonObjectGetKeyConst( result, "payload" ));
+       if( !timeout )
+               timeout = "1";   // failsafe; shouldn't happen
+       time_next_reset = now + atoi( timeout ) / 15;
+
+       jsonObjectFree( result );
+       return 0;     // Successfully reset timeout
+}
+
+/**
+       @brief Get the authkey string for the current application session, if any.
+       @param ctx Pointer to the method context.
+       @return Pointer to the cached authkey if found; otherwise NULL.
+
+       If present, the authkey string was cached from a previous method call.
+*/
+static const char* getAuthkey( osrfMethodContext* ctx ) {
+       if( ctx && ctx->session && ctx->session->userData ) {
+               const char* authkey = osrfHashGet( (osrfHash*) ctx->session->userData, "authkey" );
+        // LFW recent changes mean the userData hash gets set up earlier, but
+        // doesn't necessarily have an authkey yet
+        if (!authkey)
+            return NULL;
+
+               // Possibly reset the authentication timeout to keep the login alive.  We do so
+               // no more than once per method call, and not at all if it has been only a short
+               // time since the last reset.
+
+               // Here we reset explicitly, if at all.  We also implicitly reset the timeout
+               // whenever we call the "open-ils.auth.session.retrieve" method.
+               if( timeout_needs_resetting ) {
+                       time_t now = time( NULL );
+                       if( now >= time_next_reset && reset_timeout( authkey, now ) )
+                               authkey = NULL;    // timeout has apparently expired already
+               }
+
+               timeout_needs_resetting = 0;
+               return authkey;
+       }
+       else
+               return NULL;
+}
+
+/**
+       @brief Implement the transaction.begin method.
+       @param ctx Pointer to the method context.
+       @return Zero if successful, or -1 upon error.
+
+       Start a transaction.  Save a transaction ID for future reference.
+
+       Method parameters:
+       - authkey (PCRUD only)
+
+       Return to client: Transaction ID
+*/
+int beginTransaction( osrfMethodContext* ctx ) {
+       if(osrfMethodVerifyContext( ctx )) {
+               osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
+               return -1;
+       }
+
+       if( enforce_pcrud ) {
+               timeout_needs_resetting = 1;
+               const jsonObject* user = verifyUserPCRUD( ctx );
+               if( !user )
+                       return -1;
+       }
+
+       dbi_result result = dbi_conn_query( writehandle, "START TRANSACTION;" );
+       if( !result ) {
+               const char* msg;
+               int errnum = dbi_conn_error( writehandle, &msg );
+               osrfLogError( OSRF_LOG_MARK, "%s: Error starting transaction: %d %s",
+                       modulename, errnum, msg ? msg : "(No description available)" );
+               osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+                       "osrfMethodException", ctx->request, "Error starting transaction" );
+               if( !oilsIsDBConnected( writehandle ))
+                       osrfAppSessionPanic( ctx->session );
+               return -1;
+       } else {
+               dbi_result_free( result );
+               setXactId( ctx );
+               jsonObject* ret = jsonNewObject( getXactId( ctx ) );
+               osrfAppRespondComplete( ctx, ret );
+               jsonObjectFree( ret );
+               return 0;
+       }
+}
+
+/**
+       @brief Implement the savepoint.set method.
+       @param ctx Pointer to the method context.
+       @return Zero if successful, or -1 if not.
+
+       Issue a SAVEPOINT to the database server.
+
+       Method parameters:
+       - authkey (PCRUD only)
+       - savepoint name
+
+       Return to client: Savepoint name
+*/
+int setSavepoint( osrfMethodContext* ctx ) {
+       if(osrfMethodVerifyContext( ctx )) {
+               osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
+               return -1;
+       }
+
+       int spNamePos = 0;
+       if( enforce_pcrud ) {
+               spNamePos = 1;
+               timeout_needs_resetting = 1;
+               const jsonObject* user = verifyUserPCRUD( ctx );
+               if( !user )
+                       return -1;
+       }
+
+       // Verify that a transaction is pending
+       const char* trans_id = getXactId( ctx );
+       if( NULL == trans_id ) {
+               osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_INTERNALSERVERERROR,
+                       "osrfMethodException",
+                       ctx->request,
+                       "No active transaction -- required for savepoints"
+               );
+               return -1;
+       }
+
+       // Get the savepoint name from the method params
+       const char* spName = jsonObjectGetString( jsonObjectGetIndex(ctx->params, spNamePos) );
+
+       if (!spName) {
+               osrfLogWarning(OSRF_LOG_MARK, "savepoint.set called with no name");
+               return -1;
+       }
+
+       char *safeSpName = _sanitize_savepoint_name( spName );
+
+       dbi_result result = dbi_conn_queryf( writehandle, "SAVEPOINT \"%s\";", safeSpName );
+       free( safeSpName );
+       if( !result ) {
+               const char* msg;
+               int errnum = dbi_conn_error( writehandle, &msg );
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s: Error creating savepoint %s in transaction %s: %d %s",
+                       modulename,
+                       spName,
+                       trans_id,
+                       errnum,
+                       msg ? msg : "(No description available)"
+               );
+               osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+                       "osrfMethodException", ctx->request, "Error creating savepoint" );
+               if( !oilsIsDBConnected( writehandle ))
+                       osrfAppSessionPanic( ctx->session );
+               return -1;
+       } else {
+               dbi_result_free( result );
+               jsonObject* ret = jsonNewObject( spName );
+               osrfAppRespondComplete( ctx, ret );
+               jsonObjectFree( ret  );
+               return 0;
+       }
+}
+
+/**
+       @brief Implement the savepoint.release method.
+       @param ctx Pointer to the method context.
+       @return Zero if successful, or -1 if not.
+
+       Issue a RELEASE SAVEPOINT to the database server.
+
+       Method parameters:
+       - authkey (PCRUD only)
+       - savepoint name
+
+       Return to client: Savepoint name
+*/
+int releaseSavepoint( osrfMethodContext* ctx ) {
+       if(osrfMethodVerifyContext( ctx )) {
+               osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
+               return -1;
+       }
+
+       int spNamePos = 0;
+       if( enforce_pcrud ) {
+               spNamePos = 1;
+               timeout_needs_resetting = 1;
+               const jsonObject* user = verifyUserPCRUD( ctx );
+               if(  !user )
+                       return -1;
+       }
+
+       // Verify that a transaction is pending
+       const char* trans_id = getXactId( ctx );
+       if( NULL == trans_id ) {
+               osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_INTERNALSERVERERROR,
+                       "osrfMethodException",
+                       ctx->request,
+                       "No active transaction -- required for savepoints"
+               );
+               return -1;
+       }
+
+       // Get the savepoint name from the method params
+       const char* spName = jsonObjectGetString( jsonObjectGetIndex(ctx->params, spNamePos) );
+
+       if (!spName) {
+               osrfLogWarning(OSRF_LOG_MARK, "savepoint.release called with no name");
+               return -1;
+       }
+
+       char *safeSpName = _sanitize_savepoint_name( spName );
+
+       dbi_result result = dbi_conn_queryf( writehandle, "RELEASE SAVEPOINT \"%s\";", safeSpName );
+       free( safeSpName );
+       if( !result ) {
+               const char* msg;
+               int errnum = dbi_conn_error( writehandle, &msg );
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s: Error releasing savepoint %s in transaction %s: %d %s",
+                       modulename,
+                       spName,
+                       trans_id,
+                       errnum,
+                       msg ? msg : "(No description available)"
+               );
+               osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+                       "osrfMethodException", ctx->request, "Error releasing savepoint" );
+               if( !oilsIsDBConnected( writehandle ))
+                       osrfAppSessionPanic( ctx->session );
+               return -1;
+       } else {
+               dbi_result_free( result );
+               jsonObject* ret = jsonNewObject( spName );
+               osrfAppRespondComplete( ctx, ret );
+               jsonObjectFree( ret );
+               return 0;
+       }
+}
+
+/**
+       @brief Implement the savepoint.rollback method.
+       @param ctx Pointer to the method context.
+       @return Zero if successful, or -1 if not.
+
+       Issue a ROLLBACK TO SAVEPOINT to the database server.
+
+       Method parameters:
+       - authkey (PCRUD only)
+       - savepoint name
+
+       Return to client: Savepoint name
+*/
+int rollbackSavepoint( osrfMethodContext* ctx ) {
+       if(osrfMethodVerifyContext( ctx )) {
+               osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
+               return -1;
+       }
+
+       int spNamePos = 0;
+       if( enforce_pcrud ) {
+               spNamePos = 1;
+               timeout_needs_resetting = 1;
+               const jsonObject* user = verifyUserPCRUD( ctx );
+               if( !user )
+                       return -1;
+       }
+
+       // Verify that a transaction is pending
+       const char* trans_id = getXactId( ctx );
+       if( NULL == trans_id ) {
+               osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_INTERNALSERVERERROR,
+                       "osrfMethodException",
+                       ctx->request,
+                       "No active transaction -- required for savepoints"
+               );
+               return -1;
+       }
+
+       // Get the savepoint name from the method params
+       const char* spName = jsonObjectGetString( jsonObjectGetIndex(ctx->params, spNamePos) );
+
+       if (!spName) {
+               osrfLogWarning(OSRF_LOG_MARK, "savepoint.rollback called with no name");
+               return -1;
+       }
+
+       char *safeSpName = _sanitize_savepoint_name( spName );
+
+       dbi_result result = dbi_conn_queryf( writehandle, "ROLLBACK TO SAVEPOINT \"%s\";", safeSpName );
+       free( safeSpName );
+       if( !result ) {
+               const char* msg;
+               int errnum = dbi_conn_error( writehandle, &msg );
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s: Error rolling back savepoint %s in transaction %s: %d %s",
+                       modulename,
+                       spName,
+                       trans_id,
+                       errnum,
+                       msg ? msg : "(No description available)"
+               );
+               osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+                       "osrfMethodException", ctx->request, "Error rolling back savepoint" );
+               if( !oilsIsDBConnected( writehandle ))
+                       osrfAppSessionPanic( ctx->session );
+               return -1;
+       } else {
+               dbi_result_free( result );
+               jsonObject* ret = jsonNewObject( spName );
+               osrfAppRespondComplete( ctx, ret );
+               jsonObjectFree( ret );
+               return 0;
+       }
+}
+
+/**
+       @brief Implement the transaction.commit method.
+       @param ctx Pointer to the method context.
+       @return Zero if successful, or -1 if not.
+
+       Issue a COMMIT to the database server.
+
+       Method parameters:
+       - authkey (PCRUD only)
+
+       Return to client: Transaction ID.
+*/
+int commitTransaction( osrfMethodContext* ctx ) {
+       if(osrfMethodVerifyContext( ctx )) {
+               osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+               return -1;
+       }
+
+       if( enforce_pcrud ) {
+               timeout_needs_resetting = 1;
+               const jsonObject* user = verifyUserPCRUD( ctx );
+               if( !user )
+                       return -1;
+       }
+
+       // Verify that a transaction is pending
+       const char* trans_id = getXactId( ctx );
+       if( NULL == trans_id ) {
+               osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException", ctx->request, "No active transaction to commit" );
+               return -1;
+       }
+
+       dbi_result result = dbi_conn_query( writehandle, "COMMIT;" );
+       if( !result ) {
+               const char* msg;
+               int errnum = dbi_conn_error( writehandle, &msg );
+               osrfLogError( OSRF_LOG_MARK, "%s: Error committing transaction: %d %s",
+                       modulename, errnum, msg ? msg : "(No description available)" );
+               osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+                       "osrfMethodException", ctx->request, "Error committing transaction" );
+               if( !oilsIsDBConnected( writehandle ))
+                       osrfAppSessionPanic( ctx->session );
+               return -1;
+       } else {
+               dbi_result_free( result );
+               jsonObject* ret = jsonNewObject( trans_id );
+               osrfAppRespondComplete( ctx, ret );
+               jsonObjectFree( ret );
+               clearXactId( ctx );
+               return 0;
+       }
+}
+
+/**
+       @brief Implement the transaction.rollback method.
+       @param ctx Pointer to the method context.
+       @return Zero if successful, or -1 if not.
+
+       Issue a ROLLBACK to the database server.
+
+       Method parameters:
+       - authkey (PCRUD only)
+
+       Return to client: Transaction ID
+*/
+int rollbackTransaction( osrfMethodContext* ctx ) {
+       if( osrfMethodVerifyContext( ctx )) {
+               osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
+               return -1;
+       }
+
+       if( enforce_pcrud ) {
+               timeout_needs_resetting = 1;
+               const jsonObject* user = verifyUserPCRUD( ctx );
+               if( !user )
+                       return -1;
+       }
+
+       // Verify that a transaction is pending
+       const char* trans_id = getXactId( ctx );
+       if( NULL == trans_id ) {
+               osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException", ctx->request, "No active transaction to roll back" );
+               return -1;
+       }
+
+       dbi_result result = dbi_conn_query( writehandle, "ROLLBACK;" );
+       if( !result ) {
+               const char* msg;
+               int errnum = dbi_conn_error( writehandle, &msg );
+               osrfLogError( OSRF_LOG_MARK, "%s: Error rolling back transaction: %d %s",
+                       modulename, errnum, msg ? msg : "(No description available)" );
+               osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+                       "osrfMethodException", ctx->request, "Error rolling back transaction" );
+               if( !oilsIsDBConnected( writehandle ))
+                       osrfAppSessionPanic( ctx->session );
+               return -1;
+       } else {
+               dbi_result_free( result );
+               jsonObject* ret = jsonNewObject( trans_id );
+               osrfAppRespondComplete( ctx, ret );
+               jsonObjectFree( ret );
+               clearXactId( ctx );
+               return 0;
+       }
+}
+
+/**
+       @brief Implement the "search" method.
+       @param ctx Pointer to the method context.
+       @return Zero if successful, or -1 if not.
+
+       Method parameters:
+       - authkey (PCRUD only)
+       - WHERE clause, as jsonObject
+       - Other SQL clause(s), as a JSON_HASH: joins, SELECT list, LIMIT, etc.
+
+       Return to client: rows of the specified class that satisfy a specified WHERE clause.
+       Optionally flesh linked fields.
+*/
+int doSearch( osrfMethodContext* ctx ) {
+       if( osrfMethodVerifyContext( ctx )) {
+               osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+               return -1;
+       }
+
+       if( enforce_pcrud )
+               timeout_needs_resetting = 1;
+
+       jsonObject* where_clause;
+       jsonObject* rest_of_query;
+
+       if( enforce_pcrud ) {
+               where_clause  = jsonObjectGetIndex( ctx->params, 1 );
+               rest_of_query = jsonObjectGetIndex( ctx->params, 2 );
+       } else {
+               where_clause  = jsonObjectGetIndex( ctx->params, 0 );
+               rest_of_query = jsonObjectGetIndex( ctx->params, 1 );
+       }
+
+       if( !where_clause ) { 
+               osrfLogError( OSRF_LOG_MARK, "No WHERE clause parameter supplied" );
+               return -1;
+       }
+
+       // Get the class metadata
+       osrfHash* method_meta = (osrfHash*) ctx->method->userData;
+       osrfHash* class_meta = osrfHashGet( method_meta, "class" );
+
+       // Do the query
+       int err = 0;
+       jsonObject* obj = doFieldmapperSearch( ctx, class_meta, where_clause, rest_of_query, &err );
+       if( err ) {
+               osrfAppRespondComplete( ctx, NULL );
+               return -1;
+       }
+
+       // doFieldmapperSearch() now takes care of our responding for us
+//     // Return each row to the client
+//     jsonObject* cur = 0;
+//     unsigned long res_idx = 0;
+//
+//     while((cur = jsonObjectGetIndex( obj, res_idx++ ) )) {
+//             // We used to discard based on perms here, but now that's
+//             // inside doFieldmapperSearch()
+//             osrfAppRespond( ctx, cur );
+//     }
+
+       jsonObjectFree( obj );
+
+       osrfAppRespondComplete( ctx, NULL );
+       return 0;
+}
+
+/**
+       @brief Implement the "id_list" method.
+       @param ctx Pointer to the method context.
+       @param err Pointer through which to return an error code.
+       @return Zero if successful, or -1 if not.
+
+       Method parameters:
+       - authkey (PCRUD only)
+       - WHERE clause, as jsonObject
+       - Other SQL clause(s), as a JSON_HASH: joins, LIMIT, etc.
+
+       Return to client: The primary key values for all rows of the relevant class that
+       satisfy a specified WHERE clause.
+
+       This method relies on the assumption that every class has a primary key consisting of
+       a single column.
+*/
+int doIdList( osrfMethodContext* ctx ) {
+       if( osrfMethodVerifyContext( ctx )) {
+               osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+               return -1;
+       }
+
+       if( enforce_pcrud )
+               timeout_needs_resetting = 1;
+
+       jsonObject* where_clause;
+       jsonObject* rest_of_query;
+
+       // We use the where clause without change.  But we need to massage the rest of the
+       // query, so we work with a copy of it instead of modifying the original.
+
+       if( enforce_pcrud ) {
+               where_clause  = jsonObjectGetIndex( ctx->params, 1 );
+               rest_of_query = jsonObjectClone( jsonObjectGetIndex( ctx->params, 2 ) );
+       } else {
+               where_clause  = jsonObjectGetIndex( ctx->params, 0 );
+               rest_of_query = jsonObjectClone( jsonObjectGetIndex( ctx->params, 1 ) );
+       }
+
+       if( !where_clause ) { 
+               osrfLogError( OSRF_LOG_MARK, "No WHERE clause parameter supplied" );
+               return -1;
+       }
+
+       // Eliminate certain SQL clauses, if present.
+       if( rest_of_query ) {
+               jsonObjectRemoveKey( rest_of_query, "select" );
+               jsonObjectRemoveKey( rest_of_query, "no_i18n" );
+               jsonObjectRemoveKey( rest_of_query, "flesh" );
+               jsonObjectRemoveKey( rest_of_query, "flesh_fields" );
+       } else {
+               rest_of_query = jsonNewObjectType( JSON_HASH );
+       }
+
+       jsonObjectSetKey( rest_of_query, "no_i18n", jsonNewBoolObject( 1 ) );
+
+       // Get the class metadata
+       osrfHash* method_meta = (osrfHash*) ctx->method->userData;
+       osrfHash* class_meta = osrfHashGet( method_meta, "class" );
+
+       // Build a SELECT list containing just the primary key,
+       // i.e. like { "classname":["keyname"] }
+       jsonObject* col_list_obj = jsonNewObjectType( JSON_ARRAY );
+
+       // Load array with name of primary key
+       jsonObjectPush( col_list_obj, jsonNewObject( osrfHashGet( class_meta, "primarykey" ) ) );
+       jsonObject* select_clause = jsonNewObjectType( JSON_HASH );
+       jsonObjectSetKey( select_clause, osrfHashGet( class_meta, "classname" ), col_list_obj );
+
+       jsonObjectSetKey( rest_of_query, "select", select_clause );
+
+       // Do the query
+       int err = 0;
+       jsonObject* obj =
+               doFieldmapperSearch( ctx, class_meta, where_clause, rest_of_query, &err );
+
+       jsonObjectFree( rest_of_query );
+       if( err ) {
+               osrfAppRespondComplete( ctx, NULL );
+               return -1;
+       }
+
+       // Return each primary key value to the client
+       jsonObject* cur;
+       unsigned long res_idx = 0;
+       while((cur = jsonObjectGetIndex( obj, res_idx++ ) )) {
+               // We used to discard based on perms here, but now that's
+               // inside doFieldmapperSearch()
+               osrfAppRespond( ctx,
+                       oilsFMGetObject( cur, osrfHashGet( class_meta, "primarykey" ) ) );
+       }
+
+       jsonObjectFree( obj );
+       osrfAppRespondComplete( ctx, NULL );
+       return 0;
+}
+
+/**
+       @brief Verify that we have a valid class reference.
+       @param ctx Pointer to the method context.
+       @param param Pointer to the method parameters.
+       @return 1 if the class reference is valid, or zero if it isn't.
+
+       The class of the method params must match the class to which the method id devoted.
+       For PCRUD there are additional restrictions.
+*/
+static int verifyObjectClass ( osrfMethodContext* ctx, const jsonObject* param ) {
+
+       osrfHash* method_meta = (osrfHash*) ctx->method->userData;
+       osrfHash* class = osrfHashGet( method_meta, "class" );
+
+       // Compare the method's class to the parameters' class
+       if( !param->classname || (strcmp( osrfHashGet(class, "classname"), param->classname ))) {
+
+               // Oops -- they don't match.  Complain.
+               growing_buffer* msg = buffer_init( 128 );
+               buffer_fadd(
+                       msg,
+                       "%s: %s method for type %s was passed a %s",
+                       modulename,
+                       osrfHashGet( method_meta, "methodtype" ),
+                       osrfHashGet( class, "classname" ),
+                       param->classname ? param->classname : "(null)"
+               );
+
+               char* m = buffer_release( msg );
+               osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
+                               ctx->request, m );
+               free( m );
+
+               return 0;
+       }
+
+       if( enforce_pcrud )
+               return verifyObjectPCRUD( ctx, class, param, 1 );
+       else
+               return 1;
+}
+
+/**
+       @brief (PCRUD only) Verify that the user is properly logged in.
+       @param ctx Pointer to the method context.
+       @return If the user is logged in, a pointer to the user object from the authentication
+       server; otherwise NULL.
+*/
+static const jsonObject* verifyUserPCRUD( osrfMethodContext* ctx ) {
+
+       // Get the authkey (the first method parameter)
+       const char* auth = jsonObjectGetString( jsonObjectGetIndex( ctx->params, 0 ) );
+
+       // See if we have the same authkey, and a user object,
+       // locally cached from a previous call
+       const char* cached_authkey = getAuthkey( ctx );
+       if( cached_authkey && !strcmp( cached_authkey, auth ) ) {
+               const jsonObject* cached_user = getUserLogin( ctx );
+               if( cached_user )
+                       return cached_user;
+       }
+
+       // We have no matching authentication data in the cache.  Authenticate from scratch.
+       jsonObject* auth_object = jsonNewObject( auth );
+
+       // Fetch the user object from the authentication server
+       jsonObject* user = oilsUtilsQuickReq( "open-ils.auth", "open-ils.auth.session.retrieve",
+                       auth_object );
+       jsonObjectFree( auth_object );
+
+       if( !user->classname || strcmp(user->classname, "au" )) {
+
+               growing_buffer* msg = buffer_init( 128 );
+               buffer_fadd(
+                       msg,
+                       "%s: permacrud received a bad auth token: %s",
+                       modulename,
+                       auth
+               );
+
+               char* m = buffer_release( msg );
+               osrfAppSessionStatus( ctx->session, OSRF_STATUS_UNAUTHORIZED, "osrfMethodException",
+                               ctx->request, m );
+
+               free( m );
+               jsonObjectFree( user );
+               user = NULL;
+       } else if( writeAuditInfo( ctx, oilsFMGetStringConst( user, "id" ), oilsFMGetStringConst( user, "wsid" ) ) ) {
+               // Failed to set audit information - But note that write_audit_info already set error information.
+               jsonObjectFree( user );
+               user = NULL;
+       }
+
+       setUserLogin( ctx, user );
+       setAuthkey( ctx, auth );
+
+       // Allow ourselves up to a second before we have to reset the login timeout.
+       // It would be nice to use some fraction of the timeout interval enforced by the
+       // authentication server, but that value is not readily available at this point.
+       // Instead, we use a conservative default interval.
+       time_next_reset = time( NULL ) + 1;
+
+       return user;
+}
+
+/**
+       @brief For PCRUD: Determine whether the current user may access the current row.
+       @param ctx Pointer to the method context.
+       @param class Same as ctx->method->userData's item for key "class" except when called in recursive doFieldmapperSearch
+       @param obj Pointer to the row being potentially accessed.
+       @return 1 if access is permitted, or 0 if it isn't.
+
+       The @a obj parameter points to a JSON_HASH of column values, keyed on column name.
+*/
+static int verifyObjectPCRUD ( osrfMethodContext* ctx, osrfHash *class, const jsonObject* obj, int rs_size ) {
+
+       dbhandle = writehandle;
+
+       // Figure out what class and method are involved
+       osrfHash* method_metadata = (osrfHash*) ctx->method->userData;
+       const char* method_type = osrfHashGet( method_metadata, "methodtype" );
+
+       if (!rs_size) {
+               int *rs_size_from_hash = osrfHashGetFmt( (osrfHash *) ctx->session->userData, "rs_size_req_%d", ctx->request );
+               if (rs_size_from_hash) {
+                       rs_size = *rs_size_from_hash;
+                       osrfLogDebug(OSRF_LOG_MARK, "used rs_size from request-scoped hash: %d", rs_size);
+               }
+       }
+
+       // Set fetch to 1 in all cases except for inserts, meaning that for local or foreign
+       // contexts we will do another lookup of the current row, even if we already have a
+       // previously fetched row image, because the row image in hand may not include the
+       // foreign key(s) that we need.
+
+       // This is a quick fix with a bludgeon.  There are ways to avoid the extra lookup,
+       // but they aren't implemented yet.
+
+       int fetch = 0;
+       if( *method_type == 's' || *method_type == 'i' ) {
+               method_type = "retrieve"; // search and id_list are equivalent to retrieve for this
+               fetch = 1;
+       } else if( *method_type == 'u' || *method_type == 'd' ) {
+               fetch = 1; // MUST go to the db for the object for update and delete
+       }
+
+       // Get the appropriate permacrud entry from the IDL, depending on method type
+       osrfHash* pcrud = osrfHashGet( osrfHashGet( class, "permacrud" ), method_type );
+       if( !pcrud ) {
+               // No permacrud for this method type on this class
+
+               growing_buffer* msg = buffer_init( 128 );
+               buffer_fadd(
+                       msg,
+                       "%s: %s on class %s has no permacrud IDL entry",
+                       modulename,
+                       osrfHashGet( method_metadata, "methodtype" ),
+                       osrfHashGet( class, "classname" )
+               );
+
+               char* m = buffer_release( msg );
+               osrfAppSessionStatus( ctx->session, OSRF_STATUS_FORBIDDEN,
+                               "osrfMethodException", ctx->request, m );
+
+               free( m );
+
+               return 0;
+       }
+
+       // Get the user id, and make sure the user is logged in
+       const jsonObject* user = verifyUserPCRUD( ctx );
+       if( !user )
+               return 0;    // Not logged in?  No access.
+
+       int userid = atoi( oilsFMGetStringConst( user, "id" ) );
+
+       // Get a list of permissions from the permacrud entry.
+       osrfStringArray* permission = osrfHashGet( pcrud, "permission" );
+       if( permission->size == 0 ) {
+               osrfLogDebug(
+                       OSRF_LOG_MARK,
+                       "No permissions required for this action (class %s), passing through",
+                       osrfHashGet(class, "classname")
+               );
+               return 1;
+       }
+
+       // Build a list of org units that own the row.  This is fairly convoluted because there
+       // are several different ways that an org unit may own the row, as defined by the
+       // permacrud entry.
+
+       // Local context means that the row includes a foreign key pointing to actor.org_unit,
+       // identifying an owning org_unit..
+       osrfStringArray* local_context = osrfHashGet( pcrud, "local_context" );
+
+       // Foreign context adds a layer of indirection.  The row points to some other row that
+       // an org unit may own.  The "jump" attribute, if present, adds another layer of
+       // indirection.
+       osrfHash* foreign_context = osrfHashGet( pcrud, "foreign_context" );
+
+       // The following string array stores the list of org units.  (We don't have a thingie
+       // for storing lists of integers, so we fake it with a list of strings.)
+       osrfStringArray* context_org_array = osrfNewStringArray( 1 );
+
+       int err = 0;
+       const char* pkey_value = NULL;
+       if( str_is_true( osrfHashGet(pcrud, "global_required") ) ) {
+               // If the global_required attribute is present and true, then the only owning
+               // org unit is the root org unit, i.e. the one with no parent.
+               osrfLogDebug( OSRF_LOG_MARK,
+                               "global-level permissions required, fetching top of the org tree" );
+
+               // no need to check perms for org tree root retrieval
+               osrfHashSet((osrfHash*) ctx->session->userData, "1", "inside_verify");
+               // check for perm at top of org tree
+               const char* org_tree_root_id = org_tree_root( ctx );
+               osrfHashSet((osrfHash*) ctx->session->userData, "0", "inside_verify");
+
+               if( org_tree_root_id ) {
+                       osrfStringArrayAdd( context_org_array, org_tree_root_id );
+                       osrfLogDebug( OSRF_LOG_MARK, "top of the org tree is %s", org_tree_root_id );
+               } else  {
+                       osrfStringArrayFree( context_org_array );
+                       return 0;
+               }
+
+       } else {
+               // If the global_required attribute is absent or false, then we look for
+               // local and/or foreign context.  In order to find the relevant foreign
+               // keys, we must either read the relevant row from the database, or look at
+               // the image of the row that we already have in memory.
+
+               // Even if we have an image of the row in memory, that image may not include the
+               // foreign key column(s) that we need.  So whenever possible, we do a fresh read
+               // of the row to make sure that we have what we need.
+
+           osrfLogDebug( OSRF_LOG_MARK, "global-level permissions not required, "
+                               "fetching context org ids" );
+           const char* pkey = osrfHashGet( class, "primarykey" );
+               jsonObject *param = NULL;
+
+               if( !pkey ) {
+                       // There is no primary key, so we can't do a fresh lookup.  Use the row
+                       // image that we already have.  If it doesn't have everything we need, too bad.
+                       fetch = 0;
+                       param = jsonObjectClone( obj );
+                       osrfLogDebug( OSRF_LOG_MARK, "No primary key; using clone of object" );
+               } else if( obj->classname ) {
+                       pkey_value = oilsFMGetStringConst( obj, pkey );
+                       if( !fetch )
+                               param = jsonObjectClone( obj );
+                       osrfLogDebug( OSRF_LOG_MARK, "Object supplied, using primary key value of %s",
+                               pkey_value );
+               } else {
+                       pkey_value = jsonObjectGetString( obj );
+                       fetch = 1;
+                       osrfLogDebug( OSRF_LOG_MARK, "Object not supplied, using primary key value "
+                               "of %s and retrieving from the database", pkey_value );
+               }
+
+               if( fetch ) {
+                       // Fetch the row so that we can look at the foreign key(s)
+                       osrfHashSet((osrfHash*) ctx->session->userData, "1", "inside_verify");
+                       jsonObject* _tmp_params = single_hash( pkey, pkey_value );
+                       jsonObject* _list = doFieldmapperSearch( ctx, class, _tmp_params, NULL, &err );
+                       jsonObjectFree( _tmp_params );
+                       osrfHashSet((osrfHash*) ctx->session->userData, "0", "inside_verify");
+
+                       param = jsonObjectExtractIndex( _list, 0 );
+                       jsonObjectFree( _list );
+               }
+
+               if( !param ) {
+                       // The row doesn't exist.  Complain, and deny access.
+                       osrfLogDebug( OSRF_LOG_MARK,
+                                       "Object not found in the database with primary key %s of %s",
+                                       pkey, pkey_value );
+
+                       growing_buffer* msg = buffer_init( 128 );
+                       buffer_fadd(
+                               msg,
+                               "%s: no object found with primary key %s of %s",
+                               modulename,
+                               pkey,
+                               pkey_value
+                       );
+
+                       char* m = buffer_release( msg );
+                       osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               m
+                       );
+
+                       free( m );
+                       return 0;
+               }
+
+               if( local_context && local_context->size > 0 ) {
+                       // The IDL provides a list of column names for the foreign keys denoting
+                       // local context, i.e. columns identifying owing org units directly.  Look up
+                       // the value of each one, and if it isn't null, add it to the list of org units.
+                       osrfLogDebug( OSRF_LOG_MARK, "%d class-local context field(s) specified",
+                               local_context->size );
+                       int i = 0;
+                       const char* lcontext = NULL;
+                       while ( (lcontext = osrfStringArrayGetString(local_context, i++)) ) {
+                               const char* fkey_value = oilsFMGetStringConst( param, lcontext );
+                               if( fkey_value ) {    // if not null
+                                       osrfStringArrayAdd( context_org_array, fkey_value );
+                                       osrfLogDebug(
+                                               OSRF_LOG_MARK,
+                                               "adding class-local field %s (value: %s) to the context org list",
+                                               lcontext,
+                                               osrfStringArrayGetString( context_org_array, context_org_array->size - 1 )
+                                       );
+                               }
+                       }
+               }
+
+               if( foreign_context ) {
+                       unsigned long class_count = osrfHashGetCount( foreign_context );
+                       osrfLogDebug( OSRF_LOG_MARK, "%d foreign context classes(s) specified", class_count );
+
+                       if( class_count > 0 ) {
+
+                               // The IDL provides a list of foreign key columns pointing to rows that
+                               // an org unit may own.  Follow each link, identify the owning org unit,
+                               // and add it to the list.
+                               osrfHash* fcontext = NULL;
+                               osrfHashIterator* class_itr = osrfNewHashIterator( foreign_context );
+                               while( (fcontext = osrfHashIteratorNext( class_itr )) ) {
+                                       // For each class to which a foreign key points:
+                                       const char* class_name = osrfHashIteratorKey( class_itr );
+                                       osrfHash* fcontext = osrfHashGet( foreign_context, class_name );
+
+                                       osrfLogDebug(
+                                               OSRF_LOG_MARK,
+                                               "%d foreign context fields(s) specified for class %s",
+                                               ((osrfStringArray*)osrfHashGet(fcontext,"context"))->size,
+                                               class_name
+                                       );
+
+                                       // Get the name of the key field in the foreign table
+                                       const char* foreign_pkey = osrfHashGet( fcontext, "field" );
+
+                                       // Get the value of the foreign key pointing to the foreign table
+                                       char* foreign_pkey_value =
+                                                       oilsFMGetString( param, osrfHashGet( fcontext, "fkey" ));
+                                       if( !foreign_pkey_value )
+                                               continue;    // Foreign key value is null; skip it
+
+                                       // Look up the row to which the foreign key points
+                                       jsonObject* _tmp_params = single_hash( foreign_pkey, foreign_pkey_value );
+
+                                       osrfHashSet((osrfHash*) ctx->session->userData, "1", "inside_verify");
+                                       jsonObject* _list = doFieldmapperSearch(
+                                               ctx, osrfHashGet( oilsIDL(), class_name ), _tmp_params, NULL, &err );
+                                       osrfHashSet((osrfHash*) ctx->session->userData, "0", "inside_verify");
+
+                                       jsonObject* _fparam = NULL;
+                                       if( _list && JSON_ARRAY == _list->type && _list->size > 0 )
+                                               _fparam = jsonObjectExtractIndex( _list, 0 );
+
+                                       jsonObjectFree( _tmp_params );
+                                       jsonObjectFree( _list );
+
+                                       // At this point _fparam either points to the row identified by the
+                                       // foreign key, or it's NULL (no such row found).
+
+                                       osrfStringArray* jump_list = osrfHashGet( fcontext, "jump" );
+
+                                       const char* bad_class = NULL;  // For noting failed lookups
+                                       if( ! _fparam )
+                                               bad_class = class_name;    // Referenced row not found
+                                       else if( jump_list ) {
+                                               // Follow a chain of rows, linked by foreign keys, to find an owner
+                                               const char* flink = NULL;
+                                               int k = 0;
+                                               while ( (flink = osrfStringArrayGetString(jump_list, k++)) && _fparam ) {
+                                                       // For each entry in the jump list.  Each entry (i.e. flink) is
+                                                       // the name of a foreign key column in the current row.
+
+                                                       // From the IDL, get the linkage information for the next jump
+                                                       osrfHash* foreign_link_hash =
+                                                                       oilsIDLFindPath( "/%s/links/%s", _fparam->classname, flink );
+
+                                                       // Get the class metadata for the class
+                                                       // to which the foreign key points
+                                                       osrfHash* foreign_class_meta = osrfHashGet( oilsIDL(),
+                                                                       osrfHashGet( foreign_link_hash, "class" ));
+
+                                                       // Get the name of the referenced key of that class
+                                                       foreign_pkey = osrfHashGet( foreign_link_hash, "key" );
+
+                                                       // Get the value of the foreign key pointing to that class
+                                                       free( foreign_pkey_value );
+                                                       foreign_pkey_value = oilsFMGetString( _fparam, flink );
+                                                       if( !foreign_pkey_value )
+                                                               break;    // Foreign key is null; quit looking
+
+                                                       // Build a WHERE clause for the lookup
+                                                       _tmp_params = single_hash( foreign_pkey, foreign_pkey_value );
+
+                                                       // Do the lookup
+                                                       _list = doFieldmapperSearch( ctx, foreign_class_meta,
+                                                                       _tmp_params, NULL, &err );
+
+                                                       // Get the resulting row
+                                                       jsonObjectFree( _fparam );
+                                                       if( _list && JSON_ARRAY == _list->type && _list->size > 0 )
+                                                               _fparam = jsonObjectExtractIndex( _list, 0 );
+                                                       else {
+                                                               // Referenced row not found
+                                                               _fparam = NULL;
+                                                               bad_class = osrfHashGet( foreign_link_hash, "class" );
+                                                       }
+
+                                                       jsonObjectFree( _tmp_params );
+                                                       jsonObjectFree( _list );
+                                               }
+                                       }
+
+                                       if( bad_class ) {
+
+                                               // We had a foreign key pointing to such-and-such a row, but then
+                                               // we couldn't fetch that row.  The data in the database are in an
+                                               // inconsistent state; the database itself may even be corrupted.
+                                               growing_buffer* msg = buffer_init( 128 );
+                                               buffer_fadd(
+                                                       msg,
+                                                       "%s: no object of class %s found with primary key %s of %s",
+                                                       modulename,
+                                                       bad_class,
+                                                       foreign_pkey,
+                                                       foreign_pkey_value ? foreign_pkey_value : "(null)"
+                                               );
+
+                                               char* m = buffer_release( msg );
+                                               osrfAppSessionStatus(
+                                                       ctx->session,
+                                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                                       "osrfMethodException",
+                                                       ctx->request,
+                                                       m
+                                               );
+
+                                               free( m );
+                                               osrfHashIteratorFree( class_itr );
+                                               free( foreign_pkey_value );
+                                               jsonObjectFree( param );
+
+                                               return 0;
+                                       }
+
+                                       free( foreign_pkey_value );
+
+                                       if( _fparam ) {
+                                               // Examine each context column of the foreign row,
+                                               // and add its value to the list of org units.
+                                               int j = 0;
+                                               const char* foreign_field = NULL;
+                                               osrfStringArray* ctx_array = osrfHashGet( fcontext, "context" );
+                                               while ( (foreign_field = osrfStringArrayGetString( ctx_array, j++ )) ) {
+                                                       osrfStringArrayAdd( context_org_array,
+                                                               oilsFMGetStringConst( _fparam, foreign_field ));
+                                                       osrfLogDebug( OSRF_LOG_MARK,
+                                                               "adding foreign class %s field %s (value: %s) "
+                                                                       "to the context org list",
+                                                               class_name,
+                                                               foreign_field,
+                                                               osrfStringArrayGetString(
+                                                                       context_org_array, context_org_array->size - 1 )
+                                                       );
+                                               }
+
+                                               jsonObjectFree( _fparam );
+                                       }
+                               }
+
+                               osrfHashIteratorFree( class_itr );
+                       }
+               }
+
+               jsonObjectFree( param );
+       }
+
+       const char* context_org = NULL;
+       const char* perm = NULL;
+       int OK = 0;
+
+       // For every combination of permission and context org unit: call a stored procedure
+       // to determine if the user has this permission in the context of this org unit.
+       // If the answer is yes at any point, then we're done, and the user has permission.
+       // In other words permissions are additive.
+       int i = 0;
+       while( (perm = osrfStringArrayGetString(permission, i++)) ) {
+               dbi_result result;
+
+        osrfStringArray* pcache = NULL;
+        if (rs_size > perm_at_threshold) { // grab and cache locations of user perms
+                       pcache = getPermLocationCache(ctx, perm);
+
+                       if (!pcache) {
+                       pcache = osrfNewStringArray(0);
+       
+                               result = dbi_conn_queryf(
+                                       writehandle,
+                                       "SELECT permission.usr_has_perm_at_all(%d, '%s') AS at;",
+                                       userid,
+                                       perm
+                               );
+               
+                               if( result ) {
+                                       osrfLogDebug(
+                                               OSRF_LOG_MARK,
+                                               "Received a result for permission [%s] for user %d",
+                                               perm,
+                                               userid
+                                       );
+               
+                                       if( dbi_result_first_row( result )) {
+                           do {
+                                               jsonObject* return_val = oilsMakeJSONFromResult( result );
+                                               osrfStringArrayAdd( pcache, jsonObjectGetString( jsonObjectGetKeyConst( return_val, "at" ) ) );
+                               jsonObjectFree( return_val );
+                                           } while( dbi_result_next_row( result ));
+
+                                               setPermLocationCache(ctx, perm, pcache);
+                                       }
+               
+                                       dbi_result_free( result );
+                   }
+                       }
+        }
+
+               int j = 0;
+               while( (context_org = osrfStringArrayGetString( context_org_array, j++ )) ) {
+
+            if (rs_size > perm_at_threshold) {
+                if (osrfStringArrayContains( pcache, context_org )) {
+                    OK = 1;
+                    break;
+                }
+            }
+
+                       if( pkey_value ) {
+                               osrfLogDebug(
+                                       OSRF_LOG_MARK,
+                                       "Checking object permission [%s] for user %d "
+                                                       "on object %s (class %s) at org %d",
+                                       perm,
+                                       userid,
+                                       pkey_value,
+                                       osrfHashGet( class, "classname" ),
+                                       atoi( context_org )
+                               );
+
+                               result = dbi_conn_queryf(
+                                       writehandle,
+                                       "SELECT permission.usr_has_object_perm(%d, '%s', '%s', '%s', %d) AS has_perm;",
+                                       userid,
+                                       perm,
+                                       osrfHashGet( class, "classname" ),
+                                       pkey_value,
+                                       atoi( context_org )
+                               );
+
+                               if( result ) {
+                                       osrfLogDebug(
+                                               OSRF_LOG_MARK,
+                                               "Received a result for object permission [%s] "
+                                                               "for user %d on object %s (class %s) at org %d",
+                                               perm,
+                                               userid,
+                                               pkey_value,
+                                               osrfHashGet( class, "classname" ),
+                                               atoi( context_org )
+                                       );
+
+                                       if( dbi_result_first_row( result )) {
+                                               jsonObject* return_val = oilsMakeJSONFromResult( result );
+                                               const char* has_perm = jsonObjectGetString(
+                                                               jsonObjectGetKeyConst( return_val, "has_perm" ));
+
+                                               osrfLogDebug(
+                                                       OSRF_LOG_MARK,
+                                                       "Status of object permission [%s] for user %d "
+                                                                       "on object %s (class %s) at org %d is %s",
+                                                       perm,
+                                                       userid,
+                                                       pkey_value,
+                                                       osrfHashGet(class, "classname"),
+                                                       atoi(context_org),
+                                                       has_perm
+                                               );
+
+                                               if( *has_perm == 't' )
+                                                       OK = 1;
+                                               jsonObjectFree( return_val );
+                                       }
+
+                                       dbi_result_free( result );
+                                       if( OK )
+                                               break;
+                               } else {
+                                       const char* msg;
+                                       int errnum = dbi_conn_error( writehandle, &msg );
+                                       osrfLogWarning( OSRF_LOG_MARK,
+                                               "Unable to call check object permissions: %d, %s",
+                                               errnum, msg ? msg : "(No description available)" );
+                                       if( !oilsIsDBConnected( writehandle ))
+                                               osrfAppSessionPanic( ctx->session );
+                               }
+                       }
+
+            if (rs_size > perm_at_threshold) break;
+
+                       osrfLogDebug( OSRF_LOG_MARK,
+                                       "Checking non-object permission [%s] for user %d at org %d",
+                                       perm, userid, atoi(context_org) );
+                       result = dbi_conn_queryf(
+                               writehandle,
+                               "SELECT permission.usr_has_perm(%d, '%s', %d) AS has_perm;",
+                               userid,
+                               perm,
+                               atoi( context_org )
+                       );
+
+                       if( result ) {
+                               osrfLogDebug( OSRF_LOG_MARK,
+                                       "Received a result for permission [%s] for user %d at org %d",
+                                       perm, userid, atoi( context_org ));
+                               if( dbi_result_first_row( result )) {
+                                       jsonObject* return_val = oilsMakeJSONFromResult( result );
+                                       const char* has_perm = jsonObjectGetString(
+                                               jsonObjectGetKeyConst( return_val, "has_perm" ));
+                                       osrfLogDebug( OSRF_LOG_MARK,
+                                               "Status of permission [%s] for user %d at org %d is [%s]",
+                                               perm, userid, atoi( context_org ), has_perm );
+                                       if( *has_perm == 't' )
+                                               OK = 1;
+                                       jsonObjectFree( return_val );
+                               }
+
+                               dbi_result_free( result );
+                               if( OK )
+                                       break;
+                       } else {
+                               const char* msg;
+                               int errnum = dbi_conn_error( writehandle, &msg );
+                               osrfLogWarning( OSRF_LOG_MARK, "Unable to call user object permissions: %d, %s",
+                                       errnum, msg ? msg : "(No description available)" );
+                               if( !oilsIsDBConnected( writehandle ))
+                                       osrfAppSessionPanic( ctx->session );
+                       }
+
+               }
+
+               if( OK )
+                       break;
+       }
+
+       osrfStringArrayFree( context_org_array );
+
+       return OK;
+}
+
+/**
+       @brief Look up the root of the org_unit tree.
+       @param ctx Pointer to the method context.
+       @return The id of the root org unit, as a character string.
+
+       Query actor.org_unit where parent_ou is null, and return the id as a string.
+
+       This function assumes that there is only one root org unit, i.e. that we
+       have a single tree, not a forest.
+
+       The calling code is responsible for freeing the returned string.
+*/
+static const char* org_tree_root( osrfMethodContext* ctx ) {
+
+       static char cached_root_id[ 32 ] = "";  // extravagantly large buffer
+       static time_t last_lookup_time = 0;
+       time_t current_time = time( NULL );
+
+       if( cached_root_id[ 0 ] && ( current_time - last_lookup_time < 3600 ) ) {
+               // We successfully looked this up less than an hour ago.
+               // It's not likely to have changed since then.
+               return strdup( cached_root_id );
+       }
+       last_lookup_time = current_time;
+
+       int err = 0;
+       jsonObject* where_clause = single_hash( "parent_ou", NULL );
+       jsonObject* result = doFieldmapperSearch(
+               ctx, osrfHashGet( oilsIDL(), "aou" ), where_clause, NULL, &err );
+       jsonObjectFree( where_clause );
+
+       jsonObject* tree_top = jsonObjectGetIndex( result, 0 );
+
+       if( !tree_top ) {
+               jsonObjectFree( result );
+
+               growing_buffer* msg = buffer_init( 128 );
+               OSRF_BUFFER_ADD( msg, modulename );
+               OSRF_BUFFER_ADD( msg,
+                               ": Internal error, could not find the top of the org tree (parent_ou = NULL)" );
+
+               char* m = buffer_release( msg );
+               osrfAppSessionStatus( ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR, "osrfMethodException", ctx->request, m );
+               free( m );
+
+               cached_root_id[ 0 ] = '\0';
+               return NULL;
+       }
+
+       const char* root_org_unit_id = oilsFMGetStringConst( tree_top, "id" );
+       osrfLogDebug( OSRF_LOG_MARK, "Top of the org tree is %s", root_org_unit_id );
+
+       strcpy( cached_root_id, root_org_unit_id );
+       jsonObjectFree( result );
+       return cached_root_id;
+}
+
+/**
+       @brief Create a JSON_HASH with a single key/value pair.
+       @param key The key of the key/value pair.
+       @param value the value of the key/value pair.
+       @return Pointer to a newly created jsonObject of type JSON_HASH.
+
+       The value of the key/value is either a string or (if @a value is NULL) a null.
+*/
+static jsonObject* single_hash( const char* key, const char* value ) {
+       // Sanity check
+       if( ! key ) key = "";
+
+       jsonObject* hash = jsonNewObjectType( JSON_HASH );
+       jsonObjectSetKey( hash, key, jsonNewObject( value ) );
+       return hash;
+}
+
+
+int doCreate( osrfMethodContext* ctx ) {
+       if(osrfMethodVerifyContext( ctx )) {
+               osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+               return -1;
+       }
+
+       if( enforce_pcrud )
+               timeout_needs_resetting = 1;
+
+       osrfHash* meta = osrfHashGet( (osrfHash*) ctx->method->userData, "class" );
+       jsonObject* target = NULL;
+       jsonObject* options = NULL;
+
+       if( enforce_pcrud ) {
+               target = jsonObjectGetIndex( ctx->params, 1 );
+               options = jsonObjectGetIndex( ctx->params, 2 );
+       } else {
+               target = jsonObjectGetIndex( ctx->params, 0 );
+               options = jsonObjectGetIndex( ctx->params, 1 );
+       }
+
+       if( !verifyObjectClass( ctx, target )) {
+               osrfAppRespondComplete( ctx, NULL );
+               return -1;
+       }
+
+       osrfLogDebug( OSRF_LOG_MARK, "Object seems to be of the correct type" );
+
+       const char* trans_id = getXactId( ctx );
+       if( !trans_id ) {
+               osrfLogError( OSRF_LOG_MARK, "No active transaction -- required for CREATE" );
+
+               osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_BADREQUEST,
+                       "osrfMethodException",
+                       ctx->request,
+                       "No active transaction -- required for CREATE"
+               );
+               osrfAppRespondComplete( ctx, NULL );
+               return -1;
+       }
+
+       // The following test is harmless but redundant.  If a class is
+       // readonly, we don't register a create method for it.
+       if( str_is_true( osrfHashGet( meta, "readonly" ) ) ) {
+               osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_BADREQUEST,
+                       "osrfMethodException",
+                       ctx->request,
+                       "Cannot INSERT readonly class"
+               );
+               osrfAppRespondComplete( ctx, NULL );
+               return -1;
+       }
+
+       // Set the last_xact_id
+       int index = oilsIDL_ntop( target->classname, "last_xact_id" );
+       if( index > -1 ) {
+               osrfLogDebug(OSRF_LOG_MARK, "Setting last_xact_id to %s on %s at position %d",
+                       trans_id, target->classname, index);
+               jsonObjectSetIndex( target, index, jsonNewObject( trans_id ));
+       }
+
+       osrfLogDebug( OSRF_LOG_MARK, "There is a transaction running..." );
+
+       dbhandle = writehandle;
+
+       osrfHash* fields = osrfHashGet( meta, "fields" );
+       char* pkey       = osrfHashGet( meta, "primarykey" );
+       char* seq        = osrfHashGet( meta, "sequence" );
+
+       growing_buffer* table_buf = buffer_init( 128 );
+       growing_buffer* col_buf   = buffer_init( 128 );
+       growing_buffer* val_buf   = buffer_init( 128 );
+
+       OSRF_BUFFER_ADD( table_buf, "INSERT INTO " );
+       OSRF_BUFFER_ADD( table_buf, osrfHashGet( meta, "tablename" ));
+       OSRF_BUFFER_ADD_CHAR( col_buf, '(' );
+       buffer_add( val_buf,"VALUES (" );
+
+
+       int first = 1;
+       osrfHash* field = NULL;
+       osrfHashIterator* field_itr = osrfNewHashIterator( fields );
+       while( (field = osrfHashIteratorNext( field_itr ) ) ) {
+
+               const char* field_name = osrfHashIteratorKey( field_itr );
+
+               if( str_is_true( osrfHashGet( field, "virtual" ) ) )
+                       continue;
+
+               const jsonObject* field_object = oilsFMGetObject( target, field_name );
+
+               char* value;
+               if( field_object && field_object->classname ) {
+                       value = oilsFMGetString(
+                               field_object,
+                               (char*)oilsIDLFindPath( "/%s/primarykey", field_object->classname )
+                       );
+               } else if( field_object && JSON_BOOL == field_object->type ) {
+                       if( jsonBoolIsTrue( field_object ) )
+                               value = strdup( "t" );
+                       else
+                               value = strdup( "f" );
+               } else {
+                       value = jsonObjectToSimpleString( field_object );
+               }
+
+               if( first ) {
+                       first = 0;
+               } else {
+                       OSRF_BUFFER_ADD_CHAR( col_buf, ',' );
+                       OSRF_BUFFER_ADD_CHAR( val_buf, ',' );
+               }
+
+               buffer_add( col_buf, field_name );
+
+               if( !field_object || field_object->type == JSON_NULL ) {
+                       buffer_add( val_buf, "DEFAULT" );
+
+               } else if( !strcmp( get_primitive( field ), "number" )) {
+                       const char* numtype = get_datatype( field );
+                       if( !strcmp( numtype, "INT8" )) {
+                               buffer_fadd( val_buf, "%lld", atoll( value ));
+
+                       } else if( !strcmp( numtype, "INT" )) {
+                               buffer_fadd( val_buf, "%d", atoi( value ));
+
+                       } else if( !strcmp( numtype, "NUMERIC" )) {
+                               buffer_fadd( val_buf, "%f", atof( value ));
+                       }
+               } else {
+                       if( dbi_conn_quote_string( writehandle, &value )) {
+                               OSRF_BUFFER_ADD( val_buf, value );
+
+                       } else {
+                               osrfLogError( OSRF_LOG_MARK, "%s: Error quoting string [%s]", modulename, value );
+                               osrfAppSessionStatus(
+                                       ctx->session,
+                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                       "osrfMethodException",
+                                       ctx->request,
+                                       "Error quoting string -- please see the error log for more details"
+                               );
+                               free( value );
+                               buffer_free( table_buf );
+                               buffer_free( col_buf );
+                               buffer_free( val_buf );
+                               osrfAppRespondComplete( ctx, NULL );
+                               return -1;
+                       }
+               }
+
+               free( value );
+       }
+
+       osrfHashIteratorFree( field_itr );
+
+       OSRF_BUFFER_ADD_CHAR( col_buf, ')' );
+       OSRF_BUFFER_ADD_CHAR( val_buf, ')' );
+
+       char* table_str = buffer_release( table_buf );
+       char* col_str   = buffer_release( col_buf );
+       char* val_str   = buffer_release( val_buf );
+       growing_buffer* sql = buffer_init( 128 );
+       buffer_fadd( sql, "%s %s %s;", table_str, col_str, val_str );
+       free( table_str );
+       free( col_str );
+       free( val_str );
+
+       char* query = buffer_release( sql );
+
+       osrfLogDebug( OSRF_LOG_MARK, "%s: Insert SQL [%s]", modulename, query );
+
+       jsonObject* obj = NULL;
+       int rc = 0;
+
+       dbi_result result = dbi_conn_query( writehandle, query );
+       if( !result ) {
+               obj = jsonNewObject( NULL );
+               const char* msg;
+               int errnum = dbi_conn_error( writehandle, &msg );
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s ERROR inserting %s object using query [%s]: %d %s",
+                       modulename,
+                       osrfHashGet(meta, "fieldmapper"),
+                       query,
+                       errnum,
+                       msg ? msg : "(No description available)"
+               );
+               osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_INTERNALSERVERERROR,
+                       "osrfMethodException",
+                       ctx->request,
+                       "INSERT error -- please see the error log for more details"
+               );
+               if( !oilsIsDBConnected( writehandle ))
+                       osrfAppSessionPanic( ctx->session );
+               rc = -1;
+       } else {
+               dbi_result_free( result );
+
+               char* id = oilsFMGetString( target, pkey );
+               if( !id ) {
+                       unsigned long long new_id = dbi_conn_sequence_last( writehandle, seq );
+                       growing_buffer* _id = buffer_init( 10 );
+                       buffer_fadd( _id, "%lld", new_id );
+                       id = buffer_release( _id );
+               }
+
+               // Find quietness specification, if present
+               const char* quiet_str = NULL;
+               if( options ) {
+                       const jsonObject* quiet_obj = jsonObjectGetKeyConst( options, "quiet" );
+                       if( quiet_obj )
+                               quiet_str = jsonObjectGetString( quiet_obj );
+               }
+
+               if( str_is_true( quiet_str )) {  // if quietness is specified
+                       obj = jsonNewObject( id );
+               }
+               else {
+
+                       // Fetch the row that we just inserted, so that we can return it to the client
+                       jsonObject* where_clause = jsonNewObjectType( JSON_HASH );
+                       jsonObjectSetKey( where_clause, pkey, jsonNewObject( id ));
+
+                       int err = 0;
+                       jsonObject* list = doFieldmapperSearch( ctx, meta, where_clause, NULL, &err );
+                       if( err )
+                               rc = -1;
+                       else
+                               obj = jsonObjectClone( jsonObjectGetIndex( list, 0 ));
+
+                       jsonObjectFree( list );
+                       jsonObjectFree( where_clause );
+               }
+
+               free( id );
+       }
+
+       free( query );
+       osrfAppRespondComplete( ctx, obj );
+       jsonObjectFree( obj );
+       return rc;
+}
+
+/**
+       @brief Implement the retrieve method.
+       @param ctx Pointer to the method context.
+       @param err Pointer through which to return an error code.
+       @return If successful, a pointer to the result to be returned to the client;
+       otherwise NULL.
+
+       From the method's class, fetch a row with a specified value in the primary key.  This
+       method relies on the database design convention that a primary key consists of a single
+       column.
+
+       Method parameters:
+       - authkey (PCRUD only)
+       - value of the primary key for the desired row, for building the WHERE clause
+       - a JSON_HASH containing any other SQL clauses: select, join, etc.
+
+       Return to client: One row from the query.
+*/
+int doRetrieve( osrfMethodContext* ctx ) {
+       if(osrfMethodVerifyContext( ctx )) {
+               osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+               return -1;
+       }
+
+       if( enforce_pcrud )
+               timeout_needs_resetting = 1;
+
+       int id_pos = 0;
+       int order_pos = 1;
+
+       if( enforce_pcrud ) {
+               id_pos = 1;
+               order_pos = 2;
+       }
+
+       // Get the class metadata
+       osrfHash* class_def = osrfHashGet( (osrfHash*) ctx->method->userData, "class" );
+
+       // Get the value of the primary key, from a method parameter
+       const jsonObject* id_obj = jsonObjectGetIndex( ctx->params, id_pos );
+
+       osrfLogDebug(
+               OSRF_LOG_MARK,
+               "%s retrieving %s object with primary key value of %s",
+               modulename,
+               osrfHashGet( class_def, "fieldmapper" ),
+               jsonObjectGetString( id_obj )
+       );
+
+       // Build a WHERE clause based on the key value
+       jsonObject* where_clause = jsonNewObjectType( JSON_HASH );
+       jsonObjectSetKey(
+               where_clause,
+               osrfHashGet( class_def, "primarykey" ),  // name of key column
+               jsonObjectClone( id_obj )                // value of key column
+       );
+
+       jsonObject* rest_of_query = jsonObjectGetIndex( ctx->params, order_pos );
+
+       // Do the query
+       int err = 0;
+       jsonObject* list = doFieldmapperSearch( ctx, class_def, where_clause, rest_of_query, &err );
+
+       jsonObjectFree( where_clause );
+       if( err ) {
+               osrfAppRespondComplete( ctx, NULL );
+               return -1;
+       }
+
+       jsonObject* obj = jsonObjectExtractIndex( list, 0 );
+       jsonObjectFree( list );
+
+       if( enforce_pcrud ) {
+               // no result, skip this entirely
+               if(NULL != obj && !verifyObjectPCRUD( ctx, class_def, obj, 1 )) {
+                       jsonObjectFree( obj );
+
+                       growing_buffer* msg = buffer_init( 128 );
+                       OSRF_BUFFER_ADD( msg, modulename );
+                       OSRF_BUFFER_ADD( msg, ": Insufficient permissions to retrieve object" );
+
+                       char* m = buffer_release( msg );
+                       osrfAppSessionStatus( ctx->session, OSRF_STATUS_NOTALLOWED, "osrfMethodException",
+                                       ctx->request, m );
+                       free( m );
+
+                       osrfAppRespondComplete( ctx, NULL );
+                       return -1;
+               }
+       }
+
+       // doFieldmapperSearch() now does the responding for us
+       //osrfAppRespondComplete( ctx, obj );
+       osrfAppRespondComplete( ctx, NULL );
+
+       jsonObjectFree( obj );
+       return 0;
+}
+
+/**
+       @brief Translate a numeric value to a string representation for the database.
+       @param field Pointer to the IDL field definition.
+       @param value Pointer to a jsonObject holding the value of a field.
+       @return Pointer to a newly allocated string.
+
+       The input object is typically a JSON_NUMBER, but it may be a JSON_STRING as long as
+       its contents are numeric.  A non-numeric string is likely to result in invalid SQL,
+       or (what is worse) valid SQL that is wrong.
+
+       If the datatype of the receiving field is not numeric, wrap the value in quotes.
+
+       The calling code is responsible for freeing the resulting string by calling free().
+*/
+static char* jsonNumberToDBString( osrfHash* field, const jsonObject* value ) {
+       growing_buffer* val_buf = buffer_init( 32 );
+       const char* numtype = get_datatype( field );
+
+       // For historical reasons the following contains cruft that could be cleaned up.
+       if( !strncmp( numtype, "INT", 3 ) ) {
+               if( value->type == JSON_NUMBER )
+                       //buffer_fadd( val_buf, "%ld", (long)jsonObjectGetNumber(value) );
+                       buffer_fadd( val_buf, jsonObjectGetString( value ) );
+               else {
+                       buffer_fadd( val_buf, jsonObjectGetString( value ) );
+               }
+
+       } else if( !strcmp( numtype, "NUMERIC" )) {
+               if( value->type == JSON_NUMBER )
+                       buffer_fadd( val_buf, jsonObjectGetString( value ));
+               else {
+                       buffer_fadd( val_buf, jsonObjectGetString( value ));
+               }
+
+       } else {
+               // Presumably this was really intended to be a string, so quote it
+               char* str = jsonObjectToSimpleString( value );
+               if( dbi_conn_quote_string( dbhandle, &str )) {
+                       OSRF_BUFFER_ADD( val_buf, str );
+                       free( str );
+               } else {
+                       osrfLogError( OSRF_LOG_MARK, "%s: Error quoting key string [%s]", modulename, str );
+                       free( str );
+                       buffer_free( val_buf );
+                       return NULL;
+               }
+       }
+
+       return buffer_release( val_buf );
+}
+
+static char* searchINPredicate( const char* class_alias, osrfHash* field,
+               jsonObject* node, const char* op, osrfMethodContext* ctx ) {
+       growing_buffer* sql_buf = buffer_init( 32 );
+
+       buffer_fadd(
+               sql_buf,
+               "\"%s\".%s ",
+               class_alias,
+               osrfHashGet( field, "name" )
+       );
+
+       if( !op ) {
+               buffer_add( sql_buf, "IN (" );
+       } else if( !strcasecmp( op,"not in" )) {
+               buffer_add( sql_buf, "NOT IN (" );
+       } else {
+               buffer_add( sql_buf, "IN (" );
+       }
+
+       if( node->type == JSON_HASH ) {
+               // subquery predicate
+               char* subpred = buildQuery( ctx, node, SUBSELECT );
+               if( ! subpred ) {
+                       buffer_free( sql_buf );
+                       return NULL;
+               }
+
+               buffer_add( sql_buf, subpred );
+               free( subpred );
+
+       } else if( node->type == JSON_ARRAY ) {
+               // literal value list
+               int in_item_index = 0;
+               int in_item_first = 1;
+               const jsonObject* in_item;
+               while( (in_item = jsonObjectGetIndex( node, in_item_index++ )) ) {
+
+                       if( in_item_first )
+                               in_item_first = 0;
+                       else
+                               buffer_add( sql_buf, ", " );
+
+                       // Sanity check
+                       if( in_item->type != JSON_STRING && in_item->type != JSON_NUMBER ) {
+                               osrfLogError( OSRF_LOG_MARK,
+                                               "%s: Expected string or number within IN list; found %s",
+                                               modulename, json_type( in_item->type ) );
+                               buffer_free( sql_buf );
+                               return NULL;
+                       }
+
+                       // Append the literal value -- quoted if not a number
+                       if( JSON_NUMBER == in_item->type ) {
+                               char* val = jsonNumberToDBString( field, in_item );
+                               OSRF_BUFFER_ADD( sql_buf, val );
+                               free( val );
+
+                       } else if( !strcmp( get_primitive( field ), "number" )) {
+                               char* val = jsonNumberToDBString( field, in_item );
+                               OSRF_BUFFER_ADD( sql_buf, val );
+                               free( val );
+
+                       } else {
+                               char* key_string = jsonObjectToSimpleString( in_item );
+                               if( dbi_conn_quote_string( dbhandle, &key_string )) {
+                                       OSRF_BUFFER_ADD( sql_buf, key_string );
+                                       free( key_string );
+                               } else {
+                                       osrfLogError( OSRF_LOG_MARK,
+                                                       "%s: Error quoting key string [%s]", modulename, key_string );
+                                       free( key_string );
+                                       buffer_free( sql_buf );
+                                       return NULL;
+                               }
+                       }
+               }
+
+               if( in_item_first ) {
+                       osrfLogError(OSRF_LOG_MARK, "%s: Empty IN list", modulename );
+                       buffer_free( sql_buf );
+                       return NULL;
+               }
+       } else {
+               osrfLogError( OSRF_LOG_MARK, "%s: Expected object or array for IN clause; found %s",
+                       modulename, json_type( node->type ));
+               buffer_free( sql_buf );
+               return NULL;
+       }
+
+       OSRF_BUFFER_ADD_CHAR( sql_buf, ')' );
+
+       return buffer_release( sql_buf );
+}
+
+// Receive a JSON_ARRAY representing a function call.  The first
+// entry in the array is the function name.  The rest are parameters.
+static char* searchValueTransform( const jsonObject* array ) {
+
+       if( array->size < 1 ) {
+               osrfLogError( OSRF_LOG_MARK, "%s: Empty array for value transform", modulename );
+               return NULL;
+       }
+
+       // Get the function name
+       jsonObject* func_item = jsonObjectGetIndex( array, 0 );
+       if( func_item->type != JSON_STRING ) {
+               osrfLogError( OSRF_LOG_MARK, "%s: Error: expected function name, found %s",
+                       modulename, json_type( func_item->type ));
+               return NULL;
+       }
+
+       growing_buffer* sql_buf = buffer_init( 32 );
+
+       OSRF_BUFFER_ADD( sql_buf, jsonObjectGetString( func_item ) );
+       OSRF_BUFFER_ADD( sql_buf, "( " );
+
+       // Get the parameters
+       int func_item_index = 1;   // We already grabbed the zeroth entry
+       while( (func_item = jsonObjectGetIndex( array, func_item_index++ )) ) {
+
+               // Add a separator comma, if we need one
+               if( func_item_index > 2 )
+                       buffer_add( sql_buf, ", " );
+
+               // Add the current parameter
+               if( func_item->type == JSON_NULL ) {
+                       buffer_add( sql_buf, "NULL" );
+               } else {
+                       if( func_item->type == JSON_BOOL ) {
+                               if( jsonBoolIsTrue(func_item) ) {
+                                       buffer_add( sql_buf, "TRUE" );
+                               } else {
+                                       buffer_add( sql_buf, "FALSE" );
+                               }
+                       } else {
+                               char* val = jsonObjectToSimpleString( func_item );
+                               if( dbi_conn_quote_string( dbhandle, &val )) {
+                                       OSRF_BUFFER_ADD( sql_buf, val );
+                                       free( val );
+                               } else {
+                                       osrfLogError( OSRF_LOG_MARK, 
+                                               "%s: Error quoting key string [%s]", modulename, val );
+                                       buffer_free( sql_buf );
+                                       free( val );
+                                       return NULL;
+                               }
+                       }
+               }
+       }
+
+       buffer_add( sql_buf, " )" );
+
+       return buffer_release( sql_buf );
+}
+
+static char* searchFunctionPredicate( const char* class_alias, osrfHash* field,
+               const jsonObject* node, const char* op ) {
+
+       if( ! is_good_operator( op ) ) {
+               osrfLogError( OSRF_LOG_MARK, "%s: Invalid operator [%s]", modulename, op );
+               return NULL;
+       }
+
+       char* val = searchValueTransform( node );
+       if( !val )
+               return NULL;
+
+       growing_buffer* sql_buf = buffer_init( 32 );
+       buffer_fadd(
+               sql_buf,
+               "\"%s\".%s %s %s",
+               class_alias,
+               osrfHashGet( field, "name" ),
+               op,
+               val
+       );
+
+       free( val );
+
+       return buffer_release( sql_buf );
+}
+
+// class_alias is a class name or other table alias
+// field is a field definition as stored in the IDL
+// node comes from the method parameter, and may represent an entry in the SELECT list
+static char* searchFieldTransform( const char* class_alias, osrfHash* field,
+               const jsonObject* node ) {
+       growing_buffer* sql_buf = buffer_init( 32 );
+
+       const char* field_transform = jsonObjectGetString(
+               jsonObjectGetKeyConst( node, "transform" ) );
+       const char* transform_subcolumn = jsonObjectGetString(
+               jsonObjectGetKeyConst( node, "result_field" ) );
+
+       if( transform_subcolumn ) {
+               if( ! is_identifier( transform_subcolumn ) ) {
+                       osrfLogError( OSRF_LOG_MARK, "%s: Invalid subfield name: \"%s\"\n",
+                                       modulename, transform_subcolumn );
+                       buffer_free( sql_buf );
+                       return NULL;
+               }
+               OSRF_BUFFER_ADD_CHAR( sql_buf, '(' );    // enclose transform in parentheses
+       }
+
+       if( field_transform ) {
+
+               if( ! is_identifier( field_transform ) ) {
+                       osrfLogError( OSRF_LOG_MARK, "%s: Expected function name, found \"%s\"\n",
+                                       modulename, field_transform );
+                       buffer_free( sql_buf );
+                       return NULL;
+               }
+
+               if( obj_is_true( jsonObjectGetKeyConst( node, "distinct" ) ) ) {
+                       buffer_fadd( sql_buf, "%s(DISTINCT \"%s\".%s",
+                               field_transform, class_alias, osrfHashGet( field, "name" ));
+               } else {
+                       buffer_fadd( sql_buf, "%s(\"%s\".%s",
+                               field_transform, class_alias, osrfHashGet( field, "name" ));
+               }
+
+               const jsonObject* array = jsonObjectGetKeyConst( node, "params" );
+
+               if( array ) {
+                       if( array->type != JSON_ARRAY ) {
+                               osrfLogError( OSRF_LOG_MARK,
+                                       "%s: Expected JSON_ARRAY for function params; found %s",
+                                       modulename, json_type( array->type ) );
+                               buffer_free( sql_buf );
+                               return NULL;
+                       }
+                       int func_item_index = 0;
+                       jsonObject* func_item;
+                       while( (func_item = jsonObjectGetIndex( array, func_item_index++ ))) {
+
+                               char* val = jsonObjectToSimpleString( func_item );
+
+                               if( !val ) {
+                                       buffer_add( sql_buf, ",NULL" );
+                               } else if( dbi_conn_quote_string( dbhandle, &val )) {
+                                       OSRF_BUFFER_ADD_CHAR( sql_buf, ',' );
+                                       OSRF_BUFFER_ADD( sql_buf, val );
+                               } else {
+                                       osrfLogError( OSRF_LOG_MARK,
+                                                       "%s: Error quoting key string [%s]", modulename, val );
+                                       free( val );
+                                       buffer_free( sql_buf );
+                                       return NULL;
+                               }
+                               free( val );
+                       }
+               }
+
+               buffer_add( sql_buf, " )" );
+
+       } else {
+               buffer_fadd( sql_buf, "\"%s\".%s", class_alias, osrfHashGet( field, "name" ));
+       }
+
+       if( transform_subcolumn )
+               buffer_fadd( sql_buf, ").\"%s\"", transform_subcolumn );
+
+       return buffer_release( sql_buf );
+}
+
+static char* searchFieldTransformPredicate( const ClassInfo* class_info, osrfHash* field,
+               const jsonObject* node, const char* op ) {
+
+       if( ! is_good_operator( op ) ) {
+               osrfLogError( OSRF_LOG_MARK, "%s: Error: Invalid operator %s", modulename, op );
+               return NULL;
+       }
+
+       char* field_transform = searchFieldTransform( class_info->alias, field, node );
+       if( ! field_transform )
+               return NULL;
+       char* value = NULL;
+       int extra_parens = 0;   // boolean
+
+       const jsonObject* value_obj = jsonObjectGetKeyConst( node, "value" );
+       if( ! value_obj ) {
+               value = searchWHERE( node, class_info, AND_OP_JOIN, NULL );
+               if( !value ) {
+                       osrfLogError( OSRF_LOG_MARK, "%s: Error building condition for field transform",
+                               modulename );
+                       free( field_transform );
+                       return NULL;
+               }
+               extra_parens = 1;
+       } else if( value_obj->type == JSON_ARRAY ) {
+               value = searchValueTransform( value_obj );
+               if( !value ) {
+                       osrfLogError( OSRF_LOG_MARK,
+                               "%s: Error building value transform for field transform", modulename );
+                       free( field_transform );
+                       return NULL;
+               }
+       } else if( value_obj->type == JSON_HASH ) {
+               value = searchWHERE( value_obj, class_info, AND_OP_JOIN, NULL );
+               if( !value ) {
+                       osrfLogError( OSRF_LOG_MARK, "%s: Error building predicate for field transform",
+                               modulename );
+                       free( field_transform );
+                       return NULL;
+               }
+               extra_parens = 1;
+       } else if( value_obj->type == JSON_NUMBER ) {
+               value = jsonNumberToDBString( field, value_obj );
+       } else if( value_obj->type == JSON_NULL ) {
+               osrfLogError( OSRF_LOG_MARK,
+                       "%s: Error building predicate for field transform: null value", modulename );
+               free( field_transform );
+               return NULL;
+       } else if( value_obj->type == JSON_BOOL ) {
+               osrfLogError( OSRF_LOG_MARK,
+                       "%s: Error building predicate for field transform: boolean value", modulename );
+               free( field_transform );
+               return NULL;
+       } else {
+               if( !strcmp( get_primitive( field ), "number") ) {
+                       value = jsonNumberToDBString( field, value_obj );
+               } else {
+                       value = jsonObjectToSimpleString( value_obj );
+                       if( !dbi_conn_quote_string( dbhandle, &value )) {
+                               osrfLogError( OSRF_LOG_MARK, "%s: Error quoting key string [%s]",
+                                       modulename, value );
+                               free( value );
+                               free( field_transform );
+                               return NULL;
+                       }
+               }
+       }
+
+       const char* left_parens  = "";
+       const char* right_parens = "";
+
+       if( extra_parens ) {
+               left_parens  = "(";
+               right_parens = ")";
+       }
+
+       const char* right_percent = "";
+       const char* real_op       = op;
+
+       if( !strcasecmp( op, "startwith") ) {
+               real_op = "like";
+               right_percent = "|| '%'";
+       }
+
+       growing_buffer* sql_buf = buffer_init( 32 );
+
+       buffer_fadd(
+               sql_buf,
+               "%s%s %s %s %s%s %s%s",
+               left_parens,
+               field_transform,
+               real_op,
+               left_parens,
+               value,
+               right_percent,
+               right_parens,
+               right_parens
+       );
+
+       free( value );
+       free( field_transform );
+
+       return buffer_release( sql_buf );
+}
+
+static char* searchSimplePredicate( const char* op, const char* class_alias,
+               osrfHash* field, const jsonObject* node ) {
+
+       if( ! is_good_operator( op ) ) {
+               osrfLogError( OSRF_LOG_MARK, "%s: Invalid operator [%s]", modulename, op );
+               return NULL;
+       }
+
+       char* val = NULL;
+
+       // Get the value to which we are comparing the specified column
+       if( node->type != JSON_NULL ) {
+               if( node->type == JSON_NUMBER ) {
+                       val = jsonNumberToDBString( field, node );
+               } else if( !strcmp( get_primitive( field ), "number" ) ) {
+                       val = jsonNumberToDBString( field, node );
+               } else {
+                       val = jsonObjectToSimpleString( node );
+               }
+       }
+
+       if( val ) {
+               if( JSON_NUMBER != node->type && strcmp( get_primitive( field ), "number") ) {
+                       // Value is not numeric; enclose it in quotes
+                       if( !dbi_conn_quote_string( dbhandle, &val ) ) {
+                               osrfLogError( OSRF_LOG_MARK, "%s: Error quoting key string [%s]",
+                                       modulename, val );
+                               free( val );
+                               return NULL;
+                       }
+               }
+       } else {
+               // Compare to a null value
+               val = strdup( "NULL" );
+               if( strcmp( op, "=" ))
+                       op = "IS NOT";
+               else
+                       op = "IS";
+       }
+
+       growing_buffer* sql_buf = buffer_init( 32 );
+       buffer_fadd( sql_buf, "\"%s\".%s %s %s", class_alias, osrfHashGet(field, "name"), op, val );
+       char* pred = buffer_release( sql_buf );
+
+       free( val );
+
+       return pred;
+}
+
+static char* searchBETWEENPredicate( const char* class_alias,
+               osrfHash* field, const jsonObject* node ) {
+
+       const jsonObject* x_node = jsonObjectGetIndex( node, 0 );
+       const jsonObject* y_node = jsonObjectGetIndex( node, 1 );
+
+       if( NULL == y_node ) {
+               osrfLogError( OSRF_LOG_MARK, "%s: Not enough operands for BETWEEN operator", modulename );
+               return NULL;
+       }
+       else if( NULL != jsonObjectGetIndex( node, 2 ) ) {
+               osrfLogError( OSRF_LOG_MARK, "%s: Too many operands for BETWEEN operator", modulename );
+               return NULL;
+       }
+
+       char* x_string;
+       char* y_string;
+
+       if( !strcmp( get_primitive( field ), "number") ) {
+               x_string = jsonNumberToDBString( field, x_node );
+               y_string = jsonNumberToDBString( field, y_node );
+
+       } else {
+               x_string = jsonObjectToSimpleString( x_node );
+               y_string = jsonObjectToSimpleString( y_node );
+               if( !(dbi_conn_quote_string( dbhandle, &x_string )
+                       && dbi_conn_quote_string( dbhandle, &y_string )) ) {
+                       osrfLogError( OSRF_LOG_MARK, "%s: Error quoting key strings [%s] and [%s]",
+                                       modulename, x_string, y_string );
+                       free( x_string );
+                       free( y_string );
+                       return NULL;
+               }
+       }
+
+       growing_buffer* sql_buf = buffer_init( 32 );
+       buffer_fadd( sql_buf, "\"%s\".%s BETWEEN %s AND %s",
+                       class_alias, osrfHashGet( field, "name" ), x_string, y_string );
+       free( x_string );
+       free( y_string );
+
+       return buffer_release( sql_buf );
+}
+
+static char* searchPredicate( const ClassInfo* class_info, osrfHash* field,
+                                                         jsonObject* node, osrfMethodContext* ctx ) {
+
+       char* pred = NULL;
+       if( node->type == JSON_ARRAY ) { // equality IN search
+               pred = searchINPredicate( class_info->alias, field, node, NULL, ctx );
+       } else if( node->type == JSON_HASH ) { // other search
+               jsonIterator* pred_itr = jsonNewIterator( node );
+               if( !jsonIteratorHasNext( pred_itr ) ) {
+                       osrfLogError( OSRF_LOG_MARK, "%s: Empty predicate for field \"%s\"",
+                                       modulename, osrfHashGet(field, "name" ));
+               } else {
+                       jsonObject* pred_node = jsonIteratorNext( pred_itr );
+
+                       // Verify that there are no additional predicates
+                       if( jsonIteratorHasNext( pred_itr ) ) {
+                               osrfLogError( OSRF_LOG_MARK, "%s: Multiple predicates for field \"%s\"",
+                                               modulename, osrfHashGet(field, "name" ));
+                       } else if( !(strcasecmp( pred_itr->key,"between" )) )
+                               pred = searchBETWEENPredicate( class_info->alias, field, pred_node );
+                       else if( !(strcasecmp( pred_itr->key,"in" ))
+                                       || !(strcasecmp( pred_itr->key,"not in" )) )
+                               pred = searchINPredicate(
+                                       class_info->alias, field, pred_node, pred_itr->key, ctx );
+                       else if( pred_node->type == JSON_ARRAY )
+                               pred = searchFunctionPredicate(
+                                       class_info->alias, field, pred_node, pred_itr->key );
+                       else if( pred_node->type == JSON_HASH )
+                               pred = searchFieldTransformPredicate(
+                                       class_info, field, pred_node, pred_itr->key );
+                       else
+                               pred = searchSimplePredicate( pred_itr->key, class_info->alias, field, pred_node );
+               }
+               jsonIteratorFree( pred_itr );
+
+       } else if( node->type == JSON_NULL ) { // IS NULL search
+               growing_buffer* _p = buffer_init( 64 );
+               buffer_fadd(
+                       _p,
+                       "\"%s\".%s IS NULL",
+                       class_info->alias,
+                       osrfHashGet( field, "name" )
+               );
+               pred = buffer_release( _p );
+       } else { // equality search
+               pred = searchSimplePredicate( "=", class_info->alias, field, node );
+       }
+
+       return pred;
+
+}
+
+
+/*
+
+join : {
+       acn : {
+               field : record,
+               fkey : id
+               type : left
+               filter_op : or
+               filter : { ... },
+               join : {
+                       acp : {
+                               field : call_number,
+                               fkey : id,
+                               filter : { ... },
+                       },
+               },
+       },
+       mrd : {
+               field : record,
+               type : inner
+               fkey : id,
+               filter : { ... },
+       }
+}
+
+*/
+
+static char* searchJOIN( const jsonObject* join_hash, const ClassInfo* left_info ) {
+
+       const jsonObject* working_hash;
+       jsonObject* freeable_hash = NULL;
+
+       if( join_hash->type == JSON_HASH ) {
+               working_hash = join_hash;
+       } else if( join_hash->type == JSON_STRING ) {
+               // turn it into a JSON_HASH by creating a wrapper
+               // around a copy of the original
+               const char* _tmp = jsonObjectGetString( join_hash );
+               freeable_hash = jsonNewObjectType( JSON_HASH );
+               jsonObjectSetKey( freeable_hash, _tmp, NULL );
+               working_hash = freeable_hash;
+       } else {
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s: JOIN failed; expected JSON object type not found",
+                       modulename
+               );
+               return NULL;
+       }
+
+       growing_buffer* join_buf = buffer_init( 128 );
+       const char* leftclass = left_info->class_name;
+
+       jsonObject* snode = NULL;
+       jsonIterator* search_itr = jsonNewIterator( working_hash );
+
+       while ( (snode = jsonIteratorNext( search_itr )) ) {
+               const char* right_alias = search_itr->key;
+               const char* class =
+                               jsonObjectGetString( jsonObjectGetKeyConst( snode, "class" ) );
+               if( ! class )
+                       class = right_alias;
+
+               const ClassInfo* right_info = add_joined_class( right_alias, class );
+               if( !right_info ) {
+                       osrfLogError(
+                               OSRF_LOG_MARK,
+                               "%s: JOIN failed.  Class \"%s\" not resolved in IDL",
+                               modulename,
+                               search_itr->key
+                       );
+                       jsonIteratorFree( search_itr );
+                       buffer_free( join_buf );
+                       if( freeable_hash )
+                               jsonObjectFree( freeable_hash );
+                       return NULL;
+               }
+               osrfHash* links    = right_info->links;
+               const char* table  = right_info->source_def;
+
+               const char* fkey  = jsonObjectGetString( jsonObjectGetKeyConst( snode, "fkey" ) );
+               const char* field = jsonObjectGetString( jsonObjectGetKeyConst( snode, "field" ) );
+
+               if( field && !fkey ) {
+                       // Look up the corresponding join column in the IDL.
+                       // The link must be defined in the child table,
+                       // and point to the right parent table.
+                       osrfHash* idl_link = (osrfHash*) osrfHashGet( links, field );
+                       const char* reltype = NULL;
+                       const char* other_class = NULL;
+                       reltype = osrfHashGet( idl_link, "reltype" );
+                       if( reltype && strcmp( reltype, "has_many" ) )
+                               other_class = osrfHashGet( idl_link, "class" );
+                       if( other_class && !strcmp( other_class, leftclass ) )
+                               fkey = osrfHashGet( idl_link, "key" );
+                       if( !fkey ) {
+                               osrfLogError(
+                                       OSRF_LOG_MARK,
+                                       "%s: JOIN failed.  No link defined from %s.%s to %s",
+                                       modulename,
+                                       class,
+                                       field,
+                                       leftclass
+                               );
+                               buffer_free( join_buf );
+                               if( freeable_hash )
+                                       jsonObjectFree( freeable_hash );
+                               jsonIteratorFree( search_itr );
+                               return NULL;
+                       }
+
+               } else if( !field && fkey ) {
+                       // Look up the corresponding join column in the IDL.
+                       // The link must be defined in the child table,
+                       // and point to the right parent table.
+                       osrfHash* left_links = left_info->links;
+                       osrfHash* idl_link = (osrfHash*) osrfHashGet( left_links, fkey );
+                       const char* reltype = NULL;
+                       const char* other_class = NULL;
+                       reltype = osrfHashGet( idl_link, "reltype" );
+                       if( reltype && strcmp( reltype, "has_many" ) )
+                               other_class = osrfHashGet( idl_link, "class" );
+                       if( other_class && !strcmp( other_class, class ) )
+                               field = osrfHashGet( idl_link, "key" );
+                       if( !field ) {
+                               osrfLogError(
+                                       OSRF_LOG_MARK,
+                                       "%s: JOIN failed.  No link defined from %s.%s to %s",
+                                       modulename,
+                                       leftclass,
+                                       fkey,
+                                       class
+                               );
+                               buffer_free( join_buf );
+                               if( freeable_hash )
+                                       jsonObjectFree( freeable_hash );
+                               jsonIteratorFree( search_itr );
+                               return NULL;
+                       }
+
+               } else if( !field && !fkey ) {
+                       osrfHash* left_links = left_info->links;
+
+                       // For each link defined for the left class:
+                       // see if the link references the joined class
+                       osrfHashIterator* itr = osrfNewHashIterator( left_links );
+                       osrfHash* curr_link = NULL;
+                       while( (curr_link = osrfHashIteratorNext( itr ) ) ) {
+                               const char* other_class = osrfHashGet( curr_link, "class" );
+                               if( other_class && !strcmp( other_class, class ) ) {
+
+                                       // In the IDL, the parent class doesn't always know then names of the child
+                                       // columns that are pointing to it, so don't use that end of the link
+                                       const char* reltype = osrfHashGet( curr_link, "reltype" );
+                                       if( reltype && strcmp( reltype, "has_many" ) ) {
+                                               // Found a link between the classes
+                                               fkey = osrfHashIteratorKey( itr );
+                                               field = osrfHashGet( curr_link, "key" );
+                                               break;
+                                       }
+                               }
+                       }
+                       osrfHashIteratorFree( itr );
+
+                       if( !field || !fkey ) {
+                               // Do another such search, with the classes reversed
+
+                               // For each link defined for the joined class:
+                               // see if the link references the left class
+                               osrfHashIterator* itr = osrfNewHashIterator( links );
+                               osrfHash* curr_link = NULL;
+                               while( (curr_link = osrfHashIteratorNext( itr ) ) ) {
+                                       const char* other_class = osrfHashGet( curr_link, "class" );
+                                       if( other_class && !strcmp( other_class, leftclass ) ) {
+
+                                               // In the IDL, the parent class doesn't know then names of the child
+                                               // columns that are pointing to it, so don't use that end of the link
+                                               const char* reltype = osrfHashGet( curr_link, "reltype" );
+                                               if( reltype && strcmp( reltype, "has_many" ) ) {
+                                                       // Found a link between the classes
+                                                       field = osrfHashIteratorKey( itr );
+                                                       fkey = osrfHashGet( curr_link, "key" );
+                                                       break;
+                                               }
+                                       }
+                               }
+                               osrfHashIteratorFree( itr );
+                       }
+
+                       if( !field || !fkey ) {
+                               osrfLogError(
+                                       OSRF_LOG_MARK,
+                                       "%s: JOIN failed.  No link defined between %s and %s",
+                                       modulename,
+                                       leftclass,
+                                       class
+                               );
+                               buffer_free( join_buf );
+                               if( freeable_hash )
+                                       jsonObjectFree( freeable_hash );
+                               jsonIteratorFree( search_itr );
+                               return NULL;
+                       }
+               }
+
+               const char* type = jsonObjectGetString( jsonObjectGetKeyConst( snode, "type" ) );
+               if( type ) {
+                       if( !strcasecmp( type,"left" )) {
+                               buffer_add( join_buf, " LEFT JOIN" );
+                       } else if( !strcasecmp( type,"right" )) {
+                               buffer_add( join_buf, " RIGHT JOIN" );
+                       } else if( !strcasecmp( type,"full" )) {
+                               buffer_add( join_buf, " FULL JOIN" );
+                       } else {
+                               buffer_add( join_buf, " INNER JOIN" );
+                       }
+               } else {
+                       buffer_add( join_buf, " INNER JOIN" );
+               }
+
+               buffer_fadd( join_buf, " %s AS \"%s\" ON ( \"%s\".%s = \"%s\".%s",
+                                       table, right_alias, right_alias, field, left_info->alias, fkey );
+
+               // Add any other join conditions as specified by "filter"
+               const jsonObject* filter = jsonObjectGetKeyConst( snode, "filter" );
+               if( filter ) {
+                       const char* filter_op = jsonObjectGetString(
+                               jsonObjectGetKeyConst( snode, "filter_op" ) );
+                       if( filter_op && !strcasecmp( "or",filter_op )) {
+                               buffer_add( join_buf, " OR " );
+                       } else {
+                               buffer_add( join_buf, " AND " );
+                       }
+
+                       char* jpred = searchWHERE( filter, right_info, AND_OP_JOIN, NULL );
+                       if( jpred ) {
+                               OSRF_BUFFER_ADD_CHAR( join_buf, ' ' );
+                               OSRF_BUFFER_ADD( join_buf, jpred );
+                               free( jpred );
+                       } else {
+                               osrfLogError(
+                                       OSRF_LOG_MARK,
+                                       "%s: JOIN failed.  Invalid conditional expression.",
+                                       modulename
+                               );
+                               jsonIteratorFree( search_itr );
+                               buffer_free( join_buf );
+                               if( freeable_hash )
+                                       jsonObjectFree( freeable_hash );
+                               return NULL;
+                       }
+               }
+
+               buffer_add( join_buf, " ) " );
+
+               // Recursively add a nested join, if one is present
+               const jsonObject* join_filter = jsonObjectGetKeyConst( snode, "join" );
+               if( join_filter ) {
+                       char* jpred = searchJOIN( join_filter, right_info );
+                       if( jpred ) {
+                               OSRF_BUFFER_ADD_CHAR( join_buf, ' ' );
+                               OSRF_BUFFER_ADD( join_buf, jpred );
+                               free( jpred );
+                       } else {
+                               osrfLogError( OSRF_LOG_MARK, "%s: Invalid nested join.", modulename );
+                               jsonIteratorFree( search_itr );
+                               buffer_free( join_buf );
+                               if( freeable_hash )
+                                       jsonObjectFree( freeable_hash );
+                               return NULL;
+                       }
+               }
+       }
+
+       if( freeable_hash )
+               jsonObjectFree( freeable_hash );
+       jsonIteratorFree( search_itr );
+
+       return buffer_release( join_buf );
+}
+
+/*
+
+{ +class : { -or|-and : { field : { op : value }, ... } ... }, ... }
+{ +class : { -or|-and : [ { field : { op : value }, ... }, ...] ... }, ... }
+[ { +class : { -or|-and : [ { field : { op : value }, ... }, ...] ... }, ... }, ... ]
+
+Generate code to express a set of conditions, as for a WHERE clause.  Parameters:
+
+search_hash is the JSON expression of the conditions.
+meta is the class definition from the IDL, for the relevant table.
+opjoin_type indicates whether multiple conditions, if present, should be
+       connected by AND or OR.
+osrfMethodContext is loaded with all sorts of stuff, but all we do with it here is
+       to pass it to other functions -- and all they do with it is to use the session
+       and request members to send error messages back to the client.
+
+*/
+
+static char* searchWHERE( const jsonObject* search_hash, const ClassInfo* class_info,
+               int opjoin_type, osrfMethodContext* ctx ) {
+
+       osrfLogDebug(
+               OSRF_LOG_MARK,
+               "%s: Entering searchWHERE; search_hash addr = %p, meta addr = %p, "
+               "opjoin_type = %d, ctx addr = %p",
+               modulename,
+               search_hash,
+               class_info->class_def,
+               opjoin_type,
+               ctx
+       );
+
+       growing_buffer* sql_buf = buffer_init( 128 );
+
+       jsonObject* node = NULL;
+
+       int first = 1;
+       if( search_hash->type == JSON_ARRAY ) {
+               if( 0 == search_hash->size ) {
+                       osrfLogError(
+                               OSRF_LOG_MARK,
+                               "%s: Invalid predicate structure: empty JSON array",
+                               modulename
+                       );
+                       buffer_free( sql_buf );
+                       return NULL;
+               }
+
+               unsigned long i = 0;
+               while(( node = jsonObjectGetIndex( search_hash, i++ ) )) {
+                       if( first ) {
+                               first = 0;
+                       } else {
+                               if( opjoin_type == OR_OP_JOIN )
+                                       buffer_add( sql_buf, " OR " );
+                               else
+                                       buffer_add( sql_buf, " AND " );
+                       }
+
+                       char* subpred = searchWHERE( node, class_info, opjoin_type, ctx );
+                       if( ! subpred ) {
+                               buffer_free( sql_buf );
+                               return NULL;
+                       }
+
+                       buffer_fadd( sql_buf, "( %s )", subpred );
+                       free( subpred );
+               }
+
+       } else if( search_hash->type == JSON_HASH ) {
+               osrfLogDebug( OSRF_LOG_MARK,
+                       "%s: In WHERE clause, condition type is JSON_HASH", modulename );
+               jsonIterator* search_itr = jsonNewIterator( search_hash );
+               if( !jsonIteratorHasNext( search_itr ) ) {
+                       osrfLogError(
+                               OSRF_LOG_MARK,
+                               "%s: Invalid predicate structure: empty JSON object",
+                               modulename
+                       );
+                       jsonIteratorFree( search_itr );
+                       buffer_free( sql_buf );
+                       return NULL;
+               }
+
+               while( (node = jsonIteratorNext( search_itr )) ) {
+
+                       if( first ) {
+                               first = 0;
+                       } else {
+                               if( opjoin_type == OR_OP_JOIN )
+                                       buffer_add( sql_buf, " OR " );
+                               else
+                                       buffer_add( sql_buf, " AND " );
+                       }
+
+                       if( '+' == search_itr->key[ 0 ] ) {
+
+                               // This plus sign prefixes a class name or other table alias;
+                               // make sure the table alias is in scope
+                               ClassInfo* alias_info = search_all_alias( search_itr->key + 1 );
+                               if( ! alias_info ) {
+                                       osrfLogError(
+                                                        OSRF_LOG_MARK,
+                                                       "%s: Invalid table alias \"%s\" in WHERE clause",
+                                                       modulename,
+                                                       search_itr->key + 1
+                                       );
+                                       jsonIteratorFree( search_itr );
+                                       buffer_free( sql_buf );
+                                       return NULL;
+                               }
+
+                               if( node->type == JSON_STRING ) {
+                                       // It's the name of a column; make sure it belongs to the class
+                                       const char* fieldname = jsonObjectGetString( node );
+                                       if( ! osrfHashGet( alias_info->fields, fieldname ) ) {
+                                               osrfLogError(
+                                                       OSRF_LOG_MARK,
+                                                       "%s: Invalid column name \"%s\" in WHERE clause "
+                                                       "for table alias \"%s\"",
+                                                       modulename,
+                                                       fieldname,
+                                                       alias_info->alias
+                                               );
+                                               jsonIteratorFree( search_itr );
+                                               buffer_free( sql_buf );
+                                               return NULL;
+                                       }
+
+                                       buffer_fadd( sql_buf, " \"%s\".%s ", alias_info->alias, fieldname );
+                               } else {
+                                       // It's something more complicated
+                                       char* subpred = searchWHERE( node, alias_info, AND_OP_JOIN, ctx );
+                                       if( ! subpred ) {
+                                               jsonIteratorFree( search_itr );
+                                               buffer_free( sql_buf );
+                                               return NULL;
+                                       }
+
+                                       buffer_fadd( sql_buf, "( %s )", subpred );
+                                       free( subpred );
+                               }
+                       } else if( '-' == search_itr->key[ 0 ] ) {
+                               if( !strcasecmp( "-or", search_itr->key )) {
+                                       char* subpred = searchWHERE( node, class_info, OR_OP_JOIN, ctx );
+                                       if( ! subpred ) {
+                                               jsonIteratorFree( search_itr );
+                                               buffer_free( sql_buf );
+                                               return NULL;
+                                       }
+
+                                       buffer_fadd( sql_buf, "( %s )", subpred );
+                                       free( subpred );
+                               } else if( !strcasecmp( "-and", search_itr->key )) {
+                                       char* subpred = searchWHERE( node, class_info, AND_OP_JOIN, ctx );
+                                       if( ! subpred ) {
+                                               jsonIteratorFree( search_itr );
+                                               buffer_free( sql_buf );
+                                               return NULL;
+                                       }
+
+                                       buffer_fadd( sql_buf, "( %s )", subpred );
+                                       free( subpred );
+                               } else if( !strcasecmp("-not",search_itr->key) ) {
+                                       char* subpred = searchWHERE( node, class_info, AND_OP_JOIN, ctx );
+                                       if( ! subpred ) {
+                                               jsonIteratorFree( search_itr );
+                                               buffer_free( sql_buf );
+                                               return NULL;
+                                       }
+
+                                       buffer_fadd( sql_buf, " NOT ( %s )", subpred );
+                                       free( subpred );
+                               } else if( !strcasecmp( "-exists", search_itr->key )) {
+                                       char* subpred = buildQuery( ctx, node, SUBSELECT );
+                                       if( ! subpred ) {
+                                               jsonIteratorFree( search_itr );
+                                               buffer_free( sql_buf );
+                                               return NULL;
+                                       }
+
+                                       buffer_fadd( sql_buf, "EXISTS ( %s )", subpred );
+                                       free( subpred );
+                               } else if( !strcasecmp("-not-exists", search_itr->key )) {
+                                       char* subpred = buildQuery( ctx, node, SUBSELECT );
+                                       if( ! subpred ) {
+                                               jsonIteratorFree( search_itr );
+                                               buffer_free( sql_buf );
+                                               return NULL;
+                                       }
+
+                                       buffer_fadd( sql_buf, "NOT EXISTS ( %s )", subpred );
+                                       free( subpred );
+                               } else {     // Invalid "minus" operator
+                                       osrfLogError(
+                                                        OSRF_LOG_MARK,
+                                                       "%s: Invalid operator \"%s\" in WHERE clause",
+                                                       modulename,
+                                                       search_itr->key
+                                       );
+                                       jsonIteratorFree( search_itr );
+                                       buffer_free( sql_buf );
+                                       return NULL;
+                               }
+
+                       } else {
+
+                               const char* class = class_info->class_name;
+                               osrfHash* fields = class_info->fields;
+                               osrfHash* field = osrfHashGet( fields, search_itr->key );
+
+                               if( !field ) {
+                                       const char* table = class_info->source_def;
+                                       osrfLogError(
+                                               OSRF_LOG_MARK,
+                                               "%s: Attempt to reference non-existent column \"%s\" on %s (%s)",
+                                               modulename,
+                                               search_itr->key,
+                                               table ? table : "?",
+                                               class ? class : "?"
+                                       );
+                                       jsonIteratorFree( search_itr );
+                                       buffer_free( sql_buf );
+                                       return NULL;
+                               }
+
+                               char* subpred = searchPredicate( class_info, field, node, ctx );
+                               if( ! subpred ) {
+                                       buffer_free( sql_buf );
+                                       jsonIteratorFree( search_itr );
+                                       return NULL;
+                               }
+
+                               buffer_add( sql_buf, subpred );
+                               free( subpred );
+                       }
+               }
+               jsonIteratorFree( search_itr );
+
+       } else {
+               // ERROR ... only hash and array allowed at this level
+               char* predicate_string = jsonObjectToJSON( search_hash );
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s: Invalid predicate structure: %s",
+                       modulename,
+                       predicate_string
+               );
+               buffer_free( sql_buf );
+               free( predicate_string );
+               return NULL;
+       }
+
+       return buffer_release( sql_buf );
+}
+
+/* Build a JSON_ARRAY of field names for a given table alias
+*/
+static jsonObject* defaultSelectList( const char* table_alias ) {
+
+       if( ! table_alias )
+               table_alias = "";
+
+       ClassInfo* class_info = search_all_alias( table_alias );
+       if( ! class_info ) {
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s: Can't build default SELECT clause for \"%s\"; no such table alias",
+                       modulename,
+                       table_alias
+               );
+               return NULL;
+       }
+
+       jsonObject* array = jsonNewObjectType( JSON_ARRAY );
+       osrfHash* field_def = NULL;
+       osrfHashIterator* field_itr = osrfNewHashIterator( class_info->fields );
+       while( ( field_def = osrfHashIteratorNext( field_itr ) ) ) {
+               const char* field_name = osrfHashIteratorKey( field_itr );
+               if( ! str_is_true( osrfHashGet( field_def, "virtual" ) ) ) {
+                       jsonObjectPush( array, jsonNewObject( field_name ) );
+               }
+       }
+       osrfHashIteratorFree( field_itr );
+
+       return array;
+}
+
+// Translate a jsonObject into a UNION, INTERSECT, or EXCEPT query.
+// The jsonObject must be a JSON_HASH with an single entry for "union",
+// "intersect", or "except".  The data associated with this key must be an
+// array of hashes, each hash being a query.
+// Also allowed but currently ignored: entries for "order_by" and "alias".
+static char* doCombo( osrfMethodContext* ctx, jsonObject* combo, int flags ) {
+       // Sanity check
+       if( ! combo || combo->type != JSON_HASH )
+               return NULL;      // should be impossible; validated by caller
+
+       const jsonObject* query_array = NULL;   // array of subordinate queries
+       const char* op = NULL;     // name of operator, e.g. UNION
+       const char* alias = NULL;  // alias for the query (needed for ORDER BY)
+       int op_count = 0;          // for detecting conflicting operators
+       int excepting = 0;         // boolean
+       int all = 0;               // boolean
+       jsonObject* order_obj = NULL;
+
+       // Identify the elements in the hash
+       jsonIterator* query_itr = jsonNewIterator( combo );
+       jsonObject* curr_obj = NULL;
+       while( (curr_obj = jsonIteratorNext( query_itr ) ) ) {
+               if( ! strcmp( "union", query_itr->key ) ) {
+                       ++op_count;
+                       op = " UNION ";
+                       query_array = curr_obj;
+               } else if( ! strcmp( "intersect", query_itr->key ) ) {
+                       ++op_count;
+                       op = " INTERSECT ";
+                       query_array = curr_obj;
+               } else if( ! strcmp( "except", query_itr->key ) ) {
+                       ++op_count;
+                       op = " EXCEPT ";
+                       excepting = 1;
+                       query_array = curr_obj;
+               } else if( ! strcmp( "order_by", query_itr->key ) ) {
+                       osrfLogWarning(
+                               OSRF_LOG_MARK,
+                               "%s: ORDER BY not supported for UNION, INTERSECT, or EXCEPT",
+                               modulename
+                       );
+                       order_obj = curr_obj;
+               } else if( ! strcmp( "alias", query_itr->key ) ) {
+                       if( curr_obj->type != JSON_STRING ) {
+                               jsonIteratorFree( query_itr );
+                               return NULL;
+                       }
+                       alias = jsonObjectGetString( curr_obj );
+               } else if( ! strcmp( "all", query_itr->key ) ) {
+                       if( obj_is_true( curr_obj ) )
+                               all = 1;
+               } else {
+                       if( ctx )
+                               osrfAppSessionStatus(
+                                       ctx->session,
+                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                       "osrfMethodException",
+                                       ctx->request,
+                                       "Malformed query; unexpected entry in query object"
+                               );
+                       osrfLogError(
+                               OSRF_LOG_MARK,
+                               "%s: Unexpected entry for \"%s\" in%squery",
+                               modulename,
+                               query_itr->key,
+                               op
+                       );
+                       jsonIteratorFree( query_itr );
+                       return NULL;
+               }
+       }
+       jsonIteratorFree( query_itr );
+
+       // More sanity checks
+       if( ! query_array ) {
+               if( ctx )
+                       osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "Expected UNION, INTERSECT, or EXCEPT operator not found"
+                       );
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s: Expected UNION, INTERSECT, or EXCEPT operator not found",
+                       modulename
+               );
+               return NULL;        // should be impossible...
+       } else if( op_count > 1 ) {
+               if( ctx )
+                               osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "Found more than one of UNION, INTERSECT, and EXCEPT in same query"
+                       );
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s: Found more than one of UNION, INTERSECT, and EXCEPT in same query",
+                       modulename
+               );
+               return NULL;
+       } if( query_array->type != JSON_ARRAY ) {
+               if( ctx )
+                               osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "Malformed query: expected array of queries under UNION, INTERSECT or EXCEPT"
+                       );
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s: Expected JSON_ARRAY of queries for%soperator; found %s",
+                       modulename,
+                       op,
+                       json_type( query_array->type )
+               );
+               return NULL;
+       } if( query_array->size < 2 ) {
+               if( ctx )
+                       osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "UNION, INTERSECT or EXCEPT requires multiple queries as operands"
+                       );
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s:%srequires multiple queries as operands",
+                       modulename,
+                       op
+               );
+               return NULL;
+       } else if( excepting && query_array->size > 2 ) {
+               if( ctx )
+                       osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "EXCEPT operator has too many queries as operands"
+                       );
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s:EXCEPT operator has too many queries as operands",
+                       modulename
+               );
+               return NULL;
+       } else if( order_obj && ! alias ) {
+               if( ctx )
+                       osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "ORDER BY requires an alias for a UNION, INTERSECT, or EXCEPT"
+                       );
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s:ORDER BY requires an alias for a UNION, INTERSECT, or EXCEPT",
+                       modulename
+               );
+               return NULL;
+       }
+
+       // So far so good.  Now build the SQL.
+       growing_buffer* sql = buffer_init( 256 );
+
+       // If we nested inside another UNION, INTERSECT, or EXCEPT,
+       // Add a layer of parentheses
+       if( flags & SUBCOMBO )
+               OSRF_BUFFER_ADD( sql, "( " );
+
+       // Traverse the query array.  Each entry should be a hash.
+       int first = 1;   // boolean
+       int i = 0;
+       jsonObject* query = NULL;
+       while( (query = jsonObjectGetIndex( query_array, i++ )) ) {
+               if( query->type != JSON_HASH ) {
+                       if( ctx )
+                               osrfAppSessionStatus(
+                                       ctx->session,
+                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                       "osrfMethodException",
+                                       ctx->request,
+                                       "Malformed query under UNION, INTERSECT or EXCEPT"
+                               );
+                       osrfLogError(
+                               OSRF_LOG_MARK,
+                               "%s: Malformed query under%s -- expected JSON_HASH, found %s",
+                               modulename,
+                               op,
+                               json_type( query->type )
+                       );
+                       buffer_free( sql );
+                       return NULL;
+               }
+
+               if( first )
+                       first = 0;
+               else {
+                       OSRF_BUFFER_ADD( sql, op );
+                       if( all )
+                               OSRF_BUFFER_ADD( sql, "ALL " );
+               }
+
+               char* query_str = buildQuery( ctx, query, SUBSELECT | SUBCOMBO );
+               if( ! query_str ) {
+                       osrfLogError(
+                               OSRF_LOG_MARK,
+                               "%s: Error building query under%s",
+                               modulename,
+                               op
+                       );
+                       buffer_free( sql );
+                       return NULL;
+               }
+
+               OSRF_BUFFER_ADD( sql, query_str );
+       }
+
+       if( flags & SUBCOMBO )
+               OSRF_BUFFER_ADD_CHAR( sql, ')' );
+
+       if( !(flags & SUBSELECT) )
+               OSRF_BUFFER_ADD_CHAR( sql, ';' );
+
+       return buffer_release( sql );
+}
+
+// Translate a jsonObject into a SELECT, UNION, INTERSECT, or EXCEPT query.
+// The jsonObject must be a JSON_HASH with an entry for "from", "union", "intersect",
+// or "except" to indicate the type of query.
+char* buildQuery( osrfMethodContext* ctx, jsonObject* query, int flags ) {
+       // Sanity checks
+       if( ! query ) {
+               if( ctx )
+                       osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "Malformed query; no query object"
+                       );
+               osrfLogError( OSRF_LOG_MARK, "%s: Null pointer to query object", modulename );
+               return NULL;
+       } else if( query->type != JSON_HASH ) {
+               if( ctx )
+                       osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "Malformed query object"
+                       );
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s: Query object is %s instead of JSON_HASH",
+                       modulename,
+                       json_type( query->type )
+               );
+               return NULL;
+       }
+
+       // Determine what kind of query it purports to be, and dispatch accordingly.
+       if( jsonObjectGetKeyConst( query, "union" ) ||
+               jsonObjectGetKeyConst( query, "intersect" ) ||
+               jsonObjectGetKeyConst( query, "except" )) {
+               return doCombo( ctx, query, flags );
+       } else {
+               // It is presumably a SELECT query
+
+               // Push a node onto the stack for the current query.  Every level of
+               // subquery gets its own QueryFrame on the Stack.
+               push_query_frame();
+
+               // Build an SQL SELECT statement
+               char* sql = SELECT(
+                       ctx,
+                       jsonObjectGetKey( query, "select" ),
+                       jsonObjectGetKeyConst( query, "from" ),
+                       jsonObjectGetKeyConst( query, "where" ),
+                       jsonObjectGetKeyConst( query, "having" ),
+                       jsonObjectGetKeyConst( query, "order_by" ),
+                       jsonObjectGetKeyConst( query, "limit" ),
+                       jsonObjectGetKeyConst( query, "offset" ),
+                       flags
+               );
+               pop_query_frame();
+               return sql;
+       }
+}
+
+char* SELECT (
+               /* method context */ osrfMethodContext* ctx,
+
+               /* SELECT   */ jsonObject* selhash,
+               /* FROM     */ const jsonObject* join_hash,
+               /* WHERE    */ const jsonObject* search_hash,
+               /* HAVING   */ const jsonObject* having_hash,
+               /* ORDER BY */ const jsonObject* order_hash,
+               /* LIMIT    */ const jsonObject* limit,
+               /* OFFSET   */ const jsonObject* offset,
+               /* flags    */ int flags
+) {
+       const char* locale = osrf_message_get_last_locale();
+
+       // general tmp objects
+       const jsonObject* tmp_const;
+       jsonObject* selclass = NULL;
+       jsonObject* snode = NULL;
+       jsonObject* onode = NULL;
+
+       char* string = NULL;
+       int from_function = 0;
+       int first = 1;
+       int gfirst = 1;
+       //int hfirst = 1;
+
+       osrfLogDebug(OSRF_LOG_MARK, "cstore SELECT locale: %s", locale ? locale : "(none)" );
+
+       // punt if there's no FROM clause
+       if( !join_hash || ( join_hash->type == JSON_HASH && !join_hash->size )) {
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s: FROM clause is missing or empty",
+                       modulename
+               );
+               if( ctx )
+                       osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "FROM clause is missing or empty in JSON query"
+                       );
+               return NULL;
+       }
+
+       // the core search class
+       const char* core_class = NULL;
+
+       // get the core class -- the only key of the top level FROM clause, or a string
+       if( join_hash->type == JSON_HASH ) {
+               jsonIterator* tmp_itr = jsonNewIterator( join_hash );
+               snode = jsonIteratorNext( tmp_itr );
+
+               // Populate the current QueryFrame with information
+               // about the core class
+               if( add_query_core( NULL, tmp_itr->key ) ) {
+                       if( ctx )
+                               osrfAppSessionStatus(
+                                       ctx->session,
+                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                       "osrfMethodException",
+                                       ctx->request,
+                                       "Unable to look up core class"
+                               );
+                       return NULL;
+               }
+               core_class = curr_query->core.class_name;
+               join_hash = snode;
+
+               jsonObject* extra = jsonIteratorNext( tmp_itr );
+
+               jsonIteratorFree( tmp_itr );
+               snode = NULL;
+
+               // There shouldn't be more than one entry in join_hash
+               if( extra ) {
+                       osrfLogError(
+                               OSRF_LOG_MARK,
+                               "%s: Malformed FROM clause: extra entry in JSON_HASH",
+                               modulename
+                       );
+                       if( ctx )
+                               osrfAppSessionStatus(
+                                       ctx->session,
+                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                       "osrfMethodException",
+                                       ctx->request,
+                                       "Malformed FROM clause in JSON query"
+                               );
+                       return NULL;    // Malformed join_hash; extra entry
+               }
+       } else if( join_hash->type == JSON_ARRAY ) {
+               // We're selecting from a function, not from a table
+               from_function = 1;
+               core_class = jsonObjectGetString( jsonObjectGetIndex( join_hash, 0 ));
+               selhash = NULL;
+
+       } else if( join_hash->type == JSON_STRING ) {
+               // Populate the current QueryFrame with information
+               // about the core class
+               core_class = jsonObjectGetString( join_hash );
+               join_hash = NULL;
+               if( add_query_core( NULL, core_class ) ) {
+                       if( ctx )
+                               osrfAppSessionStatus(
+                                       ctx->session,
+                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                       "osrfMethodException",
+                                       ctx->request,
+                                       "Unable to look up core class"
+                               );
+                       return NULL;
+               }
+       }
+       else {
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s: FROM clause is unexpected JSON type: %s",
+                       modulename,
+                       json_type( join_hash->type )
+               );
+               if( ctx )
+                       osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "Ill-formed FROM clause in JSON query"
+                       );
+               return NULL;
+       }
+
+       // Build the join clause, if any, while filling out the list
+       // of joined classes in the current QueryFrame.
+       char* join_clause = NULL;
+       if( join_hash && ! from_function ) {
+
+               join_clause = searchJOIN( join_hash, &curr_query->core );
+               if( ! join_clause ) {
+                       if( ctx )
+                               osrfAppSessionStatus(
+                                       ctx->session,
+                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                       "osrfMethodException",
+                                       ctx->request,
+                                       "Unable to construct JOIN clause(s)"
+                               );
+                       return NULL;
+               }
+       }
+
+       // For in case we don't get a select list
+       jsonObject* defaultselhash = NULL;
+
+       // if there is no select list, build a default select list ...
+       if( !selhash && !from_function ) {
+               jsonObject* default_list = defaultSelectList( core_class );
+               if( ! default_list ) {
+                       if( ctx ) {
+                               osrfAppSessionStatus(
+                                       ctx->session,
+                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                       "osrfMethodException",
+                                       ctx->request,
+                                       "Unable to build default SELECT clause in JSON query"
+                               );
+                               free( join_clause );
+                               return NULL;
+                       }
+               }
+
+               selhash = defaultselhash = jsonNewObjectType( JSON_HASH );
+               jsonObjectSetKey( selhash, core_class, default_list );
+       }
+
+       // The SELECT clause can be encoded only by a hash
+       if( !from_function && selhash->type != JSON_HASH ) {
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s: Expected JSON_HASH for SELECT clause; found %s",
+                       modulename,
+                       json_type( selhash->type )
+               );
+
+               if( ctx )
+                       osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "Malformed SELECT clause in JSON query"
+                       );
+               free( join_clause );
+               return NULL;
+       }
+
+       // If you see a null or wild card specifier for the core class, or an
+       // empty array, replace it with a default SELECT list
+       tmp_const = jsonObjectGetKeyConst( selhash, core_class );
+       if( tmp_const ) {
+               int default_needed = 0;   // boolean
+               if( JSON_STRING == tmp_const->type
+                       && !strcmp( "*", jsonObjectGetString( tmp_const ) ))
+                               default_needed = 1;
+               else if( JSON_NULL == tmp_const->type )
+                       default_needed = 1;
+
+               if( default_needed ) {
+                       // Build a default SELECT list
+                       jsonObject* default_list = defaultSelectList( core_class );
+                       if( ! default_list ) {
+                               if( ctx ) {
+                                       osrfAppSessionStatus(
+                                               ctx->session,
+                                               OSRF_STATUS_INTERNALSERVERERROR,
+                                               "osrfMethodException",
+                                               ctx->request,
+                                               "Can't build default SELECT clause in JSON query"
+                                       );
+                                       free( join_clause );
+                                       return NULL;
+                               }
+                       }
+
+                       jsonObjectSetKey( selhash, core_class, default_list );
+               }
+       }
+
+       // temp buffers for the SELECT list and GROUP BY clause
+       growing_buffer* select_buf = buffer_init( 128 );
+       growing_buffer* group_buf  = buffer_init( 128 );
+
+       int aggregate_found = 0;     // boolean
+
+       // Build a select list
+       if( from_function )   // From a function we select everything
+               OSRF_BUFFER_ADD_CHAR( select_buf, '*' );
+       else {
+
+               // Build the SELECT list as SQL
+           int sel_pos = 1;
+           first = 1;
+           gfirst = 1;
+           jsonIterator* selclass_itr = jsonNewIterator( selhash );
+           while ( (selclass = jsonIteratorNext( selclass_itr )) ) {    // For each class
+
+                       const char* cname = selclass_itr->key;
+
+                       // Make sure the target relation is in the FROM clause.
+
+                       // At this point join_hash is a step down from the join_hash we
+                       // received as a parameter.  If the original was a JSON_STRING,
+                       // then json_hash is now NULL.  If the original was a JSON_HASH,
+                       // then json_hash is now the first (and only) entry in it,
+                       // denoting the core class.  We've already excluded the
+                       // possibility that the original was a JSON_ARRAY, because in
+                       // that case from_function would be non-NULL, and we wouldn't
+                       // be here.
+
+                       // If the current table alias isn't in scope, bail out
+                       ClassInfo* class_info = search_alias( cname );
+                       if( ! class_info ) {
+                               osrfLogError(
+                                       OSRF_LOG_MARK,
+                                       "%s: SELECT clause references class not in FROM clause: \"%s\"",
+                                       modulename,
+                                       cname
+                               );
+                               if( ctx )
+                                       osrfAppSessionStatus(
+                                               ctx->session,
+                                               OSRF_STATUS_INTERNALSERVERERROR,
+                                               "osrfMethodException",
+                                               ctx->request,
+                                               "Selected class not in FROM clause in JSON query"
+                                       );
+                               jsonIteratorFree( selclass_itr );
+                               buffer_free( select_buf );
+                               buffer_free( group_buf );
+                               if( defaultselhash )
+                                       jsonObjectFree( defaultselhash );
+                               free( join_clause );
+                               return NULL;
+                       }
+
+                       if( selclass->type != JSON_ARRAY ) {
+                               osrfLogError(
+                                       OSRF_LOG_MARK,
+                                       "%s: Malformed SELECT list for class \"%s\"; not an array",
+                                       modulename,
+                                       cname
+                               );
+                               if( ctx )
+                                       osrfAppSessionStatus(
+                                               ctx->session,
+                                               OSRF_STATUS_INTERNALSERVERERROR,
+                                               "osrfMethodException",
+                                               ctx->request,
+                                               "Selected class not in FROM clause in JSON query"
+                                       );
+
+                               jsonIteratorFree( selclass_itr );
+                               buffer_free( select_buf );
+                               buffer_free( group_buf );
+                               if( defaultselhash )
+                                       jsonObjectFree( defaultselhash );
+                               free( join_clause );
+                               return NULL;
+                       }
+
+                       // Look up some attributes of the current class
+                       osrfHash* idlClass        = class_info->class_def;
+                       osrfHash* class_field_set = class_info->fields;
+                       const char* class_pkey    = osrfHashGet( idlClass, "primarykey" );
+                       const char* class_tname   = osrfHashGet( idlClass, "tablename" );
+
+                       if( 0 == selclass->size ) {
+                               osrfLogWarning(
+                                       OSRF_LOG_MARK,
+                                       "%s: No columns selected from \"%s\"",
+                                       modulename,
+                                       cname
+                               );
+                       }
+
+                       // stitch together the column list for the current table alias...
+                       unsigned long field_idx = 0;
+                       jsonObject* selfield = NULL;
+                       while(( selfield = jsonObjectGetIndex( selclass, field_idx++ ) )) {
+
+                               // If we need a separator comma, add one
+                               if( first ) {
+                                       first = 0;
+                               } else {
+                                       OSRF_BUFFER_ADD_CHAR( select_buf, ',' );
+                               }
+
+                               // if the field specification is a string, add it to the list
+                               if( selfield->type == JSON_STRING ) {
+
+                                       // Look up the field in the IDL
+                                       const char* col_name = jsonObjectGetString( selfield );
+                                       osrfHash* field_def;
+
+                                       if (!osrfStringArrayContains(
+                                                       osrfHashGet(
+                                                               osrfHashGet( class_field_set, col_name ),
+                                                               "suppress_controller"),
+                                                       modulename
+                                       ))
+                                               field_def = osrfHashGet( class_field_set, col_name );
+
+                                       if( !field_def ) {
+                                               // No such field in current class
+                                               osrfLogError(
+                                                       OSRF_LOG_MARK,
+                                                       "%s: Selected column \"%s\" not defined in IDL for class \"%s\"",
+                                                       modulename,
+                                                       col_name,
+                                                       cname
+                                               );
+                                               if( ctx )
+                                                       osrfAppSessionStatus(
+                                                               ctx->session,
+                                                               OSRF_STATUS_INTERNALSERVERERROR,
+                                                               "osrfMethodException",
+                                                               ctx->request,
+                                                               "Selected column not defined in JSON query"
+                                                       );
+                                               jsonIteratorFree( selclass_itr );
+                                               buffer_free( select_buf );
+                                               buffer_free( group_buf );
+                                               if( defaultselhash )
+                                                       jsonObjectFree( defaultselhash );
+                                               free( join_clause );
+                                               return NULL;
+                                       } else if( str_is_true( osrfHashGet( field_def, "virtual" ) ) ) {
+                                               // Virtual field not allowed
+                                               osrfLogError(
+                                                       OSRF_LOG_MARK,
+                                                       "%s: Selected column \"%s\" for class \"%s\" is virtual",
+                                                       modulename,
+                                                       col_name,
+                                                       cname
+                                               );
+                                               if( ctx )
+                                                       osrfAppSessionStatus(
+                                                               ctx->session,
+                                                               OSRF_STATUS_INTERNALSERVERERROR,
+                                                               "osrfMethodException",
+                                                               ctx->request,
+                                                               "Selected column may not be virtual in JSON query"
+                                                       );
+                                               jsonIteratorFree( selclass_itr );
+                                               buffer_free( select_buf );
+                                               buffer_free( group_buf );
+                                               if( defaultselhash )
+                                                       jsonObjectFree( defaultselhash );
+                                               free( join_clause );
+                                               return NULL;
+                                       }
+
+                                       if( locale ) {
+                                               const char* i18n;
+                                               if( flags & DISABLE_I18N )
+                                                       i18n = NULL;
+                                               else
+                                                       i18n = osrfHashGet( field_def, "i18n" );
+
+                                               if( str_is_true( i18n ) ) {
+                                                       buffer_fadd( select_buf, " oils_i18n_xlate('%s', '%s', '%s', "
+                                                               "'%s', \"%s\".%s::TEXT, '%s') AS \"%s\"",
+                                                               class_tname, cname, col_name, class_pkey,
+                                                               cname, class_pkey, locale, col_name );
+                                               } else {
+                                                       buffer_fadd( select_buf, " \"%s\".%s AS \"%s\"",
+                                                               cname, col_name, col_name );
+                                               }
+                                       } else {
+                                               buffer_fadd( select_buf, " \"%s\".%s AS \"%s\"",
+                                                               cname, col_name, col_name );
+                                       }
+
+                               // ... but it could be an object, in which case we check for a Field Transform
+                               } else if( selfield->type == JSON_HASH ) {
+
+                                       const char* col_name = jsonObjectGetString(
+                                                       jsonObjectGetKeyConst( selfield, "column" ) );
+
+                                       // Get the field definition from the IDL
+                                       osrfHash* field_def;
+                                       if (!osrfStringArrayContains(
+                                                       osrfHashGet(
+                                                               osrfHashGet( class_field_set, col_name ),
+                                                               "suppress_controller"),
+                                                       modulename
+                                       ))
+                                               field_def = osrfHashGet( class_field_set, col_name );
+
+
+                                       if( !field_def ) {
+                                               // No such field in current class
+                                               osrfLogError(
+                                                       OSRF_LOG_MARK,
+                                                       "%s: Selected column \"%s\" is not defined in IDL for class \"%s\"",
+                                                       modulename,
+                                                       col_name,
+                                                       cname
+                                               );
+                                               if( ctx )
+                                                       osrfAppSessionStatus(
+                                                               ctx->session,
+                                                               OSRF_STATUS_INTERNALSERVERERROR,
+                                                               "osrfMethodException",
+                                                               ctx->request,
+                                                               "Selected column is not defined in JSON query"
+                                                       );
+                                               jsonIteratorFree( selclass_itr );
+                                               buffer_free( select_buf );
+                                               buffer_free( group_buf );
+                                               if( defaultselhash )
+                                                       jsonObjectFree( defaultselhash );
+                                               free( join_clause );
+                                               return NULL;
+                                       } else if( str_is_true( osrfHashGet( field_def, "virtual" ))) {
+                                               // No such field in current class
+                                               osrfLogError(
+                                                       OSRF_LOG_MARK,
+                                                       "%s: Selected column \"%s\" is virtual for class \"%s\"",
+                                                       modulename,
+                                                       col_name,
+                                                       cname
+                                               );
+                                               if( ctx )
+                                                       osrfAppSessionStatus(
+                                                               ctx->session,
+                                                               OSRF_STATUS_INTERNALSERVERERROR,
+                                                               "osrfMethodException",
+                                                               ctx->request,
+                                                               "Selected column is virtual in JSON query"
+                                                       );
+                                               jsonIteratorFree( selclass_itr );
+                                               buffer_free( select_buf );
+                                               buffer_free( group_buf );
+                                               if( defaultselhash )
+                                                       jsonObjectFree( defaultselhash );
+                                               free( join_clause );
+                                               return NULL;
+                                       }
+
+                                       // Decide what to use as a column alias
+                                       const char* _alias;
+                                       if((tmp_const = jsonObjectGetKeyConst( selfield, "alias" ))) {
+                                               _alias = jsonObjectGetString( tmp_const );
+                                       } else if((tmp_const = jsonObjectGetKeyConst( selfield, "result_field" ))) { // Use result_field name as the alias
+                                               _alias = jsonObjectGetString( tmp_const );
+                                       } else {         // Use field name as the alias
+                                               _alias = col_name;
+                                       }
+
+                                       if( jsonObjectGetKeyConst( selfield, "transform" )) {
+                                               char* transform_str = searchFieldTransform(
+                                                       class_info->alias, field_def, selfield );
+                                               if( transform_str ) {
+                                                       buffer_fadd( select_buf, " %s AS \"%s\"", transform_str, _alias );
+                                                       free( transform_str );
+                                               } else {
+                                                       if( ctx )
+                                                               osrfAppSessionStatus(
+                                                                       ctx->session,
+                                                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                                                       "osrfMethodException",
+                                                                       ctx->request,
+                                                                       "Unable to generate transform function in JSON query"
+                                                               );
+                                                       jsonIteratorFree( selclass_itr );
+                                                       buffer_free( select_buf );
+                                                       buffer_free( group_buf );
+                                                       if( defaultselhash )
+                                                               jsonObjectFree( defaultselhash );
+                                                       free( join_clause );
+                                                       return NULL;
+                                               }
+                                       } else {
+
+                                               if( locale ) {
+                                                       const char* i18n;
+                                                       if( flags & DISABLE_I18N )
+                                                               i18n = NULL;
+                                                       else
+                                                               i18n = osrfHashGet( field_def, "i18n" );
+
+                                                       if( str_is_true( i18n ) ) {
+                                                               buffer_fadd( select_buf,
+                                                                       " oils_i18n_xlate('%s', '%s', '%s', '%s', "
+                                                                       "\"%s\".%s::TEXT, '%s') AS \"%s\"",
+                                                                       class_tname, cname, col_name, class_pkey, cname,
+                                                                       class_pkey, locale, _alias );
+                                                       } else {
+                                                               buffer_fadd( select_buf, " \"%s\".%s AS \"%s\"",
+                                                                       cname, col_name, _alias );
+                                                       }
+                                               } else {
+                                                       buffer_fadd( select_buf, " \"%s\".%s AS \"%s\"",
+                                                               cname, col_name, _alias );
+                                               }
+                                       }
+                               }
+                               else {
+                                       osrfLogError(
+                                               OSRF_LOG_MARK,
+                                               "%s: Selected item is unexpected JSON type: %s",
+                                               modulename,
+                                               json_type( selfield->type )
+                                       );
+                                       if( ctx )
+                                               osrfAppSessionStatus(
+                                                       ctx->session,
+                                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                                       "osrfMethodException",
+                                                       ctx->request,
+                                                       "Ill-formed SELECT item in JSON query"
+                                               );
+                                       jsonIteratorFree( selclass_itr );
+                                       buffer_free( select_buf );
+                                       buffer_free( group_buf );
+                                       if( defaultselhash )
+                                               jsonObjectFree( defaultselhash );
+                                       free( join_clause );
+                                       return NULL;
+                               }
+
+                               const jsonObject* agg_obj = jsonObjectGetKeyConst( selfield, "aggregate" );
+                               if( obj_is_true( agg_obj ) )
+                                       aggregate_found = 1;
+                               else {
+                                       // Append a comma (except for the first one)
+                                       // and add the column to a GROUP BY clause
+                                       if( gfirst )
+                                               gfirst = 0;
+                                       else
+                                               OSRF_BUFFER_ADD_CHAR( group_buf, ',' );
+
+                                       buffer_fadd( group_buf, " %d", sel_pos );
+                               }
+
+#if 0
+                           if (is_agg->size || (flags & SELECT_DISTINCT)) {
+
+                                       const jsonObject* aggregate_obj = jsonObjectGetKeyConst( elfield, "aggregate");
+                                   if ( ! obj_is_true( aggregate_obj ) ) {
+                                           if (gfirst) {
+                                                   gfirst = 0;
+                                           } else {
+                                                       OSRF_BUFFER_ADD_CHAR( group_buf, ',' );
+                                           }
+
+                                           buffer_fadd(group_buf, " %d", sel_pos);
+
+                                       /*
+                                   } else if (is_agg = jsonObjectGetKeyConst( selfield, "having" )) {
+                                           if (gfirst) {
+                                                   gfirst = 0;
+                                           } else {
+                                                       OSRF_BUFFER_ADD_CHAR( group_buf, ',' );
+                                           }
+
+                                           _column = searchFieldTransform(class_info->alias, field, selfield);
+                                               OSRF_BUFFER_ADD_CHAR(group_buf, ' ');
+                                               OSRF_BUFFER_ADD(group_buf, _column);
+                                           _column = searchFieldTransform(class_info->alias, field, selfield);
+                                       */
+                                   }
+                           }
+#endif
+
+                               sel_pos++;
+                       } // end while -- iterating across SELECT columns
+
+               } // end while -- iterating across classes
+
+               jsonIteratorFree( selclass_itr );
+       }
+
+       char* col_list = buffer_release( select_buf );
+
+       // Make sure the SELECT list isn't empty.  This can happen, for example,
+       // if we try to build a default SELECT clause from a non-core table.
+
+       if( ! *col_list ) {
+               osrfLogError( OSRF_LOG_MARK, "%s: SELECT clause is empty", modulename );
+               if( ctx )
+                       osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "SELECT list is empty"
+               );
+               free( col_list );
+               buffer_free( group_buf );
+               if( defaultselhash )
+                       jsonObjectFree( defaultselhash );
+               free( join_clause );
+               return NULL;
+       }
+
+       char* table = NULL;
+       if( from_function )
+               table = searchValueTransform( join_hash );
+       else
+               table = strdup( curr_query->core.source_def );
+
+       if( !table ) {
+               if( ctx )
+                       osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "Unable to identify table for core class"
+                       );
+               free( col_list );
+               buffer_free( group_buf );
+               if( defaultselhash )
+                       jsonObjectFree( defaultselhash );
+               free( join_clause );
+               return NULL;
+       }
+
+       // Put it all together
+       growing_buffer* sql_buf = buffer_init( 128 );
+       buffer_fadd(sql_buf, "SELECT %s FROM %s AS \"%s\" ", col_list, table, core_class );
+       free( col_list );
+       free( table );
+
+       // Append the join clause, if any
+       if( join_clause ) {
+               buffer_add(sql_buf, join_clause );
+               free( join_clause );
+       }
+
+       char* order_by_list = NULL;
+       char* having_buf = NULL;
+
+       if( !from_function ) {
+
+               // Build a WHERE clause, if there is one
+               if( search_hash ) {
+                       buffer_add( sql_buf, " WHERE " );
+
+                       // and it's on the WHERE clause
+                       char* pred = searchWHERE( search_hash, &curr_query->core, AND_OP_JOIN, ctx );
+                       if( ! pred ) {
+                               if( ctx ) {
+                                       osrfAppSessionStatus(
+                                               ctx->session,
+                                               OSRF_STATUS_INTERNALSERVERERROR,
+                                               "osrfMethodException",
+                                               ctx->request,
+                                               "Severe query error in WHERE predicate -- see error log for more details"
+                                       );
+                               }
+                               buffer_free( group_buf );
+                               buffer_free( sql_buf );
+                               if( defaultselhash )
+                                       jsonObjectFree( defaultselhash );
+                               return NULL;
+                       }
+
+                       buffer_add( sql_buf, pred );
+                       free( pred );
+               }
+
+               // Build a HAVING clause, if there is one
+               if( having_hash ) {
+
+                       // and it's on the the WHERE clause
+                       having_buf = searchWHERE( having_hash, &curr_query->core, AND_OP_JOIN, ctx );
+
+                       if( ! having_buf ) {
+                               if( ctx ) {
+                                               osrfAppSessionStatus(
+                                               ctx->session,
+                                               OSRF_STATUS_INTERNALSERVERERROR,
+                                               "osrfMethodException",
+                                               ctx->request,
+                                               "Severe query error in HAVING predicate -- see error log for more details"
+                                       );
+                               }
+                               buffer_free( group_buf );
+                               buffer_free( sql_buf );
+                               if( defaultselhash )
+                                       jsonObjectFree( defaultselhash );
+                               return NULL;
+                       }
+               }
+
+               // Build an ORDER BY clause, if there is one
+               if( NULL == order_hash )
+                       ;  // No ORDER BY? do nothing
+               else if( JSON_ARRAY == order_hash->type ) {
+                       order_by_list = buildOrderByFromArray( ctx, order_hash );
+                       if( !order_by_list ) {
+                               free( having_buf );
+                               buffer_free( group_buf );
+                               buffer_free( sql_buf );
+                               if( defaultselhash )
+                                       jsonObjectFree( defaultselhash );
+                               return NULL;
+                       }
+               } else if( JSON_HASH == order_hash->type ) {
+                       // This hash is keyed on class alias.  Each class has either
+                       // an array of field names or a hash keyed on field name.
+                       growing_buffer* order_buf = NULL;  // to collect ORDER BY list
+                       jsonIterator* class_itr = jsonNewIterator( order_hash );
+                       while( (snode = jsonIteratorNext( class_itr )) ) {
+
+                               ClassInfo* order_class_info = search_alias( class_itr->key );
+                               if( ! order_class_info ) {
+                                       osrfLogError( OSRF_LOG_MARK,
+                                               "%s: Invalid class \"%s\" referenced in ORDER BY clause",
+                                               modulename, class_itr->key );
+                                       if( ctx )
+                                               osrfAppSessionStatus(
+                                                       ctx->session,
+                                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                                       "osrfMethodException",
+                                                       ctx->request,
+                                                       "Invalid class referenced in ORDER BY clause -- "
+                                                               "see error log for more details"
+                                               );
+                                       jsonIteratorFree( class_itr );
+                                       buffer_free( order_buf );
+                                       free( having_buf );
+                                       buffer_free( group_buf );
+                                       buffer_free( sql_buf );
+                                       if( defaultselhash )
+                                               jsonObjectFree( defaultselhash );
+                                       return NULL;
+                               }
+
+                               osrfHash* field_list_def = order_class_info->fields;
+
+                               if( snode->type == JSON_HASH ) {
+
+                                       // Hash is keyed on field names from the current class.  For each field
+                                       // there is another layer of hash to define the sorting details, if any,
+                                       // or a string to indicate direction of sorting.
+                                       jsonIterator* order_itr = jsonNewIterator( snode );
+                                       while( (onode = jsonIteratorNext( order_itr )) ) {
+
+                                               osrfHash* field_def = osrfHashGet( field_list_def, order_itr->key );
+                                               if( !field_def ) {
+                                                       osrfLogError( OSRF_LOG_MARK,
+                                                               "%s: Invalid field \"%s\" in ORDER BY clause",
+                                                               modulename, order_itr->key );
+                                                       if( ctx )
+                                                               osrfAppSessionStatus(
+                                                                       ctx->session,
+                                                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                                                       "osrfMethodException",
+                                                                       ctx->request,
+                                                                       "Invalid field in ORDER BY clause -- "
+                                                                       "see error log for more details"
+                                                               );
+                                                       jsonIteratorFree( order_itr );
+                                                       jsonIteratorFree( class_itr );
+                                                       buffer_free( order_buf );
+                                                       free( having_buf );
+                                                       buffer_free( group_buf );
+                                                       buffer_free( sql_buf );
+                                                       if( defaultselhash )
+                                                               jsonObjectFree( defaultselhash );
+                                                       return NULL;
+                                               } else if( str_is_true( osrfHashGet( field_def, "virtual" ) ) ) {
+                                                       osrfLogError( OSRF_LOG_MARK,
+                                                               "%s: Virtual field \"%s\" in ORDER BY clause",
+                                                               modulename, order_itr->key );
+                                                       if( ctx )
+                                                               osrfAppSessionStatus(
+                                                                       ctx->session,
+                                                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                                                       "osrfMethodException",
+                                                                       ctx->request,
+                                                                       "Virtual field in ORDER BY clause -- "
+                                                                       "see error log for more details"
+                                                       );
+                                                       jsonIteratorFree( order_itr );
+                                                       jsonIteratorFree( class_itr );
+                                                       buffer_free( order_buf );
+                                                       free( having_buf );
+                                                       buffer_free( group_buf );
+                                                       buffer_free( sql_buf );
+                                                       if( defaultselhash )
+                                                               jsonObjectFree( defaultselhash );
+                                                       return NULL;
+                                               }
+
+                                               const char* direction = NULL;
+                                               if( onode->type == JSON_HASH ) {
+                                                       if( jsonObjectGetKeyConst( onode, "transform" ) ) {
+                                                               string = searchFieldTransform(
+                                                                       class_itr->key,
+                                                                       osrfHashGet( field_list_def, order_itr->key ),
+                                                                       onode
+                                                               );
+                                                               if( ! string ) {
+                                                                       if( ctx ) osrfAppSessionStatus(
+                                                                               ctx->session,
+                                                                               OSRF_STATUS_INTERNALSERVERERROR,
+                                                                               "osrfMethodException",
+                                                                               ctx->request,
+                                                                               "Severe query error in ORDER BY clause -- "
+                                                                               "see error log for more details"
+                                                                       );
+                                                                       jsonIteratorFree( order_itr );
+                                                                       jsonIteratorFree( class_itr );
+                                                                       free( having_buf );
+                                                                       buffer_free( group_buf );
+                                                                       buffer_free( order_buf);
+                                                                       buffer_free( sql_buf );
+                                                                       if( defaultselhash )
+                                                                               jsonObjectFree( defaultselhash );
+                                                                       return NULL;
+                                                               }
+                                                       } else {
+                                                               growing_buffer* field_buf = buffer_init( 16 );
+                                                               buffer_fadd( field_buf, "\"%s\".%s",
+                                                                       class_itr->key, order_itr->key );
+                                                               string = buffer_release( field_buf );
+                                                       }
+
+                                                       if( (tmp_const = jsonObjectGetKeyConst( onode, "direction" )) ) {
+                                                               const char* dir = jsonObjectGetString( tmp_const );
+                                                               if(!strncasecmp( dir, "d", 1 )) {
+                                                                       direction = " DESC";
+                                                               } else {
+                                                                       direction = " ASC";
+                                                               }
+                                                       }
+
+                                               } else if( JSON_NULL == onode->type || JSON_ARRAY == onode->type ) {
+                                                       osrfLogError( OSRF_LOG_MARK,
+                                                               "%s: Expected JSON_STRING in ORDER BY clause; found %s",
+                                                               modulename, json_type( onode->type ) );
+                                                       if( ctx )
+                                                               osrfAppSessionStatus(
+                                                                       ctx->session,
+                                                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                                                       "osrfMethodException",
+                                                                       ctx->request,
+                                                                       "Malformed ORDER BY clause -- see error log for more details"
+                                                               );
+                                                       jsonIteratorFree( order_itr );
+                                                       jsonIteratorFree( class_itr );
+                                                       free( having_buf );
+                                                       buffer_free( group_buf );
+                                                       buffer_free( order_buf );
+                                                       buffer_free( sql_buf );
+                                                       if( defaultselhash )
+                                                               jsonObjectFree( defaultselhash );
+                                                       return NULL;
+
+                                               } else {
+                                                       string = strdup( order_itr->key );
+                                                       const char* dir = jsonObjectGetString( onode );
+                                                       if( !strncasecmp( dir, "d", 1 )) {
+                                                               direction = " DESC";
+                                                       } else {
+                                                               direction = " ASC";
+                                                       }
+                                               }
+
+                                               if( order_buf )
+                                                       OSRF_BUFFER_ADD( order_buf, ", " );
+                                               else
+                                                       order_buf = buffer_init( 128 );
+
+                                               OSRF_BUFFER_ADD( order_buf, string );
+                                               free( string );
+
+                                               if( direction ) {
+                                                        OSRF_BUFFER_ADD( order_buf, direction );
+                                               }
+
+                                       } // end while
+                                       jsonIteratorFree( order_itr );
+
+                               } else if( snode->type == JSON_ARRAY ) {
+
+                                       // Array is a list of fields from the current class
+                                       unsigned long order_idx = 0;
+                                       while(( onode = jsonObjectGetIndex( snode, order_idx++ ) )) {
+
+                                               const char* _f = jsonObjectGetString( onode );
+
+                                               osrfHash* field_def = osrfHashGet( field_list_def, _f );
+                                               if( !field_def ) {
+                                                       osrfLogError( OSRF_LOG_MARK,
+                                                                       "%s: Invalid field \"%s\" in ORDER BY clause",
+                                                                       modulename, _f );
+                                                       if( ctx )
+                                                               osrfAppSessionStatus(
+                                                                       ctx->session,
+                                                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                                                       "osrfMethodException",
+                                                                       ctx->request,
+                                                                       "Invalid field in ORDER BY clause -- "
+                                                                       "see error log for more details"
+                                                               );
+                                                       jsonIteratorFree( class_itr );
+                                                       buffer_free( order_buf );
+                                                       free( having_buf );
+                                                       buffer_free( group_buf );
+                                                       buffer_free( sql_buf );
+                                                       if( defaultselhash )
+                                                               jsonObjectFree( defaultselhash );
+                                                       return NULL;
+                                               } else if( str_is_true( osrfHashGet( field_def, "virtual" ) ) ) {
+                                                       osrfLogError( OSRF_LOG_MARK,
+                                                               "%s: Virtual field \"%s\" in ORDER BY clause",
+                                                               modulename, _f );
+                                                       if( ctx )
+                                                               osrfAppSessionStatus(
+                                                                       ctx->session,
+                                                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                                                       "osrfMethodException",
+                                                                       ctx->request,
+                                                                       "Virtual field in ORDER BY clause -- "
+                                                                       "see error log for more details"
+                                                               );
+                                                       jsonIteratorFree( class_itr );
+                                                       buffer_free( order_buf );
+                                                       free( having_buf );
+                                                       buffer_free( group_buf );
+                                                       buffer_free( sql_buf );
+                                                       if( defaultselhash )
+                                                               jsonObjectFree( defaultselhash );
+                                                       return NULL;
+                                               }
+
+                                               if( order_buf )
+                                                       OSRF_BUFFER_ADD( order_buf, ", " );
+                                               else
+                                                       order_buf = buffer_init( 128 );
+
+                                               buffer_fadd( order_buf, "\"%s\".%s", class_itr->key, _f );
+
+                                       } // end while
+
+                               // IT'S THE OOOOOOOOOOOLD STYLE!
+                               } else {
+                                       osrfLogError( OSRF_LOG_MARK,
+                                               "%s: Possible SQL injection attempt; direct order by is not allowed",
+                                               modulename );
+                                       if(ctx) {
+                                               osrfAppSessionStatus(
+                                                       ctx->session,
+                                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                                       "osrfMethodException",
+                                                       ctx->request,
+                                                       "Severe query error -- see error log for more details"
+                                               );
+                                       }
+
+                                       free( having_buf );
+                                       buffer_free( group_buf );
+                                       buffer_free( order_buf );
+                                       buffer_free( sql_buf );
+                                       if( defaultselhash )
+                                               jsonObjectFree( defaultselhash );
+                                       jsonIteratorFree( class_itr );
+                                       return NULL;
+                               }
+                       } // end while
+                       jsonIteratorFree( class_itr );
+                       if( order_buf )
+                               order_by_list = buffer_release( order_buf );
+               } else {
+                       osrfLogError( OSRF_LOG_MARK,
+                               "%s: Malformed ORDER BY clause; expected JSON_HASH or JSON_ARRAY, found %s",
+                               modulename, json_type( order_hash->type ) );
+                       if( ctx )
+                               osrfAppSessionStatus(
+                                       ctx->session,
+                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                       "osrfMethodException",
+                                       ctx->request,
+                                       "Malformed ORDER BY clause -- see error log for more details"
+                               );
+                       free( having_buf );
+                       buffer_free( group_buf );
+                       buffer_free( sql_buf );
+                       if( defaultselhash )
+                               jsonObjectFree( defaultselhash );
+                       return NULL;
+               }
+       }
+
+       string = buffer_release( group_buf );
+
+       if( *string && ( aggregate_found || (flags & SELECT_DISTINCT) ) ) {
+               OSRF_BUFFER_ADD( sql_buf, " GROUP BY " );
+               OSRF_BUFFER_ADD( sql_buf, string );
+       }
+
+       free( string );
+
+       if( having_buf && *having_buf ) {
+               OSRF_BUFFER_ADD( sql_buf, " HAVING " );
+               OSRF_BUFFER_ADD( sql_buf, having_buf );
+               free( having_buf );
+       }
+
+       if( order_by_list ) {
+
+               if( *order_by_list ) {
+                       OSRF_BUFFER_ADD( sql_buf, " ORDER BY " );
+                       OSRF_BUFFER_ADD( sql_buf, order_by_list );
+               }
+
+               free( order_by_list );
+       }
+
+       if( limit ){
+               const char* str = jsonObjectGetString( limit );
+               if (str) { // limit could be JSON_NULL, etc.
+                       buffer_fadd( sql_buf, " LIMIT %d", atoi( str ));
+               }
+       }
+
+       if( offset ) {
+               const char* str = jsonObjectGetString( offset );
+               if (str) {
+                       buffer_fadd( sql_buf, " OFFSET %d", atoi( str ));
+               }
+       }
+
+       if( !(flags & SUBSELECT) )
+               OSRF_BUFFER_ADD_CHAR( sql_buf, ';' );
+
+       if( defaultselhash )
+                jsonObjectFree( defaultselhash );
+
+       return buffer_release( sql_buf );
+
+} // end of SELECT()
+
+/**
+       @brief Build a list of ORDER BY expressions.
+       @param ctx Pointer to the method context.
+       @param order_array Pointer to a JSON_ARRAY of field specifications.
+       @return Pointer to a string containing a comma-separated list of ORDER BY expressions.
+       Each expression may be either a column reference or a function call whose first parameter
+       is a column reference.
+
+       Each entry in @a order_array must be a JSON_HASH with values for "class" and "field".
+       It may optionally include entries for "direction" and/or "transform".
+
+       The calling code is responsible for freeing the returned string.
+*/
+static char* buildOrderByFromArray( osrfMethodContext* ctx, const jsonObject* order_array ) {
+       if( ! order_array ) {
+               osrfLogError( OSRF_LOG_MARK, "%s: Logic error: NULL pointer for ORDER BY clause",
+                       modulename );
+               if( ctx )
+                       osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "Logic error: ORDER BY clause expected, not found; "
+                                       "see error log for more details"
+                       );
+               return NULL;
+       } else if( order_array->type != JSON_ARRAY ) {
+               osrfLogError( OSRF_LOG_MARK,
+                       "%s: Logic error: Expected JSON_ARRAY for ORDER BY clause, not found", modulename );
+               if( ctx )
+                       osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_INTERNALSERVERERROR,
+                       "osrfMethodException",
+                       ctx->request,
+                       "Logic error: Unexpected format for ORDER BY clause; see error log for more details" );
+               return NULL;
+       }
+
+       growing_buffer* order_buf = buffer_init( 128 );
+       int first = 1;        // boolean
+       int order_idx = 0;
+       jsonObject* order_spec;
+       while( (order_spec = jsonObjectGetIndex( order_array, order_idx++ ))) {
+
+               if( JSON_HASH != order_spec->type ) {
+                       osrfLogError( OSRF_LOG_MARK,
+                               "%s: Malformed field specification in ORDER BY clause; "
+                               "expected JSON_HASH, found %s",
+                               modulename, json_type( order_spec->type ) );
+                       if( ctx )
+                               osrfAppSessionStatus(
+                                        ctx->session,
+                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                       "osrfMethodException",
+                                       ctx->request,
+                                       "Malformed ORDER BY clause -- see error log for more details"
+                               );
+                       buffer_free( order_buf );
+                       return NULL;
+               }
+
+               const char* class_alias =
+                       jsonObjectGetString( jsonObjectGetKeyConst( order_spec, "class" ));
+               const char* field =
+                       jsonObjectGetString( jsonObjectGetKeyConst( order_spec, "field" ));
+
+               jsonObject* compare_to = jsonObjectGetKeyConst( order_spec, "compare" );
+
+               if( !field || !class_alias ) {
+                       osrfLogError( OSRF_LOG_MARK,
+                               "%s: Missing class or field name in field specification of ORDER BY clause",
+                               modulename );
+                       if( ctx )
+                               osrfAppSessionStatus(
+                                       ctx->session,
+                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                       "osrfMethodException",
+                                       ctx->request,
+                                       "Malformed ORDER BY clause -- see error log for more details"
+                               );
+                       buffer_free( order_buf );
+                       return NULL;
+               }
+
+               const ClassInfo* order_class_info = search_alias( class_alias );
+               if( ! order_class_info ) {
+                       osrfLogInternal( OSRF_LOG_MARK, "%s: ORDER BY clause references class \"%s\" "
+                               "not in FROM clause, skipping it", modulename, class_alias );
+                       continue;
+               }
+
+               // Add a separating comma, except at the beginning
+               if( first )
+                       first = 0;
+               else
+                       OSRF_BUFFER_ADD( order_buf, ", " );
+
+               osrfHash* field_def = osrfHashGet( order_class_info->fields, field );
+               if( !field_def ) {
+                       osrfLogError( OSRF_LOG_MARK,
+                               "%s: Invalid field \"%s\".%s referenced in ORDER BY clause",
+                               modulename, class_alias, field );
+                       if( ctx )
+                               osrfAppSessionStatus(
+                                       ctx->session,
+                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                       "osrfMethodException",
+                                       ctx->request,
+                                       "Invalid field referenced in ORDER BY clause -- "
+                                       "see error log for more details"
+                               );
+                       free( order_buf );
+                       return NULL;
+               } else if( str_is_true( osrfHashGet( field_def, "virtual" ) ) ) {
+                       osrfLogError( OSRF_LOG_MARK, "%s: Virtual field \"%s\" in ORDER BY clause",
+                               modulename, field );
+                       if( ctx )
+                               osrfAppSessionStatus(
+                                       ctx->session,
+                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                       "osrfMethodException",
+                                       ctx->request,
+                                       "Virtual field in ORDER BY clause -- see error log for more details"
+                               );
+                       buffer_free( order_buf );
+                       return NULL;
+               }
+
+               if( jsonObjectGetKeyConst( order_spec, "transform" )) {
+                       char* transform_str = searchFieldTransform( class_alias, field_def, order_spec );
+                       if( ! transform_str ) {
+                               if( ctx )
+                                       osrfAppSessionStatus(
+                                               ctx->session,
+                                               OSRF_STATUS_INTERNALSERVERERROR,
+                                               "osrfMethodException",
+                                               ctx->request,
+                                               "Severe query error in ORDER BY clause -- "
+                                               "see error log for more details"
+                                       );
+                               buffer_free( order_buf );
+                               return NULL;
+                       }
+
+                       OSRF_BUFFER_ADD( order_buf, transform_str );
+                       free( transform_str );
+               } else if( compare_to ) {
+                       char* compare_str = searchPredicate( order_class_info, field_def, compare_to, ctx );
+                       if( ! compare_str ) {
+                               if( ctx )
+                                       osrfAppSessionStatus(
+                                               ctx->session,
+                                               OSRF_STATUS_INTERNALSERVERERROR,
+                                               "osrfMethodException",
+                                               ctx->request,
+                                               "Severe query error in ORDER BY clause -- "
+                                               "see error log for more details"
+                                       );
+                               buffer_free( order_buf );
+                               return NULL;
+                       }
+
+                       buffer_fadd( order_buf, "(%s)", compare_str );
+                       free( compare_str );
+               }
+               else
+                       buffer_fadd( order_buf, "\"%s\".%s", class_alias, field );
+
+               const char* direction =
+                       jsonObjectGetString( jsonObjectGetKeyConst( order_spec, "direction" ) );
+               if( direction ) {
+                       if( direction[ 0 ] && ( 'd' == direction[ 0 ] || 'D' == direction[ 0 ] ) )
+                               OSRF_BUFFER_ADD( order_buf, " DESC" );
+                       else
+                               OSRF_BUFFER_ADD( order_buf, " ASC" );
+               }
+       }
+
+       return buffer_release( order_buf );
+}
+
+/**
+       @brief Build a SELECT statement.
+       @param search_hash Pointer to a JSON_HASH or JSON_ARRAY encoding the WHERE clause.
+       @param rest_of_query Pointer to a JSON_HASH containing any other SQL clauses.
+       @param meta Pointer to the class metadata for the core class.
+       @param ctx Pointer to the method context.
+       @return Pointer to a character string containing the WHERE clause; or NULL upon error.
+
+       Within the rest_of_query hash, the meaningful keys are "join", "select", "no_i18n",
+       "order_by", "limit", and "offset".
+
+       The SELECT statements built here are distinct from those built for the json_query method.
+*/
+static char* buildSELECT ( const jsonObject* search_hash, jsonObject* rest_of_query,
+       osrfHash* meta, osrfMethodContext* ctx ) {
+
+       const char* locale = osrf_message_get_last_locale();
+
+       osrfHash* fields = osrfHashGet( meta, "fields" );
+       const char* core_class = osrfHashGet( meta, "classname" );
+
+       const jsonObject* join_hash = jsonObjectGetKeyConst( rest_of_query, "join" );
+
+       jsonObject* selhash = NULL;
+       jsonObject* defaultselhash = NULL;
+
+       growing_buffer* sql_buf = buffer_init( 128 );
+       growing_buffer* select_buf = buffer_init( 128 );
+
+       if( !(selhash = jsonObjectGetKey( rest_of_query, "select" )) ) {
+               defaultselhash = jsonNewObjectType( JSON_HASH );
+               selhash = defaultselhash;
+       }
+
+       // If there's no SELECT list for the core class, build one
+       if( !jsonObjectGetKeyConst( selhash, core_class ) ) {
+               jsonObject* field_list = jsonNewObjectType( JSON_ARRAY );
+
+               // Add every non-virtual field to the field list
+               osrfHash* field_def = NULL;
+               osrfHashIterator* field_itr = osrfNewHashIterator( fields );
+               while( ( field_def = osrfHashIteratorNext( field_itr ) ) ) {
+                       if( ! str_is_true( osrfHashGet( field_def, "virtual" ) ) ) {
+                               const char* field = osrfHashIteratorKey( field_itr );
+                               jsonObjectPush( field_list, jsonNewObject( field ) );
+                       }
+               }
+               osrfHashIteratorFree( field_itr );
+               jsonObjectSetKey( selhash, core_class, field_list );
+       }
+
+       // Build a list of columns for the SELECT clause
+       int first = 1;
+       const jsonObject* snode = NULL;
+       jsonIterator* class_itr = jsonNewIterator( selhash );
+       while( (snode = jsonIteratorNext( class_itr )) ) {        // For each class
+
+               // If the class isn't in the IDL, ignore it
+               const char* cname = class_itr->key;
+               osrfHash* idlClass = osrfHashGet( oilsIDL(), cname );
+               if( !idlClass )
+                       continue;
+
+               // If the class isn't the core class, and isn't in the JOIN clause, ignore it
+               if( strcmp( core_class, class_itr->key )) {
+                       if( !join_hash )
+                               continue;
+
+                       jsonObject* found = jsonObjectFindPath( join_hash, "//%s", class_itr->key );
+                       if( !found->size ) {
+                               jsonObjectFree( found );
+                               continue;
+                       }
+
+                       jsonObjectFree( found );
+               }
+
+               const jsonObject* node = NULL;
+               jsonIterator* select_itr = jsonNewIterator( snode );
+               while( (node = jsonIteratorNext( select_itr )) ) {
+                       const char* item_str = jsonObjectGetString( node );
+                       osrfHash* field = osrfHashGet( osrfHashGet( idlClass, "fields" ), item_str );
+                       char* fname = osrfHashGet( field, "name" );
+
+                       if( !field )
+                               continue;
+
+                       if (osrfStringArrayContains( osrfHashGet(field, "suppress_controller"), modulename ))
+                               continue;
+
+                       if( first ) {
+                               first = 0;
+                       } else {
+                               OSRF_BUFFER_ADD_CHAR( select_buf, ',' );
+                       }
+
+                       if( locale ) {
+                               const char* i18n;
+                               const jsonObject* no_i18n_obj = jsonObjectGetKeyConst( rest_of_query, "no_i18n" );
+                               if( obj_is_true( no_i18n_obj ) )    // Suppress internationalization?
+                                       i18n = NULL;
+                               else
+                                       i18n = osrfHashGet( field, "i18n" );
+
+                               if( str_is_true( i18n ) ) {
+                                       char* pkey = osrfHashGet( idlClass, "primarykey" );
+                                       char* tname = osrfHashGet( idlClass, "tablename" );
+
+                                       buffer_fadd( select_buf, " oils_i18n_xlate('%s', '%s', '%s', "
+                                                       "'%s', \"%s\".%s::TEXT, '%s') AS \"%s\"",
+                                                       tname, cname, fname, pkey, cname, pkey, locale, fname );
+                               } else {
+                                       buffer_fadd( select_buf, " \"%s\".%s", cname, fname );
+                               }
+                       } else {
+                               buffer_fadd( select_buf, " \"%s\".%s", cname, fname );
+                       }
+               }
+
+               jsonIteratorFree( select_itr );
+       }
+
+       jsonIteratorFree( class_itr );
+
+       char* col_list = buffer_release( select_buf );
+       char* table = oilsGetRelation( meta );
+       if( !table )
+               table = strdup( "(null)" );
+
+       buffer_fadd( sql_buf, "SELECT %s FROM %s AS \"%s\"", col_list, table, core_class );
+       free( col_list );
+       free( table );
+
+       // Clear the query stack (as a fail-safe precaution against possible
+       // leftover garbage); then push the first query frame onto the stack.
+       clear_query_stack();
+       push_query_frame();
+       if( add_query_core( NULL, core_class ) ) {
+               if( ctx )
+                       osrfAppSessionStatus(
+                               ctx->session,
+                               OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "Unable to build query frame for core class"
+                       );
+               buffer_free( sql_buf );
+               if( defaultselhash )
+                       jsonObjectFree( defaultselhash );
+               return NULL;
+       }
+
+       // Add the JOIN clauses, if any
+       if( join_hash ) {
+               char* join_clause = searchJOIN( join_hash, &curr_query->core );
+               OSRF_BUFFER_ADD_CHAR( sql_buf, ' ' );
+               OSRF_BUFFER_ADD( sql_buf, join_clause );
+               free( join_clause );
+       }
+
+       osrfLogDebug( OSRF_LOG_MARK, "%s pre-predicate SQL =  %s",
+               modulename, OSRF_BUFFER_C_STR( sql_buf ));
+
+       OSRF_BUFFER_ADD( sql_buf, " WHERE " );
+
+       // Add the conditions in the WHERE clause
+       char* pred = searchWHERE( search_hash, &curr_query->core, AND_OP_JOIN, ctx );
+       if( !pred ) {
+               osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException",
+                               ctx->request,
+                               "Severe query error -- see error log for more details"
+                       );
+               buffer_free( sql_buf );
+               if( defaultselhash )
+                       jsonObjectFree( defaultselhash );
+               clear_query_stack();
+               return NULL;
+       } else {
+               buffer_add( sql_buf, pred );
+               free( pred );
+       }
+
+       // Add the ORDER BY, LIMIT, and/or OFFSET clauses, if present
+       if( rest_of_query ) {
+               const jsonObject* order_by = NULL;
+               if( ( order_by = jsonObjectGetKeyConst( rest_of_query, "order_by" )) ){
+
+                       char* order_by_list = NULL;
+
+                       if( JSON_ARRAY == order_by->type ) {
+                               order_by_list = buildOrderByFromArray( ctx, order_by );
+                               if( !order_by_list ) {
+                                       buffer_free( sql_buf );
+                                       if( defaultselhash )
+                                               jsonObjectFree( defaultselhash );
+                                       clear_query_stack();
+                                       return NULL;
+                               }
+                       } else if( JSON_HASH == order_by->type ) {
+                               // We expect order_by to be a JSON_HASH keyed on class names.  Traverse it
+                               // and build a list of ORDER BY expressions.
+                               growing_buffer* order_buf = buffer_init( 128 );
+                               first = 1;
+                               jsonIterator* class_itr = jsonNewIterator( order_by );
+                               while( (snode = jsonIteratorNext( class_itr )) ) {  // For each class:
+
+                                       ClassInfo* order_class_info = search_alias( class_itr->key );
+                                       if( ! order_class_info )
+                                               continue;    // class not referenced by FROM clause?  Ignore it.
+
+                                       if( JSON_HASH == snode->type ) {
+
+                                               // If the data for the current class is a JSON_HASH, then it is
+                                               // keyed on field name.
+
+                                               const jsonObject* onode = NULL;
+                                               jsonIterator* order_itr = jsonNewIterator( snode );
+                                               while( (onode = jsonIteratorNext( order_itr )) ) {  // For each field
+
+                                                       osrfHash* field_def = osrfHashGet(
+                                                               order_class_info->fields, order_itr->key );
+                                                       if( !field_def )
+                                                               continue;    // Field not defined in IDL?  Ignore it.
+                                                       if( str_is_true( osrfHashGet( field_def, "virtual")))
+                                                               continue;    // Field is virtual?  Ignore it.
+
+                                                       char* field_str = NULL;
+                                                       char* direction = NULL;
+                                                       if( onode->type == JSON_HASH ) {
+                                                               if( jsonObjectGetKeyConst( onode, "transform" ) ) {
+                                                                       field_str = searchFieldTransform(
+                                                                               class_itr->key, field_def, onode );
+                                                                       if( ! field_str ) {
+                                                                               osrfAppSessionStatus(
+                                                                                       ctx->session,
+                                                                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                                                                       "osrfMethodException",
+                                                                                       ctx->request,
+                                                                                       "Severe query error in ORDER BY clause -- "
+                                                                                       "see error log for more details"
+                                                                               );
+                                                                               jsonIteratorFree( order_itr );
+                                                                               jsonIteratorFree( class_itr );
+                                                                               buffer_free( order_buf );
+                                                                               buffer_free( sql_buf );
+                                                                               if( defaultselhash )
+                                                                                       jsonObjectFree( defaultselhash );
+                                                                               clear_query_stack();
+                                                                               return NULL;
+                                                                       }
+                                                               } else {
+                                                                       growing_buffer* field_buf = buffer_init( 16 );
+                                                                       buffer_fadd( field_buf, "\"%s\".%s",
+                                                                               class_itr->key, order_itr->key );
+                                                                       field_str = buffer_release( field_buf );
+                                                               }
+
+                                                               if( ( order_by = jsonObjectGetKeyConst( onode, "direction" )) ) {
+                                                                       const char* dir = jsonObjectGetString( order_by );
+                                                                       if(!strncasecmp( dir, "d", 1 )) {
+                                                                               direction = " DESC";
+                                                                       }
+                                                               }
+                                                       } else {
+                                                               field_str = strdup( order_itr->key );
+                                                               const char* dir = jsonObjectGetString( onode );
+                                                               if( !strncasecmp( dir, "d", 1 )) {
+                                                                       direction = " DESC";
+                                                               } else {
+                                                                       direction = " ASC";
+                                                               }
+                                                       }
+
+                                                       if( first ) {
+                                                               first = 0;
+                                                       } else {
+                                                               buffer_add( order_buf, ", " );
+                                                       }
+
+                                                       buffer_add( order_buf, field_str );
+                                                       free( field_str );
+
+                                                       if( direction ) {
+                                                               buffer_add( order_buf, direction );
+                                                       }
+                                               } // end while; looping over ORDER BY expressions
+
+                                               jsonIteratorFree( order_itr );
+
+                                       } else if( JSON_STRING == snode->type ) {
+                                               // We expect a comma-separated list of sort fields.
+                                               const char* str = jsonObjectGetString( snode );
+                                               if( strchr( str, ';' )) {
+                                                       // No semicolons allowed.  It is theoretically possible for a
+                                                       // legitimate semicolon to occur within quotes, but it's not likely
+                                                       // to occur in practice in the context of an ORDER BY list.
+                                                       osrfLogError( OSRF_LOG_MARK, "%s: Possible attempt at SOL injection -- "
+                                                               "semicolon found in ORDER BY list: \"%s\"", modulename, str );
+                                                       if( ctx ) {
+                                                               osrfAppSessionStatus(
+                                                                       ctx->session,
+                                                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                                                       "osrfMethodException",
+                                                                       ctx->request,
+                                                                       "Possible attempt at SOL injection -- "
+                                                                               "semicolon found in ORDER BY list"
+                                                               );
+                                                       }
+                                                       jsonIteratorFree( class_itr );
+                                                       buffer_free( order_buf );
+                                                       buffer_free( sql_buf );
+                                                       if( defaultselhash )
+                                                               jsonObjectFree( defaultselhash );
+                                                       clear_query_stack();
+                                                       return NULL;
+                                               }
+                                               buffer_add( order_buf, str );
+                                               break;
+                                       }
+
+                               } // end while; looping over order_by classes
+
+                               jsonIteratorFree( class_itr );
+                               order_by_list = buffer_release( order_buf );
+
+                       } else {
+                               osrfLogWarning( OSRF_LOG_MARK,
+                                       "\"order_by\" object in a query is not a JSON_HASH or JSON_ARRAY;"
+                                       "no ORDER BY generated" );
+                       }
+
+                       if( order_by_list && *order_by_list ) {
+                               OSRF_BUFFER_ADD( sql_buf, " ORDER BY " );
+                               OSRF_BUFFER_ADD( sql_buf, order_by_list );
+                       }
+
+                       free( order_by_list );
+               }
+
+               const jsonObject* limit = jsonObjectGetKeyConst( rest_of_query, "limit" );
+               if( limit ) {
+                       const char* str = jsonObjectGetString( limit );
+                       if (str) {
+                               buffer_fadd(
+                                       sql_buf,
+                                       " LIMIT %d",
+                                       atoi(str)
+                               );
+                       }
+               }
+
+               const jsonObject* offset = jsonObjectGetKeyConst( rest_of_query, "offset" );
+               if( offset ) {
+                       const char* str = jsonObjectGetString( offset );
+                       if (str) {
+                               buffer_fadd(
+                                       sql_buf,
+                                       " OFFSET %d",
+                                       atoi( str )
+                               );
+                       }
+               }
+       }
+
+       if( defaultselhash )
+               jsonObjectFree( defaultselhash );
+       clear_query_stack();
+
+       OSRF_BUFFER_ADD_CHAR( sql_buf, ';' );
+       return buffer_release( sql_buf );
+}
+
+int doJSONSearch ( osrfMethodContext* ctx ) {
+       if(osrfMethodVerifyContext( ctx )) {
+               osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
+               return -1;
+       }
+
+       osrfLogDebug( OSRF_LOG_MARK, "Received query request" );
+
+       int err = 0;
+
+       jsonObject* hash = jsonObjectGetIndex( ctx->params, 0 );
+
+       int flags = 0;
+
+       if( obj_is_true( jsonObjectGetKeyConst( hash, "distinct" )))
+               flags |= SELECT_DISTINCT;
+
+       if( obj_is_true( jsonObjectGetKeyConst( hash, "no_i18n" )))
+               flags |= DISABLE_I18N;
+
+       osrfLogDebug( OSRF_LOG_MARK, "Building SQL ..." );
+       clear_query_stack();       // a possibly needless precaution
+       char* sql = buildQuery( ctx, hash, flags );
+       clear_query_stack();
+
+       if( !sql ) {
+               err = -1;
+               return err;
+       }
+
+       osrfLogDebug( OSRF_LOG_MARK, "%s SQL =  %s", modulename, sql );
+
+       // XXX for now...
+       dbhandle = writehandle;
+
+       dbi_result result = dbi_conn_query( dbhandle, sql );
+
+       if( result ) {
+               osrfLogDebug( OSRF_LOG_MARK, "Query returned with no errors" );
+
+               if( dbi_result_first_row( result )) {
+                       /* JSONify the result */
+                       osrfLogDebug( OSRF_LOG_MARK, "Query returned at least one row" );
+
+                       do {
+                               jsonObject* return_val = oilsMakeJSONFromResult( result );
+                               osrfAppRespond( ctx, return_val );
+                               jsonObjectFree( return_val );
+                       } while( dbi_result_next_row( result ));
+
+               } else {
+                       osrfLogDebug( OSRF_LOG_MARK, "%s returned no results for query %s", modulename, sql );
+               }
+
+               osrfAppRespondComplete( ctx, NULL );
+
+               /* clean up the query */
+               dbi_result_free( result );
+
+       } else {
+               err = -1;
+               const char* msg;
+               int errnum = dbi_conn_error( dbhandle, &msg );
+               osrfLogError( OSRF_LOG_MARK, "%s: Error with query [%s]: %d %s",
+                       modulename, sql, errnum, msg ? msg : "(No description available)" );
+               osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_INTERNALSERVERERROR,
+                       "osrfMethodException",
+                       ctx->request,
+                       "Severe query error -- see error log for more details"
+               );
+               if( !oilsIsDBConnected( dbhandle ))
+                       osrfAppSessionPanic( ctx->session );
+       }
+
+       free( sql );
+       return err;
+}
+
+// The last parameter, err, is used to report an error condition by updating an int owned by
+// the calling code.
+
+// In case of an error, we set *err to -1.  If there is no error, *err is left unchanged.
+// It is the responsibility of the calling code to initialize *err before the
+// call, so that it will be able to make sense of the result.
+
+// Note also that we return NULL if and only if we set *err to -1.  So the err parameter is
+// redundant anyway.
+static jsonObject* doFieldmapperSearch( osrfMethodContext* ctx, osrfHash* class_meta,
+               jsonObject* where_hash, jsonObject* query_hash, int* err ) {
+
+       // XXX for now...
+       dbhandle = writehandle;
+
+       char* core_class = osrfHashGet( class_meta, "classname" );
+       osrfLogDebug( OSRF_LOG_MARK, "entering doFieldmapperSearch() with core_class %s", core_class );
+
+       char* pkey = osrfHashGet( class_meta, "primarykey" );
+
+       if (!ctx->session->userData)
+               (void) initSessionCache( ctx );
+
+       char *methodtype = osrfHashGet( (osrfHash *) ctx->method->userData, "methodtype" );
+       char *inside_verify = osrfHashGet( (osrfHash*) ctx->session->userData, "inside_verify" );
+       int need_to_verify = (inside_verify ? !atoi(inside_verify) : 1);
+
+       int i_respond_directly = 0;
+       int flesh_depth = 0;
+
+       char* sql = buildSELECT( where_hash, query_hash, class_meta, ctx );
+       if( !sql ) {
+               osrfLogDebug( OSRF_LOG_MARK, "Problem building query, returning NULL" );
+               *err = -1;
+               return NULL;
+       }
+
+       osrfLogDebug( OSRF_LOG_MARK, "%s SQL =  %s", modulename, sql );
+
+       dbi_result result = dbi_conn_query( dbhandle, sql );
+       if( NULL == result ) {
+               const char* msg;
+               int errnum = dbi_conn_error( dbhandle, &msg );
+               osrfLogError(OSRF_LOG_MARK, "%s: Error retrieving %s with query [%s]: %d %s",
+                       modulename, osrfHashGet( class_meta, "fieldmapper" ), sql, errnum,
+                       msg ? msg : "(No description available)" );
+               if( !oilsIsDBConnected( dbhandle ))
+                       osrfAppSessionPanic( ctx->session );
+               osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_INTERNALSERVERERROR,
+                       "osrfMethodException",
+                       ctx->request,
+                       "Severe query error -- see error log for more details"
+               );
+               *err = -1;
+               free( sql );
+               return NULL;
+
+       } else {
+               osrfLogDebug( OSRF_LOG_MARK, "Query returned with no errors" );
+       }
+
+       jsonObject* res_list = jsonNewObjectType( JSON_ARRAY );
+       jsonObject* row_obj = NULL;
+
+       // The following two steps are for verifyObjectPCRUD()'s benefit.
+       // 1. get the flesh depth
+       const jsonObject* _tmp = jsonObjectGetKeyConst( query_hash, "flesh" );
+       if( _tmp ) {
+               flesh_depth = (int) jsonObjectGetNumber( _tmp );
+               if( flesh_depth == -1 || flesh_depth > max_flesh_depth )
+                       flesh_depth = max_flesh_depth;
+       }
+
+       // 2. figure out one consistent rs_size for verifyObjectPCRUD to use
+       // over the whole life of this request.  This means if we've already set
+       // up a rs_size_req_%d, do nothing.
+       //      a. Incidentally, we can also use this opportunity to set i_respond_directly
+       int *rs_size = osrfHashGetFmt( (osrfHash *) ctx->session->userData, "rs_size_req_%d", ctx->request );
+       if( !rs_size ) {        // pointer null, so value not set in hash
+               // i_respond_directly can only be true at the /top/ of a recursive search, if even that.
+               i_respond_directly = ( *methodtype == 'r' || *methodtype == 'i' || *methodtype == 's' );
+
+               rs_size = (int *) safe_malloc( sizeof(int) );   // will be freed by sessionDataFree()
+               unsigned long long result_count = dbi_result_get_numrows( result );
+               *rs_size = (int) result_count * (flesh_depth + 1);      // yes, we could lose some bits, but come on
+               osrfHashSet( (osrfHash *) ctx->session->userData, rs_size, "rs_size_req_%d", ctx->request );
+       }
+
+       if( dbi_result_first_row( result )) {
+
+               // Convert each row to a JSON_ARRAY of column values, and enclose those objects
+               // in a JSON_ARRAY of rows.  If two or more rows have the same key value, then
+               // eliminate the duplicates.
+               osrfLogDebug( OSRF_LOG_MARK, "Query returned at least one row" );
+               osrfHash* dedup = osrfNewHash();
+               do {
+                       row_obj = oilsMakeFieldmapperFromResult( result, class_meta );
+                       char* pkey_val = oilsFMGetString( row_obj, pkey );
+                       if( osrfHashGet( dedup, pkey_val ) ) {
+                               jsonObjectFree( row_obj );
+                               free( pkey_val );
+                       } else {
+                               if( !enforce_pcrud || !need_to_verify ||
+                                               verifyObjectPCRUD( ctx, class_meta, row_obj, 0 /* means check user data for rs_size */ )) {
+                                       osrfHashSet( dedup, pkey_val, pkey_val );
+                                       jsonObjectPush( res_list, row_obj );
+                               }
+                       }
+               } while( dbi_result_next_row( result ));
+               osrfHashFree( dedup );
+
+       } else {
+               osrfLogDebug( OSRF_LOG_MARK, "%s returned no results for query %s",
+                       modulename, sql );
+       }
+
+       /* clean up the query */
+       dbi_result_free( result );
+       free( sql );
+
+       // If we're asked to flesh, and there's anything to flesh, then flesh it
+       // (formerly we would skip fleshing if in pcrud mode, but now we support
+       // fleshing even in PCRUD).
+       if( res_list->size ) {
+               jsonObject* temp_blob;  // We need a non-zero flesh depth, and a list of fields to flesh
+               jsonObject* flesh_fields; 
+               jsonObject* flesh_blob = NULL;
+               osrfStringArray* link_fields = NULL;
+               osrfHash* links = NULL;
+               int want_flesh = 0;
+
+               if( query_hash ) {
+                       temp_blob = jsonObjectGetKey( query_hash, "flesh_fields" );
+                       if( temp_blob && flesh_depth > 0 ) {
+
+                               flesh_blob = jsonObjectClone( temp_blob );
+                               flesh_fields = jsonObjectGetKey( flesh_blob, core_class );
+
+                               links = osrfHashGet( class_meta, "links" );
+
+                               // Make an osrfStringArray of the names of fields to be fleshed
+                               if( flesh_fields ) {
+                                       if( flesh_fields->size == 1 ) {
+                                               const char* _t = jsonObjectGetString(
+                                                       jsonObjectGetIndex( flesh_fields, 0 ) );
+                                               if( !strcmp( _t, "*" ))
+                                                       link_fields = osrfHashKeys( links );
+                                       }
+
+                                       if( !link_fields ) {
+                                               jsonObject* _f;
+                                               link_fields = osrfNewStringArray( 1 );
+                                               jsonIterator* _i = jsonNewIterator( flesh_fields );
+                                               while ((_f = jsonIteratorNext( _i ))) {
+                                                       osrfStringArrayAdd( link_fields, jsonObjectGetString( _f ) );
+                                               }
+                                               jsonIteratorFree( _i );
+                                       }
+                               }
+                               want_flesh = link_fields ? 1 : 0;
+                       }
+               }
+
+               osrfHash* fields = osrfHashGet( class_meta, "fields" );
+
+               // Iterate over the JSON_ARRAY of rows
+               jsonObject* cur;
+               unsigned long res_idx = 0;
+               while((cur = jsonObjectGetIndex( res_list, res_idx++ ) )) {
+
+                       int i = 0;
+                       const char* link_field;
+
+                       // Iterate over the list of fleshable fields
+                       if ( want_flesh ) {
+                               while( (link_field = osrfStringArrayGetString(link_fields, i++)) ) {
+
+                                       osrfLogDebug( OSRF_LOG_MARK, "Starting to flesh %s", link_field );
+
+                                       osrfHash* kid_link = osrfHashGet( links, link_field );
+                                       if( !kid_link )
+                                               continue;     // Not a link field; skip it
+
+                                       osrfHash* field = osrfHashGet( fields, link_field );
+                                       if( !field )
+                                               continue;     // Not a field at all; skip it (IDL is ill-formed)
+
+                                       osrfHash* kid_idl = osrfHashGet( oilsIDL(),
+                                               osrfHashGet( kid_link, "class" ));
+                                       if( !kid_idl )
+                                               continue;   // The class it links to doesn't exist; skip it
+
+                                       const char* reltype = osrfHashGet( kid_link, "reltype" );
+                                       if( !reltype )
+                                               continue;   // No reltype; skip it (IDL is ill-formed)
+
+                                       osrfHash* value_field = field;
+
+                                       if(    !strcmp( reltype, "has_many" )
+                                               || !strcmp( reltype, "might_have" ) ) { // has_many or might_have
+                                               value_field = osrfHashGet(
+                                                       fields, osrfHashGet( class_meta, "primarykey" ) );
+                                       }
+
+                                       int kid_has_controller = osrfStringArrayContains( osrfHashGet(kid_idl, "controller"), modulename );
+                                       // fleshing pcrud case: we require the controller in need_to_verify mode
+                                       if ( !kid_has_controller && enforce_pcrud && need_to_verify ) {
+                                               osrfLogInfo( OSRF_LOG_MARK, "%s is not listed as a controller for %s; moving on", modulename, core_class );
+
+                                               jsonObjectSetIndex(
+                                                       cur,
+                                                       (unsigned long) atoi( osrfHashGet(field, "array_position") ),
+                                                       jsonNewObjectType(
+                                                               !strcmp( reltype, "has_many" ) ? JSON_ARRAY : JSON_NULL
+                                                       )
+                                               );
+                                               continue;
+                                       }
+
+                                       osrfStringArray* link_map = osrfHashGet( kid_link, "map" );
+
+                                       if( link_map->size > 0 ) {
+                                               jsonObject* _kid_key = jsonNewObjectType( JSON_ARRAY );
+                                               jsonObjectPush(
+                                                       _kid_key,
+                                                       jsonNewObject( osrfStringArrayGetString( link_map, 0 ) )
+                                               );
+
+                                               jsonObjectSetKey(
+                                                       flesh_blob,
+                                                       osrfHashGet( kid_link, "class" ),
+                                                       _kid_key
+                                               );
+                                       };
+
+                                       osrfLogDebug(
+                                               OSRF_LOG_MARK,
+                                               "Link field: %s, remote class: %s, fkey: %s, reltype: %s",
+                                               osrfHashGet( kid_link, "field" ),
+                                               osrfHashGet( kid_link, "class" ),
+                                               osrfHashGet( kid_link, "key" ),
+                                               osrfHashGet( kid_link, "reltype" )
+                                       );
+
+                                       const char* search_key = jsonObjectGetString(
+                                               jsonObjectGetIndex( cur,
+                                                       atoi( osrfHashGet( value_field, "array_position" ) )
+                                               )
+                                       );
+
+                                       if( !search_key ) {
+                                               osrfLogDebug( OSRF_LOG_MARK, "Nothing to search for!" );
+                                               continue;
+                                       }
+
+                                       osrfLogDebug( OSRF_LOG_MARK, "Creating param objects..." );
+
+                                       // construct WHERE clause
+                                       jsonObject* where_clause  = jsonNewObjectType( JSON_HASH );
+                                       jsonObjectSetKey(
+                                               where_clause,
+                                               osrfHashGet( kid_link, "key" ),
+                                               jsonNewObject( search_key )
+                                       );
+
+                                       // construct the rest of the query, mostly
+                                       // by copying pieces of the previous level of query
+                                       jsonObject* rest_of_query = jsonNewObjectType( JSON_HASH );
+                                       jsonObjectSetKey( rest_of_query, "flesh",
+                                               jsonNewNumberObject( flesh_depth - 1 + link_map->size )
+                                       );
+
+                                       if( flesh_blob )
+                                               jsonObjectSetKey( rest_of_query, "flesh_fields",
+                                                       jsonObjectClone( flesh_blob ));
+
+                                       if( jsonObjectGetKeyConst( query_hash, "order_by" )) {
+                                               jsonObjectSetKey( rest_of_query, "order_by",
+                                                       jsonObjectClone( jsonObjectGetKeyConst( query_hash, "order_by" ))
+                                               );
+                                       }
+
+                                       if( jsonObjectGetKeyConst( query_hash, "select" )) {
+                                               jsonObjectSetKey( rest_of_query, "select",
+                                                       jsonObjectClone( jsonObjectGetKeyConst( query_hash, "select" ))
+                                               );
+                                       }
+
+                                       // do the query, recursively, to expand the fleshable field
+                                       jsonObject* kids = doFieldmapperSearch( ctx, kid_idl,
+                                               where_clause, rest_of_query, err );
+
+                                       jsonObjectFree( where_clause );
+                                       jsonObjectFree( rest_of_query );
+
+                                       if( *err ) {
+                                               osrfStringArrayFree( link_fields );
+                                               jsonObjectFree( res_list );
+                                               jsonObjectFree( flesh_blob );
+                                               return NULL;
+                                       }
+
+                                       osrfLogDebug( OSRF_LOG_MARK, "Search for %s return %d linked objects",
+                                               osrfHashGet( kid_link, "class" ), kids->size );
+
+                                       // Traverse the result set
+                                       jsonObject* X = NULL;
+                                       if( link_map->size > 0 && kids->size > 0 ) {
+                                               X = kids;
+                                               kids = jsonNewObjectType( JSON_ARRAY );
+
+                                               jsonObject* _k_node;
+                                               unsigned long res_idx = 0;
+                                               while((_k_node = jsonObjectGetIndex( X, res_idx++ ) )) {
+                                                       jsonObjectPush(
+                                                               kids,
+                                                               jsonObjectClone(
+                                                                       jsonObjectGetIndex(
+                                                                               _k_node,
+                                                                               (unsigned long) atoi(
+                                                                                       osrfHashGet(
+                                                                                               osrfHashGet(
+                                                                                                       osrfHashGet(
+                                                                                                               osrfHashGet(
+                                                                                                                       oilsIDL(),
+                                                                                                                       osrfHashGet( kid_link, "class" )
+                                                                                                               ),
+                                                                                                               "fields"
+                                                                                                       ),
+                                                                                                       osrfStringArrayGetString( link_map, 0 )
+                                                                                               ),
+                                                                                               "array_position"
+                                                                                       )
+                                                                               )
+                                                                       )
+                                                               )
+                                                       );
+                                               } // end while loop traversing X
+                                       }
+
+                                       if (kids->size > 0) {
+
+                                               if((   !strcmp( osrfHashGet( kid_link, "reltype" ), "has_a" )
+                                                       || !strcmp( osrfHashGet( kid_link, "reltype" ), "might_have" ))
+                                               ) {
+                                                       osrfLogDebug(OSRF_LOG_MARK, "Storing fleshed objects in %s",
+                                                               osrfHashGet( kid_link, "field" ));
+                                                       jsonObjectSetIndex(
+                                                               cur,
+                                                               (unsigned long) atoi( osrfHashGet( field, "array_position" ) ),
+                                                               jsonObjectClone( jsonObjectGetIndex( kids, 0 ))
+                                                       );
+                                               }
+                                       }
+
+                                       if( !strcmp( osrfHashGet( kid_link, "reltype" ), "has_many" )) {
+                                               // has_many
+                                               osrfLogDebug( OSRF_LOG_MARK, "Storing fleshed objects in %s",
+                                                       osrfHashGet( kid_link, "field" ) );
+                                               jsonObjectSetIndex(
+                                                       cur,
+                                                       (unsigned long) atoi( osrfHashGet( field, "array_position" ) ),
+                                                       jsonObjectClone( kids )
+                                               );
+                                       }
+
+                                       if( X ) {
+                                               jsonObjectFree( kids );
+                                               kids = X;
+                                       }
+
+                                       jsonObjectFree( kids );
+
+                                       osrfLogDebug( OSRF_LOG_MARK, "Fleshing of %s complete",
+                                               osrfHashGet( kid_link, "field" ) );
+                                       osrfLogDebug( OSRF_LOG_MARK, "%s", jsonObjectToJSON( cur ));
+
+                               } // end while loop traversing list of fleshable fields
+                       }
+
+                       if( i_respond_directly ) {
+                               if ( *methodtype == 'i' ) {
+                                       osrfAppRespond( ctx,
+                                               oilsFMGetObject( cur, osrfHashGet( class_meta, "primarykey" ) ) );
+                               } else {
+                                       osrfAppRespond( ctx, cur );
+                               }
+                       }
+               } // end while loop traversing res_list
+               jsonObjectFree( flesh_blob );
+               osrfStringArrayFree( link_fields );
+       }
+
+       if( i_respond_directly ) {
+               jsonObjectFree( res_list );
+               return jsonNewObjectType( JSON_ARRAY );
+       } else {
+               return res_list;
+       }
+}
+
+
+int doUpdate( osrfMethodContext* ctx ) {
+       if( osrfMethodVerifyContext( ctx )) {
+               osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+               return -1;
+       }
+
+       if( enforce_pcrud )
+               timeout_needs_resetting = 1;
+
+       osrfHash* meta = osrfHashGet( (osrfHash*) ctx->method->userData, "class" );
+
+       jsonObject* target = NULL;
+       if( enforce_pcrud )
+               target = jsonObjectGetIndex( ctx->params, 1 );
+       else
+               target = jsonObjectGetIndex( ctx->params, 0 );
+
+       if(!verifyObjectClass( ctx, target )) {
+               osrfAppRespondComplete( ctx, NULL );
+               return -1;
+       }
+
+       if( getXactId( ctx ) == NULL ) {
+               osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_BADREQUEST,
+                       "osrfMethodException",
+                       ctx->request,
+                       "No active transaction -- required for UPDATE"
+               );
+               osrfAppRespondComplete( ctx, NULL );
+               return -1;
+       }
+
+       // The following test is harmless but redundant.  If a class is
+       // readonly, we don't register an update method for it.
+       if( str_is_true( osrfHashGet( meta, "readonly" ) ) ) {
+               osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_BADREQUEST,
+                       "osrfMethodException",
+                       ctx->request,
+                       "Cannot UPDATE readonly class"
+               );
+               osrfAppRespondComplete( ctx, NULL );
+               return -1;
+       }
+
+       const char* trans_id = getXactId( ctx );
+
+       // Set the last_xact_id
+       int index = oilsIDL_ntop( target->classname, "last_xact_id" );
+       if( index > -1 ) {
+               osrfLogDebug( OSRF_LOG_MARK, "Setting last_xact_id to %s on %s at position %d",
+                               trans_id, target->classname, index );
+               jsonObjectSetIndex( target, index, jsonNewObject( trans_id ));
+       }
+
+       char* pkey = osrfHashGet( meta, "primarykey" );
+       osrfHash* fields = osrfHashGet( meta, "fields" );
+
+       char* id = oilsFMGetString( target, pkey );
+
+       osrfLogDebug(
+               OSRF_LOG_MARK,
+               "%s updating %s object with %s = %s",
+               modulename,
+               osrfHashGet( meta, "fieldmapper" ),
+               pkey,
+               id
+       );
+
+       dbhandle = writehandle;
+       growing_buffer* sql = buffer_init( 128 );
+       buffer_fadd( sql,"UPDATE %s SET", osrfHashGet( meta, "tablename" ));
+
+       int first = 1;
+       osrfHash* field_def = NULL;
+       osrfHashIterator* field_itr = osrfNewHashIterator( fields );
+       while( ( field_def = osrfHashIteratorNext( field_itr ) ) ) {
+
+               // Skip virtual fields, and the primary key
+               if( str_is_true( osrfHashGet( field_def, "virtual") ) )
+                       continue;
+
+               if (osrfStringArrayContains( osrfHashGet(field_def, "suppress_controller"), modulename ))
+                       continue;
+
+
+               const char* field_name = osrfHashIteratorKey( field_itr );
+               if( ! strcmp( field_name, pkey ) )
+                       continue;
+
+               const jsonObject* field_object = oilsFMGetObject( target, field_name );
+
+               int value_is_numeric = 0;    // boolean
+               char* value;
+               if( field_object && field_object->classname ) {
+                       value = oilsFMGetString(
+                               field_object,
+                               (char*) oilsIDLFindPath( "/%s/primarykey", field_object->classname )
+                       );
+               } else if( field_object && JSON_BOOL == field_object->type ) {
+                       if( jsonBoolIsTrue( field_object ) )
+                               value = strdup( "t" );
+                       else
+                               value = strdup( "f" );
+               } else {
+                       value = jsonObjectToSimpleString( field_object );
+                       if( field_object && JSON_NUMBER == field_object->type )
+                               value_is_numeric = 1;
+               }
+
+               osrfLogDebug( OSRF_LOG_MARK, "Updating %s object with %s = %s",
+                               osrfHashGet( meta, "fieldmapper" ), field_name, value);
+
+               if( !field_object || field_object->type == JSON_NULL ) {
+                       if( !( !( strcmp( osrfHashGet( meta, "classname" ), "au" ) )
+                                       && !( strcmp( field_name, "passwd" ) )) ) { // arg at the special case!
+                               if( first )
+                                       first = 0;
+                               else
+                                       OSRF_BUFFER_ADD_CHAR( sql, ',' );
+                               buffer_fadd( sql, " %s = NULL", field_name );
+                       }
+
+               } else if( value_is_numeric || !strcmp( get_primitive( field_def ), "number") ) {
+                       if( first )
+                               first = 0;
+                       else
+                               OSRF_BUFFER_ADD_CHAR( sql, ',' );
+
+                       const char* numtype = get_datatype( field_def );
+                       if( !strncmp( numtype, "INT", 3 ) ) {
+                               buffer_fadd( sql, " %s = %ld", field_name, atol( value ) );
+                       } else if( !strcmp( numtype, "NUMERIC" ) ) {
+                               buffer_fadd( sql, " %s = %f", field_name, atof( value ) );
+                       } else {
+                               // Must really be intended as a string, so quote it
+                               if( dbi_conn_quote_string( dbhandle, &value )) {
+                                       buffer_fadd( sql, " %s = %s", field_name, value );
+                               } else {
+                                       osrfLogError( OSRF_LOG_MARK, "%s: Error quoting string [%s]",
+                                               modulename, value );
+                                       osrfAppSessionStatus(
+                                               ctx->session,
+                                               OSRF_STATUS_INTERNALSERVERERROR,
+                                               "osrfMethodException",
+                                               ctx->request,
+                                               "Error quoting string -- please see the error log for more details"
+                                       );
+                                       free( value );
+                                       free( id );
+                                       osrfHashIteratorFree( field_itr );
+                                       buffer_free( sql );
+                                       osrfAppRespondComplete( ctx, NULL );
+                                       return -1;
+                               }
+                       }
+
+                       osrfLogDebug( OSRF_LOG_MARK, "%s is of type %s", field_name, numtype );
+
+               } else {
+                       if( dbi_conn_quote_string( dbhandle, &value ) ) {
+                               if( first )
+                                       first = 0;
+                               else
+                                       OSRF_BUFFER_ADD_CHAR( sql, ',' );
+                               buffer_fadd( sql, " %s = %s", field_name, value );
+                       } else {
+                               osrfLogError( OSRF_LOG_MARK, "%s: Error quoting string [%s]", modulename, value );
+                               osrfAppSessionStatus(
+                                       ctx->session,
+                                       OSRF_STATUS_INTERNALSERVERERROR,
+                                       "osrfMethodException",
+                                       ctx->request,
+                                       "Error quoting string -- please see the error log for more details"
+                               );
+                               free( value );
+                               free( id );
+                               osrfHashIteratorFree( field_itr );
+                               buffer_free( sql );
+                               osrfAppRespondComplete( ctx, NULL );
+                               return -1;
+                       }
+               }
+
+               free( value );
+
+       } // end while
+
+       osrfHashIteratorFree( field_itr );
+
+       jsonObject* obj = jsonNewObject( id );
+
+       if( strcmp( get_primitive( osrfHashGet( osrfHashGet(meta, "fields"), pkey )), "number" ))
+               dbi_conn_quote_string( dbhandle, &id );
+
+       buffer_fadd( sql, " WHERE %s = %s;", pkey, id );
+
+       char* query = buffer_release( sql );
+       osrfLogDebug( OSRF_LOG_MARK, "%s: Update SQL [%s]", modulename, query );
+
+       dbi_result result = dbi_conn_query( dbhandle, query );
+       free( query );
+
+       int rc = 0;
+       if( !result ) {
+               jsonObjectFree( obj );
+               obj = jsonNewObject( NULL );
+               const char* msg;
+               int errnum = dbi_conn_error( dbhandle, &msg );
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s ERROR updating %s object with %s = %s: %d %s",
+                       modulename,
+                       osrfHashGet( meta, "fieldmapper" ),
+                       pkey,
+                       id,
+                       errnum,
+                       msg ? msg : "(No description available)"
+               );
+               osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_INTERNALSERVERERROR,
+                       "osrfMethodException",
+                       ctx->request,
+                       "Error in updating a row -- please see the error log for more details"
+               );
+               if( !oilsIsDBConnected( dbhandle ))
+                       osrfAppSessionPanic( ctx->session );
+               rc = -1;
+       } else
+               dbi_result_free( result );
+
+       free( id );
+       osrfAppRespondComplete( ctx, obj );
+       jsonObjectFree( obj );
+       return rc;
+}
+
+int doDelete( osrfMethodContext* ctx ) {
+       if( osrfMethodVerifyContext( ctx )) {
+               osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+               return -1;
+       }
+
+       if( enforce_pcrud )
+               timeout_needs_resetting = 1;
+
+       osrfHash* meta = osrfHashGet( (osrfHash*) ctx->method->userData, "class" );
+
+       if( getXactId( ctx ) == NULL ) {
+               osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_BADREQUEST,
+                       "osrfMethodException",
+                       ctx->request,
+                       "No active transaction -- required for DELETE"
+               );
+               osrfAppRespondComplete( ctx, NULL );
+               return -1;
+       }
+
+       // The following test is harmless but redundant.  If a class is
+       // readonly, we don't register a delete method for it.
+       if( str_is_true( osrfHashGet( meta, "readonly" ) ) ) {
+               osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_BADREQUEST,
+                       "osrfMethodException",
+                       ctx->request,
+                       "Cannot DELETE readonly class"
+               );
+               osrfAppRespondComplete( ctx, NULL );
+               return -1;
+       }
+
+       dbhandle = writehandle;
+
+       char* pkey = osrfHashGet( meta, "primarykey" );
+
+       int _obj_pos = 0;
+       if( enforce_pcrud )
+               _obj_pos = 1;
+
+       char* id;
+       if( jsonObjectGetIndex( ctx->params, _obj_pos )->classname ) {
+               if( !verifyObjectClass( ctx, jsonObjectGetIndex( ctx->params, _obj_pos ))) {
+                       osrfAppRespondComplete( ctx, NULL );
+                       return -1;
+               }
+
+               id = oilsFMGetString( jsonObjectGetIndex(ctx->params, _obj_pos), pkey );
+       } else {
+               if( enforce_pcrud && !verifyObjectPCRUD( ctx, meta, NULL, 1 )) {
+                       osrfAppRespondComplete( ctx, NULL );
+                       return -1;
+               }
+               id = jsonObjectToSimpleString( jsonObjectGetIndex( ctx->params, _obj_pos ));
+       }
+
+       osrfLogDebug(
+               OSRF_LOG_MARK,
+               "%s deleting %s object with %s = %s",
+               modulename,
+               osrfHashGet( meta, "fieldmapper" ),
+               pkey,
+               id
+       );
+
+       jsonObject* obj = jsonNewObject( id );
+
+       if( strcmp( get_primitive( osrfHashGet( osrfHashGet(meta, "fields"), pkey ) ), "number" ) )
+               dbi_conn_quote_string( writehandle, &id );
+
+       dbi_result result = dbi_conn_queryf( writehandle, "DELETE FROM %s WHERE %s = %s;",
+               osrfHashGet( meta, "tablename" ), pkey, id );
+
+       int rc = 0;
+       if( !result ) {
+               rc = -1;
+               jsonObjectFree( obj );
+               obj = jsonNewObject( NULL );
+               const char* msg;
+               int errnum = dbi_conn_error( writehandle, &msg );
+               osrfLogError(
+                       OSRF_LOG_MARK,
+                       "%s ERROR deleting %s object with %s = %s: %d %s",
+                       modulename,
+                       osrfHashGet( meta, "fieldmapper" ),
+                       pkey,
+                       id,
+                       errnum,
+                       msg ? msg : "(No description available)"
+               );
+               osrfAppSessionStatus(
+                       ctx->session,
+                       OSRF_STATUS_INTERNALSERVERERROR,
+                       "osrfMethodException",
+                       ctx->request,
+                       "Error in deleting a row -- please see the error log for more details"
+               );
+               if( !oilsIsDBConnected( writehandle ))
+                       osrfAppSessionPanic( ctx->session );
+       } else
+               dbi_result_free( result );
+
+       free( id );
+
+       osrfAppRespondComplete( ctx, obj );
+       jsonObjectFree( obj );
+       return rc;
+}
+
+/**
+       @brief Translate a row returned from the database into a jsonObject of type JSON_ARRAY.
+       @param result An iterator for a result set; we only look at the current row.
+       @param @meta Pointer to the class metadata for the core class.
+       @return Pointer to the resulting jsonObject if successful; otherwise NULL.
+
+       If a column is not defined in the IDL, or if it has no array_position defined for it in
+       the IDL, or if it is defined as virtual, ignore it.
+
+       Otherwise, translate the column value into a jsonObject of type JSON_NULL, JSON_NUMBER,
+       or JSON_STRING.  Then insert this jsonObject into the JSON_ARRAY according to its
+       array_position in the IDL.
+
+       A field defined in the IDL but not represented in the returned row will leave a hole
+       in the JSON_ARRAY.  In effect it will be treated as a null value.
+
+       In the resulting JSON_ARRAY, the field values appear in the sequence defined by the IDL,
+       regardless of their sequence in the SELECT statement.  The JSON_ARRAY is assigned the
+       classname corresponding to the @a meta argument.
+
+       The calling code is responsible for freeing the the resulting jsonObject by calling
+       jsonObjectFree().
+*/
+static jsonObject* oilsMakeFieldmapperFromResult( dbi_result result, osrfHash* meta) {
+       if( !( result && meta )) return NULL;
+
+       jsonObject* object = jsonNewObjectType( JSON_ARRAY );
+       jsonObjectSetClass( object, osrfHashGet( meta, "classname" ));
+       osrfLogInternal( OSRF_LOG_MARK, "Setting object class to %s ", object->classname );
+
+       osrfHash* fields = osrfHashGet( meta, "fields" );
+
+       int columnIndex = 1;
+       const char* columnName;
+
+       /* cycle through the columns in the row returned from the database */
+       while( (columnName = dbi_result_get_field_name( result, columnIndex )) ) {
+
+               osrfLogInternal( OSRF_LOG_MARK, "Looking for column named [%s]...", (char*) columnName );
+
+               int fmIndex = -1;  // Will be set to the IDL's sequence number for this field
+
+               /* determine the field type and storage attributes */
+               unsigned short type = dbi_result_get_field_type_idx( result, columnIndex );
+               int attr            = dbi_result_get_field_attribs_idx( result, columnIndex );
+
+               // Fetch the IDL's sequence number for the field.  If the field isn't in the IDL,
+               // or if it has no sequence number there, or if it's virtual, skip it.
+               osrfHash* _f = osrfHashGet( fields, (char*) columnName );
+               if( _f ) {
+
+                       if( str_is_true( osrfHashGet( _f, "virtual" )))
+                               continue;   // skip this column: IDL says it's virtual
+
+                       const char* pos = (char*) osrfHashGet( _f, "array_position" );
+                       if( !pos )      // IDL has no sequence number for it.  This shouldn't happen,
+                               continue;    // since we assign sequence numbers dynamically as we load the IDL.
+
+                       fmIndex = atoi( pos );
+                       osrfLogInternal( OSRF_LOG_MARK, "... Found column at position [%s]...", pos );
+               } else {
+                       continue;     // This field is not defined in the IDL
+               }
+
+               // Stuff the column value into a slot in the JSON_ARRAY, indexed according to the
+               // sequence number from the IDL (which is likely to be different from the sequence
+               // of columns in the SELECT clause).
+               if( dbi_result_field_is_null_idx( result, columnIndex )) {
+                       jsonObjectSetIndex( object, fmIndex, jsonNewObject( NULL ));
+               } else {
+
+                       switch( type ) {
+
+                               case DBI_TYPE_INTEGER :
+
+                                       if( attr & DBI_INTEGER_SIZE8 )
+                                               jsonObjectSetIndex( object, fmIndex,
+                                                       jsonNewNumberObject(
+                                                               dbi_result_get_longlong_idx( result, columnIndex )));
+                                       else
+                                               jsonObjectSetIndex( object, fmIndex,
+                                                       jsonNewNumberObject( dbi_result_get_int_idx( result, columnIndex )));
+
+                                       break;
+
+                               case DBI_TYPE_DECIMAL :
+                                       jsonObjectSetIndex( object, fmIndex,
+                                                       jsonNewNumberObject( dbi_result_get_double_idx(result, columnIndex )));
+                                       break;
+
+                               case DBI_TYPE_STRING :
+
+                                       jsonObjectSetIndex(
+                                               object,
+                                               fmIndex,
+                                               jsonNewObject( dbi_result_get_string_idx( result, columnIndex ))
+                                       );
+
+                                       break;
+
+                               case DBI_TYPE_DATETIME : {
+
+                                       char dt_string[ 256 ] = "";
+                                       struct tm gmdt;
+
+                                       // Fetch the date column as a time_t
+                                       time_t _tmp_dt = dbi_result_get_datetime_idx( result, columnIndex );
+
+                                       // Translate the time_t to a human-readable string
+                                       if( !( attr & DBI_DATETIME_DATE )) {
+                                               gmtime_r( &_tmp_dt, &gmdt );
+                                               strftime( dt_string, sizeof( dt_string ), "%T", &gmdt );
+                                       } else if( !( attr & DBI_DATETIME_TIME )) {
+                                               localtime_r( &_tmp_dt, &gmdt );
+                                               strftime( dt_string, sizeof( dt_string ), "%04Y-%m-%d", &gmdt );
+                                       } else {
+                                               localtime_r( &_tmp_dt, &gmdt );
+                                               strftime( dt_string, sizeof( dt_string ), "%04Y-%m-%dT%T%z", &gmdt );
+                                       }
+
+                                       jsonObjectSetIndex( object, fmIndex, jsonNewObject( dt_string ));
+
+                                       break;
+                               }
+                               case DBI_TYPE_BINARY :
+                                       osrfLogError( OSRF_LOG_MARK,
+                                               "Can't do binary at column %s : index %d", columnName, columnIndex );
+                       } // End switch
+               }
+               ++columnIndex;
+       } // End while
+
+       return object;
+}
+
+static jsonObject* oilsMakeJSONFromResult( dbi_result result ) {
+       if( !result ) return NULL;
+
+       jsonObject* object = jsonNewObject( NULL );
+
+       time_t _tmp_dt;
+       char dt_string[ 256 ];
+       struct tm gmdt;
+
+       int fmIndex;
+       int columnIndex = 1;
+       int attr;
+       unsigned short type;
+       const char* columnName;
+
+       /* cycle through the column list */
+       while(( columnName = dbi_result_get_field_name( result, columnIndex ))) {
+
+               osrfLogInternal( OSRF_LOG_MARK, "Looking for column named [%s]...", (char*) columnName );
+
+               fmIndex = -1; // reset the position
+
+               /* determine the field type and storage attributes */
+               type = dbi_result_get_field_type_idx( result, columnIndex );
+               attr = dbi_result_get_field_attribs_idx( result, columnIndex );
+
+               if( dbi_result_field_is_null_idx( result, columnIndex )) {
+                       jsonObjectSetKey( object, columnName, jsonNewObject( NULL ));
+               } else {
+
+                       switch( type ) {
+
+                               case DBI_TYPE_INTEGER :
+
+                                       if( attr & DBI_INTEGER_SIZE8 )
+                                               jsonObjectSetKey( object, columnName,
+                                                               jsonNewNumberObject( dbi_result_get_longlong_idx(
+                                                                               result, columnIndex )) );
+                                       else
+                                               jsonObjectSetKey( object, columnName, jsonNewNumberObject(
+                                                               dbi_result_get_int_idx( result, columnIndex )) );
+                                       break;
+
+                               case DBI_TYPE_DECIMAL :
+                                       jsonObjectSetKey( object, columnName, jsonNewNumberObject(
+                                               dbi_result_get_double_idx( result, columnIndex )) );
+                                       break;
+
+                               case DBI_TYPE_STRING :
+                                       jsonObjectSetKey( object, columnName,
+                                               jsonNewObject( dbi_result_get_string_idx( result, columnIndex )));
+                                       break;
+
+                               case DBI_TYPE_DATETIME :
+
+                                       memset( dt_string, '\0', sizeof( dt_string ));
+                                       memset( &gmdt, '\0', sizeof( gmdt ));
+
+                                       _tmp_dt = dbi_result_get_datetime_idx( result, columnIndex );
+
+                                       if( !( attr & DBI_DATETIME_DATE )) {
+                                               gmtime_r( &_tmp_dt, &gmdt );
+                                               strftime( dt_string, sizeof( dt_string ), "%T", &gmdt );
+                                       } else if( !( attr & DBI_DATETIME_TIME )) {
+                                               localtime_r( &_tmp_dt, &gmdt );
+                                               strftime( dt_string, sizeof( dt_string ), "%04Y-%m-%d", &gmdt );
+                                       } else {
+                                               localtime_r( &_tmp_dt, &gmdt );
+                                               strftime( dt_string, sizeof( dt_string ), "%04Y-%m-%dT%T%z", &gmdt );
+                                       }
+
+                                       jsonObjectSetKey( object, columnName, jsonNewObject( dt_string ));
+                                       break;
+
+                               case DBI_TYPE_BINARY :
+                                       osrfLogError( OSRF_LOG_MARK,
+                                               "Can't do binary at column %s : index %d", columnName, columnIndex );
+                       }
+               }
+               ++columnIndex;
+       } // end while loop traversing result
+
+       return object;
+}
+
+// Interpret a string as true or false
+int str_is_true( const char* str ) {
+       if( NULL == str || strcasecmp( str, "true" ) )
+               return 0;
+       else
+               return 1;
+}
+
+// Interpret a jsonObject as true or false
+static int obj_is_true( const jsonObject* obj ) {
+       if( !obj )
+               return 0;
+       else switch( obj->type )
+       {
+               case JSON_BOOL :
+                       if( obj->value.b )
+                               return 1;
+                       else
+                               return 0;
+               case JSON_STRING :
+                       if( strcasecmp( obj->value.s, "true" ) )
+                               return 0;
+                       else
+                               return 1;
+               case JSON_NUMBER :          // Support 1/0 for perl's sake
+                       if( jsonObjectGetNumber( obj ) == 1.0 )
+                               return 1;
+                       else
+                               return 0;
+               default :
+                       return 0;
+       }
+}
+
+// Translate a numeric code into a text string identifying a type of
+// jsonObject.  To be used for building error messages.
+static const char* json_type( int code ) {
+       switch ( code )
+       {
+               case 0 :
+                       return "JSON_HASH";
+               case 1 :
+                       return "JSON_ARRAY";
+               case 2 :
+                       return "JSON_STRING";
+               case 3 :
+                       return "JSON_NUMBER";
+               case 4 :
+                       return "JSON_NULL";
+               case 5 :
+                       return "JSON_BOOL";
+               default :
+                       return "(unrecognized)";
+       }
+}
+
+// Extract the "primitive" attribute from an IDL field definition.
+// If we haven't initialized the app, then we must be running in
+// some kind of testbed.  In that case, default to "string".
+static const char* get_primitive( osrfHash* field ) {
+       const char* s = osrfHashGet( field, "primitive" );
+       if( !s ) {
+               if( child_initialized )
+                       osrfLogError(
+                               OSRF_LOG_MARK,
+                               "%s ERROR No \"datatype\" attribute for field \"%s\"",
+                               modulename,
+                               osrfHashGet( field, "name" )
+                       );
+
+               s = "string";
+       }
+       return s;
+}
+
+// Extract the "datatype" attribute from an IDL field definition.
+// If we haven't initialized the app, then we must be running in
+// some kind of testbed.  In that case, default to to NUMERIC,
+// since we look at the datatype only for numbers.
+static const char* get_datatype( osrfHash* field ) {
+       const char* s = osrfHashGet( field, "datatype" );
+       if( !s ) {
+               if( child_initialized )
+                       osrfLogError(
+                               OSRF_LOG_MARK,
+                               "%s ERROR No \"datatype\" attribute for field \"%s\"",
+                               modulename,
+                               osrfHashGet( field, "name" )
+                       );
+               else
+                       s = "NUMERIC";
+       }
+       return s;
+}
+
+/**
+       @brief Determine whether a string is potentially a valid SQL identifier.
+       @param s The identifier to be tested.
+       @return 1 if the input string is potentially a valid SQL identifier, or 0 if not.
+
+       Purpose: to prevent certain kinds of SQL injection.  To that end we don't necessarily
+       need to follow all the rules exactly, such as requiring that the first character not
+       be a digit.
+
+       We allow leading and trailing white space.  In between, we do not allow punctuation
+       (except for underscores and dollar signs), control characters, or embedded white space.
+
+       More pedantically we should allow quoted identifiers containing arbitrary characters, but
+       for the foreseeable future such quoted identifiers are not likely to be an issue.
+*/
+int is_identifier( const char* s) {
+       if( !s )
+               return 0;
+
+       // Skip leading white space
+       while( isspace( (unsigned char) *s ) )
+               ++s;
+
+       if( !s )
+               return 0;   // Nothing but white space?  Not okay.
+
+       // Check each character until we reach white space or
+       // end-of-string.  Letters, digits, underscores, and
+       // dollar signs are okay. With the exception of periods
+       // (as in schema.identifier), control characters and other
+       // punctuation characters are not okay.  Anything else
+       // is okay -- it could for example be part of a multibyte
+       // UTF8 character such as a letter with diacritical marks,
+       // and those are allowed.
+       do {
+               if( isalnum( (unsigned char) *s )
+                       || '.' == *s
+                       || '_' == *s
+                       || '$' == *s )
+                       ;  // Fine; keep going
+               else if(   ispunct( (unsigned char) *s )
+                               || iscntrl( (unsigned char) *s ) )
+                       return 0;
+                       ++s;
+       } while( *s && ! isspace( (unsigned char) *s ) );
+
+       // If we found any white space in the above loop,
+       // the rest had better be all white space.
+
+       while( isspace( (unsigned char) *s ) )
+               ++s;
+
+       if( *s )
+               return 0;   // White space was embedded within non-white space
+
+       return 1;
+}
+
+/**
+       @brief Determine whether to accept a character string as a comparison operator.
+       @param op The candidate comparison operator.
+       @return 1 if the string is acceptable as a comparison operator, or 0 if not.
+
+       We don't validate the operator for real.  We just make sure that it doesn't contain
+       any semicolons or white space (with special exceptions for a few specific operators).
+       The idea is to block certain kinds of SQL injection.  If it has no semicolons or white
+       space but it's still not a valid operator, then the database will complain.
+
+       Another approach would be to compare the string against a short list of approved operators.
+       We don't do that because we want to allow custom operators like ">100*", which at this
+       writing would be difficult or impossible to express otherwise in a JSON query.
+*/
+int is_good_operator( const char* op ) {
+       if( !op ) return 0;   // Sanity check
+
+       const char* s = op;
+       while( *s ) {
+               if( isspace( (unsigned char) *s ) ) {
+                       // Special exceptions for SIMILAR TO, IS DISTINCT FROM,
+                       // and IS NOT DISTINCT FROM.
+                       if( !strcasecmp( op, "similar to" ) )
+                               return 1;
+                       else if( !strcasecmp( op, "is distinct from" ) )
+                               return 1;
+                       else if( !strcasecmp( op, "is not distinct from" ) )
+                               return 1;
+                       else
+                               return 0;
+               }
+               else if( ';' == *s )
+                       return 0;
+               ++s;
+       }
+       return 1;
+}
+
+/**
+       @name Query Frame Management
+
+       The following machinery supports a stack of query frames for use by SELECT().
+
+       A query frame caches information about one level of a SELECT query.  When we enter
+       a subquery, we push another query frame onto the stack, and pop it off when we leave.
+
+       The query frame stores information about the core class, and about any joined classes
+       in the FROM clause.
+
+       The main purpose is to map table aliases to classes and tables, so that a query can
+       join to the same table more than once.  A secondary goal is to reduce the number of
+       lookups in the IDL by caching the results.
+*/
+/*@{*/
+
+#define STATIC_CLASS_INFO_COUNT 3
+
+static ClassInfo static_class_info[ STATIC_CLASS_INFO_COUNT ];
+
+/**
+       @brief Allocate a ClassInfo as raw memory.
+       @return Pointer to the newly allocated ClassInfo.
+
+       Except for the in_use flag, which is used only by the allocation and deallocation
+       logic, we don't initialize the ClassInfo here.
+*/
+static ClassInfo* allocate_class_info( void ) {
+       // In order to reduce the number of mallocs and frees, we return a static
+       // instance of ClassInfo, if we can find one that we're not already using.
+       // We rely on the fact that the compiler will implicitly initialize the
+       // static instances so that in_use == 0.
+
+       int i;
+       for( i = 0; i < STATIC_CLASS_INFO_COUNT; ++i ) {
+               if( ! static_class_info[ i ].in_use ) {
+                       static_class_info[ i ].in_use = 1;
+                       return static_class_info + i;
+               }
+       }
+
+       // The static ones are all in use.  Malloc one.
+
+       return safe_malloc( sizeof( ClassInfo ) );
+}
+
+/**
+       @brief Free any malloc'd memory owned by a ClassInfo, returning it to a pristine state.
+       @param info Pointer to the ClassInfo to be cleared.
+*/
+static void clear_class_info( ClassInfo* info ) {
+       // Sanity check
+       if( ! info )
+               return;
+
+       // Free any malloc'd strings
+
+       if( info->alias != info->alias_store )
+               free( info->alias );
+
+       if( info->class_name != info->class_name_store )
+               free( info->class_name );
+
+       free( info->source_def );
+
+       info->alias = info->class_name = info->source_def = NULL;
+       info->next = NULL;
+}
+
+/**
+       @brief Free a ClassInfo and everything it owns.
+       @param info Pointer to the ClassInfo to be freed.
+*/
+static void free_class_info( ClassInfo* info ) {
+       // Sanity check
+       if( ! info )
+               return;
+
+       clear_class_info( info );
+
+       // If it's one of the static instances, just mark it as not in use
+
+       int i;
+       for( i = 0; i < STATIC_CLASS_INFO_COUNT; ++i ) {
+               if( info == static_class_info + i ) {
+                       static_class_info[ i ].in_use = 0;
+                       return;
+               }
+       }
+
+       // Otherwise it must have been malloc'd, so free it
+
+       free( info );
+}
+
+/**
+       @brief Populate an already-allocated ClassInfo.
+       @param info Pointer to the ClassInfo to be populated.
+       @param alias Alias for the class.  If it is NULL, or an empty string, use the class
+       name for an alias.
+       @param class Name of the class.
+       @return Zero if successful, or 1 if not.
+
+       Populate the ClassInfo with copies of the alias and class name, and with pointers to
+       the relevant portions of the IDL for the specified class.
+*/
+static int build_class_info( ClassInfo* info, const char* alias, const char* class ) {
+       // Sanity checks
+       if( ! info ){
+               osrfLogError( OSRF_LOG_MARK,
+                                         "%s ERROR: No ClassInfo available to populate", modulename );
+               info->alias = info->class_name = info->source_def = NULL;
+               info->class_def = info->fields = info->links = NULL;
+               return 1;
+       }
+
+       if( ! class ) {
+               osrfLogError( OSRF_LOG_MARK,
+                                         "%s ERROR: No class name provided for lookup", modulename );
+               info->alias = info->class_name = info->source_def = NULL;
+               info->class_def = info->fields = info->links = NULL;
+               return 1;
+       }
+
+       // Alias defaults to class name if not supplied
+       if( ! alias || ! alias[ 0 ] )
+               alias = class;
+
+       // Look up class info in the IDL
+       osrfHash* class_def = osrfHashGet( oilsIDL(), class );
+       if( ! class_def ) {
+               osrfLogError( OSRF_LOG_MARK,
+                                         "%s ERROR: Class %s not defined in IDL", modulename, class );
+               info->alias = info->class_name = info->source_def = NULL;
+               info->class_def = info->fields = info->links = NULL;
+               return 1;
+       } else if( str_is_true( osrfHashGet( class_def, "virtual" ) ) ) {
+               osrfLogError( OSRF_LOG_MARK,
+                                         "%s ERROR: Class %s is defined as virtual", modulename, class );
+               info->alias = info->class_name = info->source_def = NULL;
+               info->class_def = info->fields = info->links = NULL;
+               return 1;
+       }
+
+       osrfHash* links = osrfHashGet( class_def, "links" );
+       if( ! links ) {
+               osrfLogError( OSRF_LOG_MARK,
+                                         "%s ERROR: No links defined in IDL for class %s", modulename, class );
+               info->alias = info->class_name = info->source_def = NULL;
+               info->class_def = info->fields = info->links = NULL;
+               return 1;
+       }
+
+       osrfHash* fields = osrfHashGet( class_def, "fields" );
+       if( ! fields ) {
+               osrfLogError( OSRF_LOG_MARK,
+                                         "%s ERROR: No fields defined in IDL for class %s", modulename, class );
+               info->alias = info->class_name = info->source_def = NULL;
+               info->class_def = info->fields = info->links = NULL;
+               return 1;
+       }
+
+       char* source_def = oilsGetRelation( class_def );
+       if( ! source_def )
+               return 1;
+
+       // We got everything we need, so populate the ClassInfo
+       if( strlen( alias ) > ALIAS_STORE_SIZE )
+               info->alias = strdup( alias );
+       else {
+               strcpy( info->alias_store, alias );
+               info->alias = info->alias_store;
+       }
+
+       if( strlen( class ) > CLASS_NAME_STORE_SIZE )
+               info->class_name = strdup( class );
+       else {
+               strcpy( info->class_name_store, class );
+               info->class_name = info->class_name_store;
+       }
+
+       info->source_def = source_def;
+
+       info->class_def = class_def;
+       info->links     = links;
+       info->fields    = fields;
+
+       return 0;
+}
+
+#define STATIC_FRAME_COUNT 3
+
+static QueryFrame static_frame[ STATIC_FRAME_COUNT ];
+
+/**
+       @brief Allocate a QueryFrame as raw memory.
+       @return Pointer to the newly allocated QueryFrame.
+
+       Except for the in_use flag, which is used only by the allocation and deallocation
+       logic, we don't initialize the QueryFrame here.
+*/
+static QueryFrame* allocate_frame( void ) {
+       // In order to reduce the number of mallocs and frees, we return a static
+       // instance of QueryFrame, if we can find one that we're not already using.
+       // We rely on the fact that the compiler will implicitly initialize the
+       // static instances so that in_use == 0.
+
+       int i;
+       for( i = 0; i < STATIC_FRAME_COUNT; ++i ) {
+               if( ! static_frame[ i ].in_use ) {
+                       static_frame[ i ].in_use = 1;
+                       return static_frame + i;
+               }
+       }
+
+       // The static ones are all in use.  Malloc one.
+
+       return safe_malloc( sizeof( QueryFrame ) );
+}
+
+/**
+       @brief Free a QueryFrame, and all the memory it owns.
+       @param frame Pointer to the QueryFrame to be freed.
+*/
+static void free_query_frame( QueryFrame* frame ) {
+       // Sanity check
+       if( ! frame )
+               return;
+
+       clear_class_info( &frame->core );
+
+       // Free the join list
+       ClassInfo* temp;
+       ClassInfo* info = frame->join_list;
+       while( info ) {
+               temp = info->next;
+               free_class_info( info );
+               info = temp;
+       }
+
+       frame->join_list = NULL;
+       frame->next = NULL;
+
+       // If the frame is a static instance, just mark it as unused
+       int i;
+       for( i = 0; i < STATIC_FRAME_COUNT; ++i ) {
+               if( frame == static_frame + i ) {
+                       static_frame[ i ].in_use = 0;
+                       return;
+               }
+       }
+
+       // Otherwise it must have been malloc'd, so free it
+
+       free( frame );
+}
+
+/**
+       @brief Search a given QueryFrame for a specified alias.
+       @param frame Pointer to the QueryFrame to be searched.
+       @param target The alias for which to search.
+       @return Pointer to the ClassInfo for the specified alias, if found; otherwise NULL.
+*/
+static ClassInfo* search_alias_in_frame( QueryFrame* frame, const char* target ) {
+       if( ! frame || ! target ) {
+               return NULL;
+       }
+
+       ClassInfo* found_class = NULL;
+
+       if( !strcmp( target, frame->core.alias ) )
+               return &(frame->core);
+       else {
+               ClassInfo* curr_class = frame->join_list;
+               while( curr_class ) {
+                       if( strcmp( target, curr_class->alias ) )
+                               curr_class = curr_class->next;
+                       else {
+                               found_class = curr_class;
+                               break;
+                       }
+               }
+       }
+
+       return found_class;
+}
+
+/**
+       @brief Push a new (blank) QueryFrame onto the stack.
+*/
+static void push_query_frame( void ) {
+       QueryFrame* frame = allocate_frame();
+       frame->join_list = NULL;
+       frame->next = curr_query;
+
+       // Initialize the ClassInfo for the core class
+       ClassInfo* core = &frame->core;
+       core->alias = core->class_name = core->source_def = NULL;
+       core->class_def = core->fields = core->links = NULL;
+
+       curr_query = frame;
+}
+
+/**
+       @brief Pop a QueryFrame off the stack and destroy it.
+*/
+static void pop_query_frame( void ) {
+       // Sanity check
+       if( ! curr_query )
+               return;
+
+       QueryFrame* popped = curr_query;
+       curr_query = popped->next;
+
+       free_query_frame( popped );
+}
+
+/**
+       @brief Populate the ClassInfo for the core class.
+       @param alias Alias for the core class.  If it is NULL or an empty string, we use the
+       class name as an alias.
+       @param class_name Name of the core class.
+       @return Zero if successful, or 1 if not.
+
+       Populate the ClassInfo of the core class with copies of the alias and class name, and
+       with pointers to the relevant portions of the IDL for the core class.
+*/
+static int add_query_core( const char* alias, const char* class_name ) {
+
+       // Sanity checks
+       if( ! curr_query ) {
+               osrfLogError( OSRF_LOG_MARK,
+                                         "%s ERROR: No QueryFrame available for class %s", modulename, class_name );
+               return 1;
+       } else if( curr_query->core.alias ) {
+               osrfLogError( OSRF_LOG_MARK,
+                                         "%s ERROR: Core class %s already populated as %s",
+                                         modulename, curr_query->core.class_name, curr_query->core.alias );
+               return 1;
+       }
+
+       build_class_info( &curr_query->core, alias, class_name );
+       if( curr_query->core.alias )
+               return 0;
+       else {
+               osrfLogError( OSRF_LOG_MARK,
+                                         "%s ERROR: Unable to look up core class %s", modulename, class_name );
+               return 1;
+       }
+}
+
+/**
+       @brief Search the current QueryFrame for a specified alias.
+       @param target The alias for which to search.
+       @return A pointer to the corresponding ClassInfo, if found; otherwise NULL.
+*/
+static inline ClassInfo* search_alias( const char* target ) {
+       return search_alias_in_frame( curr_query, target );
+}
+
+/**
+       @brief Search all levels of query for a specified alias, starting with the current query.
+       @param target The alias for which to search.
+       @return A pointer to the corresponding ClassInfo, if found; otherwise NULL.
+*/
+static ClassInfo* search_all_alias( const char* target ) {
+       ClassInfo* found_class = NULL;
+       QueryFrame* curr_frame = curr_query;
+
+       while( curr_frame ) {
+               if(( found_class = search_alias_in_frame( curr_frame, target ) ))
+                       break;
+               else
+                       curr_frame = curr_frame->next;
+       }
+
+       return found_class;
+}
+
+/**
+       @brief Add a class to the list of classes joined to the current query.
+       @param alias Alias of the class to be added.  If it is NULL or an empty string, we use
+       the class name as an alias.
+       @param classname The name of the class to be added.
+       @return A pointer to the ClassInfo for the added class, if successful; otherwise NULL.
+*/
+static ClassInfo* add_joined_class( const char* alias, const char* classname ) {
+
+       if( ! classname || ! *classname ) {    // sanity check
+               osrfLogError( OSRF_LOG_MARK, "Can't join a class with no class name" );
+               return NULL;
+       }
+
+       if( ! alias )
+               alias = classname;
+
+       const ClassInfo* conflict = search_alias( alias );
+       if( conflict ) {
+               osrfLogError( OSRF_LOG_MARK,
+                                         "%s ERROR: Table alias \"%s\" conflicts with class \"%s\"",
+                                         modulename, alias, conflict->class_name );
+               return NULL;
+       }
+
+       ClassInfo* info = allocate_class_info();
+
+       if( build_class_info( info, alias, classname ) ) {
+               free_class_info( info );
+               return NULL;
+       }
+
+       // Add the new ClassInfo to the join list of the current QueryFrame
+       info->next = curr_query->join_list;
+       curr_query->join_list = info;
+
+       return info;
+}
+
+/**
+       @brief Destroy all nodes on the query stack.
+*/
+static void clear_query_stack( void ) {
+       while( curr_query )
+               pop_query_frame();
+}
+
+/**
+       @brief Implement the set_audit_info method.
+       @param ctx Pointer to the method context.
+       @return Zero if successful, or -1 if not.
+
+       Issue a SAVEPOINT to the database server.
+
+       Method parameters:
+       - authkey
+       - user id (int)
+       - workstation id (int)
+
+       If user id is not provided the authkey will be used.
+       For PCRUD the authkey is always used, even if a user is provided.
+*/
+int setAuditInfo( osrfMethodContext* ctx ) {
+       if(osrfMethodVerifyContext( ctx )) {
+               osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
+               return -1;
+       }
+
+       // Get the user id from the parameters
+       const char* user_id = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 1) );
+
+       if( enforce_pcrud || !user_id ) {
+               timeout_needs_resetting = 1;
+               const jsonObject* user = verifyUserPCRUD( ctx );
+               if( !user )
+                       return -1;
+               osrfAppRespondComplete( ctx, NULL );
+               return 0;
+       }
+
+       // Not PCRUD and have a user_id?
+       int result = writeAuditInfo( ctx, user_id, jsonObjectGetString( jsonObjectGetIndex(ctx->params, 2) ) );
+       osrfAppRespondComplete( ctx, NULL );
+       return result;
+}
+
+/**
+       @brief Save a audit info
+       @param ctx Pointer to the method context.
+       @param user_id User ID to write as a string
+       @param ws_id Workstation ID to write as a string
+*/
+int writeAuditInfo( osrfMethodContext* ctx, const char* user_id, const char* ws_id) {
+       if( ctx && ctx->session ) {
+               osrfAppSession* session = ctx->session;
+
+               osrfHash* cache = session->userData;
+
+               // If the session doesn't already have a hash, create one.  Make sure
+               // that the application session frees the hash when it terminates.
+               if( NULL == cache ) {
+                       session->userData = cache = osrfNewHash();
+                       osrfHashSetCallback( cache, &sessionDataFree );
+                       ctx->session->userDataFree = &userDataFree;
+               }
+
+               dbi_result result = dbi_conn_queryf( writehandle, "SELECT auditor.set_audit_info( %s, %s );", user_id, ws_id ? ws_id : "NULL" );
+               if( !result ) {
+                       osrfLogWarning( OSRF_LOG_MARK, "BAD RESULT" );
+                       const char* msg;
+                       int errnum = dbi_conn_error( writehandle, &msg );
+                       osrfLogError(
+                               OSRF_LOG_MARK,
+                               "%s: Error setting auditor information: %d %s",
+                               modulename,
+                               errnum,
+                               msg ? msg : "(No description available)"
+                       );
+                       osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+                               "osrfMethodException", ctx->request, "Error setting auditor info" );
+                       if( !oilsIsDBConnected( writehandle ))
+                               osrfAppSessionPanic( ctx->session );
+                       return -1;
+               } else {
+                       dbi_result_free( result );
+               }
+       }
+       return 0;
+}
+
+/**
+       @brief Remove all but safe character from savepoint name
+       @param sp User-supplied savepoint name
+       @return sanitized savepoint name, or NULL
+
+    The caller is expected to free the returned string.  Note that
+    this function exists only because we can't use PQescapeLiteral
+    without either forking libdbi or abandoning it.
+*/
+static char* _sanitize_savepoint_name( const char* sp ) {
+
+       const char* safe_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345789_";
+
+       // PostgreSQL uses NAMEDATALEN-1 as a max length for identifiers,
+       // and the default value of NAMEDATALEN is 64; that should be long enough
+       // for our purposes, and it's unlikely that anyone is going to recompile
+       // PostgreSQL to have a smaller value, so cap the identifier name
+       // accordingly to avoid the remote chance that someone manages to pass in a
+       // 12GB savepoint name
+       const int MAX_LITERAL_NAMELEN = 63;
+       int len = 0;
+       len = strlen( sp );
+       if (len > MAX_LITERAL_NAMELEN) {
+               len = MAX_LITERAL_NAMELEN;
+       }
+
+       char* safeSpName = safe_malloc( len + 1 );
+       int i = 0;
+       int j;
+       char* found;
+       for (j = 0; j < len; j++) {
+       found = strchr(safe_chars, sp[j]);
+               if (found) {
+                       safeSpName[ i++ ] = found[0];
+               }
+       }
+       safeSpName[ i ] = '\0';
+       return safeSpName;
+}
+
+/*@}*/
diff --git a/src/c/oils_utils.c b/src/c/oils_utils.c
new file mode 100644 (file)
index 0000000..cb334ac
--- /dev/null
@@ -0,0 +1,496 @@
+#include <ctype.h>
+#include <limits.h>
+#include "openils/oils_utils.h"
+#include "openils/oils_idl.h"
+
+osrfHash* oilsInitIDL(const char* idl_filename) {
+
+       char* freeable_filename = NULL;
+       const char* filename;
+
+       if(idl_filename)
+               filename = idl_filename;
+       else {
+               freeable_filename = osrf_settings_host_value("/IDL");
+               filename = freeable_filename;
+       }
+
+       if (!filename) {
+               osrfLogError(OSRF_LOG_MARK, "No settings config for '/IDL'");
+               return NULL;
+       }
+
+       osrfLogInfo(OSRF_LOG_MARK, "Parsing IDL %s", filename);
+
+       if (!oilsIDLInit( filename )) {
+               osrfLogError(OSRF_LOG_MARK, "Problem loading IDL file [%s]!", filename);
+               if(freeable_filename)
+                       free(freeable_filename);
+               return NULL;
+       }
+
+       if(freeable_filename)
+               free(freeable_filename);
+       return oilsIDL();
+}
+
+/**
+       @brief Return a const string with the value of a specified column in a row object.
+       @param object Pointer to the row object.
+       @param field Name of the column.
+       @return Pointer to a const string representing the value of the specified column,
+               or NULL in case of error.
+
+       The row object must be a JSON_ARRAY with a classname.  The column value must be a
+       JSON_STRING or a JSON_NUMBER.  Any other object type results in a return of NULL.
+
+       The return value points into the internal contents of the row object, which
+       retains ownership.
+*/
+const char* oilsFMGetStringConst( const jsonObject* object, const char* field ) {
+       return jsonObjectGetString(oilsFMGetObject( object, field ));
+}
+
+/**
+       @brief Return a string with the value of a specified column in a row object.
+       @param object Pointer to the row object.
+       @param field Name of the column.
+       @return Pointer to a newly allocated string representing the value of the specified column,
+               or NULL in case of error.
+
+       The row object must be a JSON_ARRAY with a classname.  The column value must be a
+       JSON_STRING or a JSON_NUMBER.  Any other object type results in a return of NULL.
+
+       The calling code is responsible for freeing the returned string by calling free().
+ */
+char* oilsFMGetString( const jsonObject* object, const char* field ) {
+       return jsonObjectToSimpleString(oilsFMGetObject( object, field ));
+}
+
+/**
+       @brief Return a pointer to the value of a specified column in a row object.
+       @param object Pointer to the row object.
+       @param field Name of the column.
+       @return Pointer to the jsonObject representing the value of the specified column, or NULL
+               in case of error.
+
+       The row object must be a JSON_ARRAY with a classname.
+
+       The return value may point to a JSON_NULL, JSON_STRING, JSON_NUMBER, or JSON_ARRAY.  It
+       points into the internal contents of the row object, which retains ownership.
+*/
+const jsonObject* oilsFMGetObject( const jsonObject* object, const char* field ) {
+       if(!(object && field)) return NULL;
+       if( object->type != JSON_ARRAY || !object->classname ) return NULL;
+       int pos = fm_ntop(object->classname, field);
+       if( pos > -1 )
+               return jsonObjectGetIndex( object, pos );
+       return NULL;
+}
+
+
+int oilsFMSetString( jsonObject* object, const char* field, const char* string ) {
+       if(!(object && field && string)) return -1;
+       osrfLogInternal(OSRF_LOG_MARK, "oilsFMSetString(): Collecing position for field %s", field);
+       int pos = fm_ntop(object->classname, field);
+       if( pos > -1 ) {
+               osrfLogInternal(OSRF_LOG_MARK, "oilsFMSetString(): Setting string "
+                               "%s at field %s [position %d]", string, field, pos );
+               jsonObjectSetIndex( object, pos, jsonNewObject(string) );
+               return 0;
+       }
+       return -1;
+}
+
+
+int oilsUtilsIsDBTrue( const char* val ) {
+       if( val && strcasecmp(val, "f") && strcmp(val, "0") ) return 1;
+       return 0;
+}
+
+
+long oilsFMGetObjectId( const jsonObject* obj ) {
+       long id = -1;
+       if(!obj) return id;
+       char* ids = oilsFMGetString( obj, "id" );
+       if(ids) {
+               id = atol(ids);
+               free(ids);
+       }
+       return id;
+}
+
+int oilsUtilsTrackUserActivity(long usr, const char* ewho, const char* ewhat, const char* ehow) {
+    if (!usr && !(ewho || ewhat || ehow)) return 0;
+    int rowcount = 0;
+
+    jsonObject* params = jsonParseFmt(
+        "{\"from\":[\"actor.insert_usr_activity\", %ld, \"%s\", \"%s\", \"%s\"]}",
+        usr, 
+        (NULL == ewho)  ? "" : ewho, 
+        (NULL == ewhat) ? "" : ewhat, 
+        (NULL == ehow)  ? "" : ehow
+    );
+
+       osrfAppSession* session = osrfAppSessionClientInit("open-ils.cstore");
+    osrfAppSessionConnect(session);
+    int reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
+       osrfMessage* omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+
+    if(omsg) {
+        osrfMessageFree(omsg);
+        reqid = osrfAppSessionSendRequest(session, params, "open-ils.cstore.json_query", 1);
+           omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+
+        if(omsg) {
+            const jsonObject* rows = osrfMessageGetResult(omsg);
+            if (rows) rowcount = rows->size;
+            osrfMessageFree(omsg); // frees rows
+            if (rowcount) {
+                reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.commit", 1);
+                   omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+                osrfMessageFree(omsg);
+            } else {
+                reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
+                   omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+                osrfMessageFree(omsg);
+            }
+        }
+    }
+
+    osrfAppSessionFree(session); // calls disconnect internally
+    jsonObjectFree(params);
+    return rowcount;
+}
+
+
+
+oilsEvent* oilsUtilsCheckPerms( int userid, int orgid, char* permissions[], int size ) {
+       if (!permissions) return NULL;
+       int i;
+       oilsEvent* evt = NULL;
+
+       // Find the root org unit, i.e. the one with no parent.
+       // Assumption: there is only one org unit with no parent.
+       if (orgid == -1) {
+               jsonObject* where_clause = jsonParse( "{\"parent_ou\":null}" );
+               jsonObject* org = oilsUtilsQuickReq(
+                       "open-ils.cstore",
+                       "open-ils.cstore.direct.actor.org_unit.search",
+                       where_clause
+               );
+               jsonObjectFree( where_clause );
+
+               orgid = (int)jsonObjectGetNumber( oilsFMGetObject( org, "id" ) );
+
+               jsonObjectFree(org);
+       }
+
+       for( i = 0; i < size && permissions[i]; i++ ) {
+
+               char* perm = permissions[i];
+               jsonObject* params = jsonParseFmt("[%d, \"%s\", %d]", userid, perm, orgid);
+               jsonObject* o = oilsUtilsQuickReq( "open-ils.storage",
+                       "open-ils.storage.permission.user_has_perm", params );
+
+               char* r = jsonObjectToSimpleString(o);
+
+               if(r && !strcmp(r, "0"))
+                       evt = oilsNewEvent3( OSRF_LOG_MARK, OILS_EVENT_PERM_FAILURE, perm, orgid );
+
+               jsonObjectFree(params);
+               jsonObjectFree(o);
+               free(r);
+
+               if(evt)
+                       break;
+       }
+
+       return evt;
+}
+
+/**
+       @brief Perform a remote procedure call.
+       @param service The name of the service to invoke.
+       @param method The name of the method to call.
+       @param params The parameters to be passed to the method, if any.
+       @return A copy of whatever the method returns as a result, or a JSON_NULL if the method
+       doesn't return anything.
+
+       If the @a params parameter points to a JSON_ARRAY, pass each element of the array
+       as a separate parameter.  If it points to any other kind of jsonObject, pass it as a
+       single parameter.  If it is NULL, pass no parameters.
+
+       The calling code is responsible for freeing the returned object by calling jsonObjectFree().
+*/
+jsonObject* oilsUtilsQuickReq( const char* service, const char* method,
+               const jsonObject* params ) {
+       if(!(service && method)) return NULL;
+
+       osrfLogDebug(OSRF_LOG_MARK, "oilsUtilsQuickReq(): %s - %s", service, method );
+
+       // Open an application session with the service, and send the request
+       osrfAppSession* session = osrfAppSessionClientInit( service );
+       int reqid = osrfAppSessionSendRequest( session, params, method, 1 );
+
+       // Get the response
+       osrfMessage* omsg = osrfAppSessionRequestRecv( session, reqid, 60 );
+       jsonObject* result = jsonObjectClone( osrfMessageGetResult(omsg) );
+
+       // Clean up
+       osrfMessageFree(omsg);
+       osrfAppSessionFree(session);
+       return result;
+}
+
+/**
+       @brief Call a method of the open-ils.storage service.
+       @param method Name of the method.
+       @param params Parameters to be passed to the method, if any.
+       @return A copy of whatever the method returns as a result, or a JSON_NULL if the method
+       doesn't return anything.
+
+       If the @a params parameter points to a JSON_ARRAY, pass each element of the array
+       as a separate parameter.  If it points to any other kind of jsonObject, pass it as a
+       single parameter.  If it is NULL, pass no parameters.
+
+       The calling code is responsible for freeing the returned object by calling jsonObjectFree().
+*/
+jsonObject* oilsUtilsStorageReq( const char* method, const jsonObject* params ) {
+       return oilsUtilsQuickReq( "open-ils.storage", method, params );
+}
+
+/**
+       @brief Call a method of the open-ils.cstore service.
+       @param method Name of the method.
+       @param params Parameters to be passed to the method, if any.
+       @return A copy of whatever the method returns as a result, or a JSON_NULL if the method
+       doesn't return anything.
+
+       If the @a params parameter points to a JSON_ARRAY, pass each element of the array
+       as a separate parameter.  If it points to any other kind of jsonObject, pass it as a
+       single parameter.  If it is NULL, pass no parameters.
+
+       The calling code is responsible for freeing the returned object by calling jsonObjectFree().
+*/
+jsonObject* oilsUtilsCStoreReq( const char* method, const jsonObject* params ) {
+       return oilsUtilsQuickReq("open-ils.cstore", method, params);
+}
+
+
+
+/**
+       @brief Given a username, fetch the corresponding row from the actor.usr table, if any.
+       @param name The username for which to search.
+       @return A Fieldmapper object for the relevant row in the actor.usr table, if it exists;
+       or a JSON_NULL if it doesn't.
+
+       The calling code is responsible for freeing the returned object by calling jsonObjectFree().
+*/
+jsonObject* oilsUtilsFetchUserByUsername( const char* name ) {
+       if(!name) return NULL;
+       jsonObject* params = jsonParseFmt("{\"usrname\":\"%s\"}", name);
+       jsonObject* user = oilsUtilsQuickReq(
+               "open-ils.cstore", "open-ils.cstore.direct.actor.user.search", params );
+
+       jsonObjectFree(params);
+       long id = oilsFMGetObjectId(user);
+       osrfLogDebug(OSRF_LOG_MARK, "Fetched user %s:%ld", name, id);
+       return user;
+}
+
+/**
+       @brief Given a barcode, fetch the corresponding row from the actor.usr table, if any.
+       @param name The barcode for which to search.
+       @return A Fieldmapper object for the relevant row in the actor.usr table, if it exists;
+       or a JSON_NULL if it doesn't.
+
+       Look up the barcode in actor.card.  Follow a foreign key from there to get a row in
+       actor.usr.
+
+       The calling code is responsible for freeing the returned object by calling jsonObjectFree().
+*/
+jsonObject* oilsUtilsFetchUserByBarcode(const char* barcode) {
+       if(!barcode) return NULL;
+
+       osrfLogInfo(OSRF_LOG_MARK, "Fetching user by barcode %s", barcode);
+
+       jsonObject* params = jsonParseFmt("{\"barcode\":\"%s\"}", barcode);
+       jsonObject* card = oilsUtilsQuickReq(
+               "open-ils.cstore", "open-ils.cstore.direct.actor.card.search", params );
+       jsonObjectFree(params);
+
+       if(!card)
+               return NULL;   // No such card
+
+       // Get the user's id as a double
+       char* usr = oilsFMGetString(card, "usr");
+       jsonObjectFree(card);
+       if(!usr)
+               return NULL;   // No user id (shouldn't happen)
+       double iusr = strtod(usr, NULL);
+       free(usr);
+
+       // Look up the user in actor.usr
+       params = jsonParseFmt("[%f]", iusr);
+       jsonObject* user = oilsUtilsQuickReq(
+               "open-ils.cstore", "open-ils.cstore.direct.actor.user.retrieve", params);
+
+       jsonObjectFree(params);
+       return user;
+}
+
+char* oilsUtilsFetchOrgSetting( int orgid, const char* setting ) {
+       if(!setting) return NULL;
+
+       jsonObject* params = jsonParseFmt("[%d, \"%s\"]", orgid, setting );
+
+       jsonObject* set = oilsUtilsQuickReq(
+               "open-ils.actor",
+               "open-ils.actor.ou_setting.ancestor_default", params);
+
+       char* value = jsonObjectToSimpleString( jsonObjectGetKeyConst( set, "value" ));
+       jsonObjectFree(params);
+       jsonObjectFree(set);
+       osrfLogDebug(OSRF_LOG_MARK, "Fetched org [%d] setting: %s => %s", orgid, setting, value);
+       return value;
+}
+
+
+
+char* oilsUtilsLogin( const char* uname, const char* passwd, const char* type, int orgId ) {
+       if(!(uname && passwd)) return NULL;
+
+       osrfLogDebug(OSRF_LOG_MARK, "Logging in with username %s", uname );
+       char* token = NULL;
+
+       jsonObject* params = jsonParseFmt("[\"%s\"]", uname);
+
+       jsonObject* o = oilsUtilsQuickReq(
+               "open-ils.auth", "open-ils.auth.authenticate.init", params );
+
+       const char* seed = jsonObjectGetString(o);
+       char* passhash = md5sum(passwd);
+       char buf[256];
+       snprintf(buf, sizeof(buf), "%s%s", seed, passhash);
+       char* fullhash = md5sum(buf);
+
+       jsonObjectFree(o);
+       jsonObjectFree(params);
+       free(passhash);
+
+       params = jsonParseFmt( "[\"%s\", \"%s\", \"%s\", \"%d\"]", uname, fullhash, type, orgId );
+       o = oilsUtilsQuickReq( "open-ils.auth",
+               "open-ils.auth.authenticate.complete", params );
+
+       if(o) {
+               const char* tok = jsonObjectGetString(
+                       jsonObjectGetKeyConst( jsonObjectGetKey( o,"payload" ), "authtoken" ));
+               if( tok )
+                       token = strdup( tok );
+       }
+
+       free(fullhash);
+       jsonObjectFree(params);
+       jsonObjectFree(o);
+
+       return token;
+}
+
+
+jsonObject* oilsUtilsFetchWorkstation( long id ) {
+       jsonObject* p = jsonParseFmt("[%ld]", id);
+       jsonObject* r = oilsUtilsQuickReq(
+               "open-ils.storage",
+               "open-ils.storage.direct.actor.workstation.retrieve", p );
+       jsonObjectFree(p);
+       return r;
+}
+
+jsonObject* oilsUtilsFetchWorkstationByName( const char* name ) {
+       jsonObject* p = jsonParseFmt("{\"name\":\"%s\"}", name);
+       jsonObject* r = oilsUtilsCStoreReq(
+               "open-ils.cstore.direct.actor.workstation.search", p);
+       jsonObjectFree(p);
+       return r;
+}
+
+/**
+       @brief Convert a string to a number representing a time interval in seconds.
+       @param interval Pointer to string, e.g. "420" or "2 weeks".
+       @return If successful, the number of seconds that the string represents; otherwise -1.
+
+       If the string is all digits (apart from any leading or trailing white space), convert
+       it directly.  Otherwise pass it to PostgreSQL for translation.
+
+       The result is the same as if we were to pass every string to PostgreSQL, except that,
+       depending on the value of LONG_MAX, we return values for some strings that represent
+       intervals too long for PostgreSQL to represent (i.e. more than 2147483647 seconds).
+
+       WARNING: a valid interval of -1 second will be indistinguishable from an error.  If
+       such an interval is a plausible possibility, don't use this function.
+*/
+long oilsUtilsIntervalToSeconds( const char* s ) {
+
+       if( !s ) {
+               osrfLogWarning( OSRF_LOG_MARK, "String to be converted is NULL" );
+               return -1;
+       }
+
+       // Skip leading white space
+       while( isspace( (unsigned char) *s ))
+               ++s;
+
+       if( '\0' == *s ) {
+               osrfLogWarning( OSRF_LOG_MARK, "String to be converted is empty or all white space" );
+               return -1;
+       }
+
+       // See if the string is a raw number, i.e. all digits
+       // (apart from any leading or trailing white space)
+
+       const char* p = s;   // For traversing and examining the remaining string
+       if( isdigit( (unsigned char) *p )) {
+               // Looks like a number so far...skip over the digits
+               do {
+                       ++p;
+               } while( isdigit( (unsigned char) *p ));
+               // Skip over any following white space
+               while( isspace( (unsigned char) *p ))
+                       ++p;
+               if( '\0' == *p ) {
+                       // This string is a raw number.  Convert it directly.
+                       long n = strtol( s, NULL, 10 );
+                       if( LONG_MAX == n ) {
+                               // numeric overflow
+                               osrfLogWarning( OSRF_LOG_MARK,
+                                       "String \"%s\"represents a number too big for a long", s );
+                               return -1;
+                       } else
+                               return n;
+               }
+       }
+
+       // If we get to this point, the string is not all digits.  Pass it to PostgreSQL.
+
+       // Build the query
+       jsonObject* query_obj = jsonParseFmt(
+               "{\"from\":[\"config.interval_to_seconds\",\"%s\"]}", s );
+
+       // Execute the query
+       jsonObject* result = oilsUtilsCStoreReq(
+               "open-ils.cstore.json_query", query_obj );
+       jsonObjectFree( query_obj );
+
+       // Get the results
+       const jsonObject* seconds_obj = jsonObjectGetKeyConst( result, "config.interval_to_seconds" );
+       long seconds = -1;
+       if( seconds_obj && JSON_NUMBER == seconds_obj->type )
+               seconds = (long) jsonObjectGetNumber( seconds_obj );
+       else
+               osrfLogError( OSRF_LOG_MARK,
+                       "Error calling json_query to convert \"%s\" to seconds", s );
+
+       jsonObjectFree( result );
+       return seconds;
+}
diff --git a/src/c/sharestuff_auth.c b/src/c/sharestuff_auth.c
new file mode 100644 (file)
index 0000000..97cc6cd
--- /dev/null
@@ -0,0 +1,755 @@
+#include "opensrf/osrf_app_session.h"
+#include "opensrf/osrf_application.h"
+#include "opensrf/osrf_settings.h"
+#include "opensrf/osrf_json.h"
+#include "opensrf/log.h"
+#include "openils/oils_utils.h"
+#include "openils/oils_constants.h"
+#include "openils/oils_event.h"
+
+#define OILS_AUTH_CACHE_PRFX "oils_auth_"
+#define OILS_AUTH_COUNT_SFFX "_count"
+
+#define MODULENAME "open-ils.auth"
+
+#define OILS_AUTH_OPAC "opac"
+#define OILS_AUTH_STAFF "staff"
+#define OILS_AUTH_TEMP "temp"
+#define OILS_AUTH_PERSIST "persist"
+
+// Default time for extending a persistent session: ten minutes
+#define DEFAULT_RESET_INTERVAL 10 * 60
+
+int osrfAppInitialize();
+int osrfAppChildInit();
+
+static long _oilsAuthOPACTimeout = 0;
+static long _oilsAuthStaffTimeout = 0;
+static long _oilsAuthOverrideTimeout = 0;
+static long _oilsAuthPersistTimeout = 0;
+static long _oilsAuthSeedTimeout = 0;
+static long _oilsAuthBlockTimeout = 0;
+static long _oilsAuthBlockCount = 0;
+
+
+/**
+       @brief Initialize the application by registering functions for method calls.
+       @return Zero in all cases.
+*/
+int osrfAppInitialize() {
+
+       osrfLogInfo(OSRF_LOG_MARK, "Initializing Auth Server...");
+
+       /* load and parse the IDL */
+       if (!oilsInitIDL(NULL)) return 1; /* return non-zero to indicate error */
+
+       osrfAppRegisterMethod(
+               MODULENAME,
+               "open-ils.auth.authenticate.init",
+               "oilsAuthInit",
+               "Start the authentication process and returns the intermediate authentication seed"
+               " PARAMS( username )", 1, 0 );
+
+       osrfAppRegisterMethod(
+               MODULENAME,
+               "open-ils.auth.authenticate.complete",
+               "oilsAuthComplete",
+               "Completes the authentication process.  Returns an object like so: "
+               "{authtoken : <token>, authtime:<time>}, where authtoken is the login "
+               "token and authtime is the number of seconds the session will be active"
+               "PARAMS(username, md5sum( seed + md5sum( password ) ), type, org_id ) "
+               "type can be one of 'opac','staff', or 'temp' and it defaults to 'staff' "
+               "org_id is the location at which the login should be considered "
+               "active for login timeout purposes", 1, 0 );
+
+       osrfAppRegisterMethod(
+               MODULENAME,
+               "open-ils.auth.authenticate.verify",
+               "oilsAuthComplete",
+               "Verifies the user provided a valid username and password."
+               "Params and are the same as open-ils.auth.authenticate.complete."
+               "Returns SUCCESS event on success, failure event on failure", 1, 0);
+
+
+       osrfAppRegisterMethod(
+               MODULENAME,
+               "open-ils.auth.session.retrieve",
+               "oilsAuthSessionRetrieve",
+               "Pass in the auth token and this retrieves the user object.  The auth "
+               "timeout is reset when this call is made "
+               "Returns the user object (password blanked) for the given login session "
+               "PARAMS( authToken )", 1, 0 );
+
+       osrfAppRegisterMethod(
+               MODULENAME,
+               "open-ils.auth.session.delete",
+               "oilsAuthSessionDelete",
+               "Destroys the given login session "
+               "PARAMS( authToken )",  1, 0 );
+
+       osrfAppRegisterMethod(
+               MODULENAME,
+               "open-ils.auth.session.reset_timeout",
+               "oilsAuthResetTimeout",
+               "Resets the login timeout for the given session "
+               "Returns an ILS Event with payload = session_timeout of session "
+               "if found, otherwise returns the NO_SESSION event"
+               "PARAMS( authToken )", 1, 0 );
+
+       if(!_oilsAuthSeedTimeout) { /* Load the default timeouts */
+
+               jsonObject* value_obj;
+
+               value_obj = osrf_settings_host_value_object(
+                       "/apps/open-ils.auth/app_settings/auth_limits/seed" );
+               _oilsAuthSeedTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+               jsonObjectFree(value_obj);
+               if( -1 == _oilsAuthSeedTimeout ) {
+                       osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Auth Seeds - Using 30 seconds" );
+                       _oilsAuthSeedTimeout = 30;
+               }
+
+               value_obj = osrf_settings_host_value_object(
+                       "/apps/open-ils.auth/app_settings/auth_limits/block_time" );
+               _oilsAuthBlockTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+               jsonObjectFree(value_obj);
+               if( -1 == _oilsAuthBlockTimeout ) {
+                       osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Blocking Timeout - Using 3x Seed" );
+                       _oilsAuthBlockTimeout = _oilsAuthSeedTimeout * 3;
+               }
+
+               value_obj = osrf_settings_host_value_object(
+                       "/apps/open-ils.auth/app_settings/auth_limits/block_count" );
+               _oilsAuthBlockCount = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+               jsonObjectFree(value_obj);
+               if( -1 == _oilsAuthBlockCount ) {
+                       osrfLogWarning( OSRF_LOG_MARK, "Invalid count for Blocking - Using 10" );
+                       _oilsAuthBlockCount = 10;
+               }
+
+               osrfLogInfo(OSRF_LOG_MARK, "Set auth limits: "
+                       "seed => %ld : block_timeout => %ld : block_count => %ld",
+                       _oilsAuthSeedTimeout, _oilsAuthBlockTimeout, _oilsAuthBlockCount );
+       }
+
+       return 0;
+}
+
+/**
+       @brief Dummy placeholder for initializing a server drone.
+
+       There is nothing to do, so do nothing.
+*/
+int osrfAppChildInit() {
+       return 0;
+}
+
+/**
+       @brief Implement the "init" method.
+       @param ctx The method context.
+       @return Zero if successful, or -1 if not.
+
+       Method parameters:
+       - username
+
+       Return to client: Intermediate authentication seed.
+
+       Combine the username with a timestamp and process ID, and take an md5 hash of the result.
+       Store the hash in memcache, with a key based on the username.  Then return the hash to
+       the client.
+
+       However: if the username includes one or more embedded blank spaces, return a dummy
+       hash without storing anything in memcache.  The dummy will never match a stored hash, so
+       any attempt to authenticate with it will fail.
+*/
+int oilsAuthInit( osrfMethodContext* ctx ) {
+       OSRF_METHOD_VERIFY_CONTEXT(ctx);
+
+       char* username  = jsonObjectToSimpleString( jsonObjectGetIndex(ctx->params, 0) );
+       if( username ) {
+
+               jsonObject* resp;
+
+               if( strchr( username, ' ' ) ) {
+
+                       // Embedded spaces are not allowed in a username.  Use "x" as a dummy
+                       // seed.  It will never be a valid seed because 'x' is not a hex digit.
+                       resp = jsonNewObject( "x" );
+
+               } else {
+
+                       // Build a key and a seed; store them in memcache.
+                       char* key  = va_list_to_string( "%s%s", OILS_AUTH_CACHE_PRFX, username );
+                       char* countkey = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, username, OILS_AUTH_COUNT_SFFX );
+                       char* seed = md5sum( "%d.%ld.%s", (int) time(NULL), (long) getpid(), username );
+                       jsonObject* countobject = osrfCacheGetObject( countkey );
+                       if(!countobject) {
+                               countobject = jsonNewNumberObject( (double) 0 );
+                       }
+                       osrfCachePutString( key, seed, _oilsAuthSeedTimeout );
+                       osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
+
+                       osrfLogDebug( OSRF_LOG_MARK, "oilsAuthInit(): has seed %s and key %s", seed, key );
+
+                       // Build a returnable object containing the seed.
+                       resp = jsonNewObject( seed );
+
+                       free( seed );
+                       free( key );
+                       free( countkey );
+                       jsonObjectFree( countobject );
+               }
+
+               // Return the seed to the client.
+               osrfAppRespondComplete( ctx, resp );
+
+               jsonObjectFree(resp);
+               free(username);
+               return 0;
+       }
+
+       return -1;  // Error: no username parameter
+}
+
+/**
+       Verifies that the user has permission to login with the
+       given type.  If the permission fails, an oilsEvent is returned
+       to the caller.
+       @return -1 if the permission check failed, 0 if the permission
+       is granted
+*/
+static int oilsAuthCheckLoginPerm(
+               osrfMethodContext* ctx, const jsonObject* userObj ) {
+
+       if(!userObj) return -1;
+       oilsEvent* perm = NULL;
+
+       char* permissions[] = { "LOGIN" };
+       perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
+
+       if(perm) {
+               osrfAppRespondComplete( ctx, oilsEventToJSON(perm) );
+               oilsEventFree(perm);
+               return -1;
+       }
+
+       return 0;
+}
+
+/**
+       Returns 1 if the password provided matches the user's real password
+       Returns 0 otherwise
+       Returns -1 on error
+*/
+/**
+       @brief Verify the password received from the client.
+       @param ctx The method context.
+       @param userObj An object from the database, representing the user.
+       @param password An obfuscated password received from the client.
+       @return 1 if the password is valid; 0 if it isn't; or -1 upon error.
+
+       (None of the so-called "passwords" used here are in plaintext.  All have been passed
+       through at least one layer of hashing to obfuscate them.)
+
+       Take the password from the user object.  Append it to the username seed from memcache,
+       as stored previously by a call to the init method.  Take an md5 hash of the result.
+       Then compare this hash to the password received from the client.
+
+       In order for the two to match, other than by dumb luck, the client had to construct
+       the password it passed in the same way.  That means it neded to know not only the
+       original password (either hashed or plaintext), but also the seed.  The latter requirement
+       means that the client process needs either to be the same process that called the init
+       method or to receive the seed from the process that did so.
+*/
+static int oilsAuthVerifyPassword( const osrfMethodContext* ctx,
+               const jsonObject* userObj, const char* uname, const char* password ) {
+
+       // Get the username seed, as stored previously in memcache by the init method
+       char* seed = osrfCacheGetString( "%s%s", OILS_AUTH_CACHE_PRFX, uname );
+       if(!seed) {
+               return osrfAppRequestRespondException( ctx->session,
+                       ctx->request, "No authentication seed found. "
+                       "open-ils.auth.authenticate.init must be called first "
+                       " (check that memcached is running and can be connected to) "
+               );
+       }
+    
+       // We won't be needing the seed again, remove it
+       osrfCacheRemove( "%s%s", OILS_AUTH_CACHE_PRFX, uname );
+
+       // Get the hashed password from the user object
+       char* realPassword = oilsFMGetString( userObj, "passwd" );
+
+       osrfLogInternal(OSRF_LOG_MARK, "oilsAuth retrieved real password: [%s]", realPassword);
+       osrfLogDebug(OSRF_LOG_MARK, "oilsAuth retrieved seed from cache: %s", seed );
+
+       // Concatenate them and take an MD5 hash of the result
+       char* maskedPw = md5sum( "%s%s", seed, realPassword );
+
+       free(realPassword);
+       free(seed);
+
+       if( !maskedPw ) {
+               // This happens only if md5sum() runs out of memory
+               free( maskedPw );
+               return -1;  // md5sum() ran out of memory
+       }
+
+       osrfLogDebug(OSRF_LOG_MARK,  "oilsAuth generated masked password %s. "
+                       "Testing against provided password %s", maskedPw, password );
+
+       int ret = 0;
+       if( !strcmp( maskedPw, password ) )
+               ret = 1;
+
+       free(maskedPw);
+
+       char* countkey = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, uname, OILS_AUTH_COUNT_SFFX );
+       jsonObject* countobject = osrfCacheGetObject( countkey );
+       if(countobject) {
+               long failcount = (long) jsonObjectGetNumber( countobject );
+               if(failcount >= _oilsAuthBlockCount) {
+                       ret = 0;
+                   osrfLogInfo(OSRF_LOG_MARK, "oilsAuth found too many recent failures for '%s' : %i, forcing failure state.", uname, failcount);
+               }
+               if(ret == 0) {
+                       failcount += 1;
+               }
+               jsonObjectSetNumber( countobject, failcount );
+               osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
+               jsonObjectFree(countobject);
+       }
+       free(countkey);
+
+       return ret;
+}
+
+/**
+       @brief Determine the login timeout.
+       @param userObj Pointer to an object describing the user.
+       @param orgloc Org unit to use for settings lookups (negative or zero means unspecified)
+       @return The length of the timeout, in seconds.
+
+       The default timeout value comes from the configuration file.
+
+       The default may be overridden by a corresponding org unit setting.  The @a orgloc
+       parameter says what org unit to use for the lookup.  If @a orgloc <= 0, or if the
+       lookup for @a orgloc yields no result, we look up the setting for the user's home org unit
+       instead (except that if it's the same as @a orgloc we don't bother repeating the lookup).
+
+       Whether defined in the config file or in an org unit setting, a timeout value may be
+       expressed as a raw number (i.e. all digits, possibly with leading and/or trailing white
+       space) or as an interval string to be translated into seconds by PostgreSQL.
+*/
+static long oilsAuthGetTimeout( const jsonObject* userObj) {
+
+       if(!_oilsAuthTimeout) { /* Load the default timeouts */
+
+               jsonObject* value_obj;
+
+               value_obj = osrf_settings_host_value_object(
+                       "/apps/sharestuff.auth/app_settings/default_timeout" );
+               _oilsAuthTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+               jsonObjectFree(value_obj);
+               if( -1 == _oilsAuthTimeout ) {
+                       osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for logins" );
+                       _oilsAuthTimeout = 0;
+               }
+
+               osrfLogInfo(OSRF_LOG_MARK, "Set default auth timeout: %ld", _oilsAuthTimeout );
+       }
+
+       int orgloc = (int) jsonObjectGetNumber( oilsFMGetObject( userObj, "home_ou" ));
+
+       // Get the org unit setting, if there is one.
+       char* timeout = oilsUtilsFetchOrgSetting( orgloc, OILS_ORG_SETTING_TIMEOUT );
+
+       if(!timeout)
+               return _oilsAuthTimeout;   // No override from org unit setting
+
+       // Translate the org unit setting to a number
+       long t;
+       if( !*timeout ) {
+               osrfLogWarning( OSRF_LOG_MARK, "Timeout org unit setting is an empty string; using default" );
+               t = _oilsAuthTimeout;
+       } else {
+               // Treat timeout string as an interval, and convert it to seconds
+               t = oilsUtilsIntervalToSeconds( timeout );
+               if( -1 == t ) {
+                       // Unable to convert; possibly an invalid interval string
+                       osrfLogError( OSRF_LOG_MARK,
+                               "Unable to convert timeout interval \"%s\"; using default",
+                               timeout );
+                       t = default_timeout;
+               }
+       }
+
+       free(timeout);
+       return t;
+}
+
+/*
+       Adds the authentication token to the user cache.
+       Returns the event that should be returned to the user.
+       Event must be freed
+*/
+static oilsEvent* oilsAuthHandleLoginOK( jsonObject* userObj, const char* uname ) {
+
+       oilsEvent* response;
+
+       long timeout;
+       char* wsorg = jsonObjectToSimpleString(oilsFMGetObject(userObj, "home_ou"));
+       osrfLogDebug( OSRF_LOG_MARK,
+               "Auth session trying workstation id %d for auth timeout", atoi(wsorg));
+       timeout = oilsAuthGetTimeout( userObj );
+       free(wsorg);
+
+       osrfLogDebug(OSRF_LOG_MARK, "Auth session timeout for %s: %ld", uname, timeout );
+
+       char* string = va_list_to_string(
+                       "%d.%ld.%s", (long) getpid(), time(NULL), uname );
+       char* authToken = md5sum(string);
+       char* authKey = va_list_to_string(
+                       "%s%s", OILS_AUTH_CACHE_PRFX, authToken );
+
+       osrfLogActivity(OSRF_LOG_MARK,
+               "successful login: username=%s, authtoken=%s", uname, authToken );
+
+       oilsFMSetString( userObj, "passwd", "" );
+       jsonObject* cacheObj = jsonParseFmt( "{\"authtime\": %ld}", timeout );
+       jsonObjectSetKey( cacheObj, "userobj", jsonObjectClone(userObj));
+
+       osrfCachePutObject( authKey, cacheObj, (time_t) timeout );
+       jsonObjectFree(cacheObj);
+       osrfLogInternal(OSRF_LOG_MARK, "oilsAuthHandleLoginOK(): Placed user object into cache");
+       jsonObject* payload = jsonParseFmt(
+               "{ \"authtoken\": \"%s\", \"authtime\": %ld }", authToken, timeout );
+
+       response = oilsNewEvent2( OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload );
+       free(string); free(authToken); free(authKey);
+       jsonObjectFree(payload);
+
+       return response;
+}
+
+
+/**
+       @brief Implement the "complete" method.
+       @param ctx The method context.
+       @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
+       client to indicate completion; a positive integer if successful but no such STATUS
+       message has been sent.
+
+       Method parameters:
+       - a hash with some combination of the following elements:
+               - "username"
+               - "password" (hashed with the cached seed; not plaintext)
+
+       Both username and password are required.
+
+       Return to client: Intermediate authentication seed.
+
+       Validate the password using the username.
+
+       Upon deciding whether to allow the logon, return a corresponding event to the client.
+*/
+int oilsAuthComplete( osrfMethodContext* ctx ) {
+       OSRF_METHOD_VERIFY_CONTEXT(ctx);
+
+       const jsonObject* args  = jsonObjectGetIndex(ctx->params, 0);
+
+       const char* uname       = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
+       const char* password    = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
+
+       /* Use __FILE__, harmless_line_number for creating
+        * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
+        * giving away information about why an authentication attempt failed.
+        */
+       int harmless_line_number = __LINE__;
+
+       if( !(uname && password) ) {
+               return osrfAppRequestRespondException( ctx->session, ctx->request,
+                       "username and password required for method: %s", ctx->method->name );
+       }
+
+       oilsEvent* response = NULL;
+       jsonObject* userObj = NULL;
+
+       // Fetch a row from the actor.usr table by username.
+       userObj = oilsUtilsFetchUserByUsername( uname );
+       if( userObj && JSON_NULL == userObj->type ) {
+               jsonObjectFree( userObj );
+               userObj = NULL;         // username not found
+       }
+
+       int     barred = 0, deleted = 0;
+       char   *barred_str, *deleted_str;
+
+       if(userObj) {
+               barred_str = oilsFMGetString( userObj, "barred" );
+               barred = oilsUtilsIsDBTrue( barred_str );
+               free( barred_str );
+
+               deleted_str = oilsFMGetString( userObj, "deleted" );
+               deleted = oilsUtilsIsDBTrue( deleted_str );
+               free( deleted_str );
+       }
+
+       if(!userObj || barred || deleted) {
+               response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
+               osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s", uname );
+               osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
+               oilsEventFree(response);
+               return 0;           // No such user
+       }
+
+       // Such a user exists and isn't barred or deleted.
+       // Now see if he or she has the right credentials.
+       int passOK = -1;
+       passOK = oilsAuthVerifyPassword( ctx, userObj, uname, password );
+
+       if( passOK < 0 ) {
+               jsonObjectFree(userObj);
+               return passOK;
+       }
+
+       // See if the account is active
+       char* active = oilsFMGetString(userObj, "active");
+       if( !oilsUtilsIsDBTrue(active) ) {
+               if( passOK )
+                       response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_INACTIVE" );
+               else
+                       response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
+
+               osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
+               oilsEventFree(response);
+               jsonObjectFree(userObj);
+               free(active);
+               return 0;
+       }
+       free(active);
+
+       // See if the user is even allowed to log in
+       if( oilsAuthCheckLoginPerm( ctx, userObj, type ) == -1 ) {
+               jsonObjectFree(userObj);
+               return 0;
+       }
+
+       if( passOK ) { // login successful  
+        
+               if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
+                       response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
+               } else {
+                       response = oilsAuthHandleLoginOK( userObj, uname );
+               }
+
+       } else {
+               response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
+               osrfLogInfo(OSRF_LOG_MARK,  "failed login: username=%s", uname );
+       }
+
+       jsonObjectFree(userObj);
+       osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
+       oilsEventFree(response);
+
+       return 0;
+}
+
+
+
+int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
+       OSRF_METHOD_VERIFY_CONTEXT(ctx);
+
+       const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
+       jsonObject* resp = NULL;
+
+       if( authToken ) {
+               osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
+               char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
+               osrfCacheRemove(key);
+               resp = jsonNewObject(authToken); /**/
+               free(key);
+       }
+
+       osrfAppRespondComplete( ctx, resp );
+       jsonObjectFree(resp);
+       return 0;
+}
+
+/**
+ * Fetches the user object from the database and updates the user object in 
+ * the cache object, which then has to be re-inserted into the cache.
+ * User object is retrieved inside a transaction to avoid replication issues.
+ */
+static int _oilsAuthReloadUser(jsonObject* cacheObj) {
+    int reqid, userId;
+    osrfAppSession* session;
+       osrfMessage* omsg;
+    jsonObject *param, *userObj, *newUserObj;
+
+    userObj = jsonObjectGetKey( cacheObj, "userobj" );
+    userId = oilsFMGetObjectId( userObj );
+
+    session = osrfAppSessionClientInit( "open-ils.cstore" );
+    osrfAppSessionConnect(session);
+
+    reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
+       omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+
+    if(omsg) {
+
+        osrfMessageFree(omsg);
+        param = jsonNewNumberObject(userId);
+        reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
+           omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+        jsonObjectFree(param);
+
+        if(omsg) {
+            newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
+            osrfMessageFree(omsg);
+            reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
+               omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+            osrfMessageFree(omsg);
+        }
+    }
+
+    osrfAppSessionFree(session); // calls disconnect internally
+
+    if(newUserObj)
+        return 1;
+
+    osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
+    return 0;
+}
+
+/**
+       Resets the auth login timeout
+       @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
+*/
+static oilsEvent*  _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
+       if(!authToken) return NULL;
+
+       oilsEvent* evt = NULL;
+       time_t timeout;
+
+       osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
+       char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
+       jsonObject* cacheObj = osrfCacheGetObject( key );
+
+       osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
+       evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
+
+    if(reloadUser) {
+        _oilsAuthReloadUser(cacheObj);
+    }
+
+       // Determine a new timeout value
+       jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
+       if( endtime_obj ) {
+               // Extend the current endtime by a fixed amount
+               time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
+               int reset_interval = DEFAULT_RESET_INTERVAL;
+               const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
+                       cacheObj, "reset_interval" );
+               if( reset_interval_obj ) {
+                       reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
+                       if( reset_interval <= 0 )
+                               reset_interval = DEFAULT_RESET_INTERVAL;
+               }
+
+               time_t now = time( NULL );
+               time_t new_endtime = now + reset_interval;
+               if( new_endtime > endtime ) {
+                       // Keep the session alive a little longer
+                       jsonObjectSetNumber( endtime_obj, (double) new_endtime );
+                       timeout = reset_interval;
+                       osrfCachePutObject( key, cacheObj, timeout );
+               } else {
+                       // The session isn't close to expiring, so don't reset anything.
+                       // Just report the time remaining.
+                       timeout = endtime - now;
+               }
+       } else {
+               // Reapply the existing timeout from the current time
+               timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
+               osrfCachePutObject( key, cacheObj, timeout );
+       }
+
+       jsonObject* payload = jsonNewNumberObject( (double) timeout );
+       evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
+       jsonObjectFree(payload);
+       jsonObjectFree(cacheObj);
+
+       free(key);
+       return evt;
+}
+
+int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
+       OSRF_METHOD_VERIFY_CONTEXT(ctx);
+       const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
+    double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
+       oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
+       osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
+       oilsEventFree(evt);
+       return 0;
+}
+
+
+int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
+       OSRF_METHOD_VERIFY_CONTEXT(ctx);
+    bool returnFull = false;
+
+       const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
+
+    if(ctx->params->size > 1) {
+        // caller wants full cached object, with authtime, etc.
+        const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
+        if(rt && strcmp(rt, "0") != 0) 
+            returnFull = true;
+    }
+
+       jsonObject* cacheObj = NULL;
+       oilsEvent* evt = NULL;
+
+       if( authToken ){
+
+               // Reset the timeout to keep the session alive
+               evt = _oilsAuthResetTimeout(authToken, 0);
+
+               if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
+                       osrfAppRespondComplete( ctx, oilsEventToJSON( evt ));    // can't reset timeout
+
+               } else {
+
+                       // Retrieve the cached session object
+                       osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
+                       char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
+                       cacheObj = osrfCacheGetObject( key );
+                       if(cacheObj) {
+                               // Return a copy of the cached user object
+                if(returnFull)
+                                   osrfAppRespondComplete( ctx, cacheObj);
+                else
+                                   osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
+                               jsonObjectFree(cacheObj);
+                       } else {
+                               // Auth token is invalid or expired
+                               oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
+                               osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
+                               oilsEventFree(evt2);
+                       }
+                       free(key);
+               }
+
+       } else {
+
+               // No session
+               evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
+               osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
+       }
+
+       if(evt)
+               oilsEventFree(evt);
+
+       return 0;
+}
diff --git a/src/c/test_json_query.c b/src/c/test_json_query.c
new file mode 100644 (file)
index 0000000..9da9732
--- /dev/null
@@ -0,0 +1,263 @@
+/*
+Copyright (C) 2009  Georgia Public Library Service 
+Scott McKellar <scott@esilibrary.com>
+
+       This program is free software; you can redistribute it and/or
+       modify it under the terms of the GNU General Public License
+       as published by the Free Software Foundation; either version 2
+       of the License, or (at your option) any later version.
+
+       This program is distributed in the hope that it will be useful,
+       but WITHOUT ANY WARRANTY; without even the implied warranty of
+       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+       GNU General Public License for more details.
+
+       Description : Translates a JSON query into SQL and writes the
+       results to standard output.  Synopsis:
+
+       test_json_query [-i IDL_file] [-f file_name] [-v] query
+       
+       -i supplies the name of the IDL file.  If no IDL file is specified,
+          json_test_query uses the value of the environmental variable
+          OILS_IDL_FILENAME, if it is defined, or defaults to
+          "/openils/conf/fm_IDL.xml".
+
+       -f supplies the name of a text file containing the JSON query to
+          be translated.  A file name constisting of a single hyphen
+          denotes standard input.  If this option is present, all
+          non-option arguments are ignored.
+
+       -v verbose; outputs the name of the IDL file and the text of the
+          JSON query.
+
+       If there is no -f option supplied, json_query translates the 
+       first non-option parameter.  This parameter is subject to the
+       usual mangling by the shell.  In most cases it will be sufficient
+       to enclose it in single quotes, but of course any single quotes
+       embedded within the query will need to be escaped.
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <dbi/dbi.h>
+#include "opensrf/utils.h"
+#include "opensrf/osrf_json.h"
+#include "opensrf/osrf_application.h"
+#include "opensrf/osrf_app_session.h"
+#include "openils/oils_idl.h"
+#include "openils/oils_sql.h"
+
+#define DISABLE_I18N    2
+#define SELECT_DISTINCT 1
+
+static int obj_is_true( const jsonObject* obj );
+static int test_json_query( const char* json_query );
+static char* load_query( const char* filename );
+
+int main( int argc, char* argv[] ) {
+
+       // Parse command line
+
+       const char* idl_file_name = NULL;
+       const char* query_file_name = NULL;
+       int verbose = 0;                        // boolean
+
+       int opt;
+       opterr = 0;
+       const char optstring[] = ":f:i:v";
+
+       while( ( opt = getopt( argc, argv, optstring ) ) != -1 ) {
+               switch( opt )
+               {
+                       case 'f' :  // get file name of query
+                               if( query_file_name ) {
+                                       fprintf( stderr, "Multiple input files not allowed\n" );
+                                       return EXIT_FAILURE;
+                               }
+                               else
+                                       query_file_name = optarg;
+                               break;
+                       case 'i' :  // get name of IDL file
+                               if( idl_file_name ) {
+                                       fprintf( stderr, "Multiple IDL file names not allowed\n" );
+                                       return EXIT_FAILURE;
+                               }
+                               else
+                                       idl_file_name = optarg;
+                               break;
+                       case 'v' :  // Verbose
+                               verbose = 1;
+                               break;
+                       case '?' :  // Invalid option
+                               fprintf( stderr, "Invalid option '-%c' on command line\n",
+                                                (char) optopt );
+                               return EXIT_FAILURE;
+                       default :  // Huh?
+                               fprintf( stderr, "Internal error: unexpected value '%c'"
+                                               "for optopt", (char) optopt );
+                               return EXIT_FAILURE;
+
+               }
+       }
+
+       // If the command line doesn't specify an IDL file, get it
+       // from an environmental variable, or apply a default
+       if( NULL == idl_file_name ) {
+               idl_file_name = getenv( "OILS_IDL_FILENAME" );
+               if( NULL == idl_file_name )
+                       idl_file_name = "/openils/conf/fm_IDL.xml";
+       }
+
+       if( verbose )
+               printf( "IDL file: %s\n", idl_file_name );
+
+       char* loaded_json = NULL;
+       const char* json_query = NULL;
+
+       // Get the JSON query into a string
+       if( query_file_name ) {   // Got a file?  Load it
+               if( optind < argc )
+                       fprintf( stderr, "Extra parameter(s) ignored\n" );
+               loaded_json = load_query( query_file_name );
+               if( !loaded_json )
+                       return EXIT_FAILURE;
+               json_query = loaded_json;
+       } else {                  // No file?  Use command line parameter
+               if ( optind == argc ) {
+                       fprintf( stderr, "No JSON query specified\n" );
+                       return EXIT_FAILURE;
+               } else
+                       json_query = argv[ optind ];
+       }
+
+       if( verbose )
+               printf( "JSON query: %s\n", json_query );
+
+       osrfLogSetLevel( OSRF_LOG_WARNING );    // Suppress informational messages
+       (void) oilsIDLInit( idl_file_name );    // Load IDL into memory
+
+       // Load a database driver, connect to it, and install the connection in
+       // the cstore module.  We don't actually connect to a database, but we
+       // need the driver to process quoted strings correctly.
+       if( dbi_initialize( NULL ) < 0 ) {
+               printf( "Unable to load database driver\n" );
+               return EXIT_FAILURE;
+       };
+
+       dbi_conn conn = dbi_conn_new( "pgsql" );  // change string if ever necessary
+       if( !conn ) {
+               printf( "Unable to establish dbi connection\n" );
+               dbi_shutdown();
+               return EXIT_FAILURE;
+       }
+
+       oilsSetDBConnection( conn );
+
+       // The foregoing is an inelegant kludge.  The true, proper, and uniquely
+       // correct thing to do is to load the system settings and then call
+       // osrfAppInitialize() and osrfAppChildInit().  Maybe we'll actually
+       // do that some day, but this will do for now.
+
+       // Translate the JSON into SQL
+       int rc = test_json_query( json_query );
+
+       dbi_conn_close( conn );
+       dbi_shutdown();
+       if( loaded_json )
+               free( loaded_json );
+
+       return rc ? EXIT_FAILURE : EXIT_SUCCESS;
+}
+
+static int test_json_query( const char* json_query ) {
+
+       jsonObject* hash = jsonParse( json_query );
+       if( !hash ) {
+               fprintf( stderr, "Invalid JSON\n" );
+               return -1;
+       }
+
+       int flags = 0;
+
+       if ( obj_is_true( jsonObjectGetKeyConst( hash, "distinct" )))
+               flags |= SELECT_DISTINCT;
+
+       if ( obj_is_true( jsonObjectGetKeyConst( hash, "no_i18n" )))
+               flags |= DISABLE_I18N;
+
+       char* sql_query = buildQuery( NULL, hash, flags );
+
+       if ( !sql_query ) {
+               fprintf( stderr, "Invalid query\n" );
+               return -1;
+       }
+       else
+               printf( "%s\n", sql_query );
+
+       free( sql_query );
+       jsonObjectFree( hash );
+       return 0;
+}
+
+// Interpret a jsonObject as true or false
+static int obj_is_true( const jsonObject* obj ) {
+       if( !obj )
+               return 0;
+       else switch( obj->type )
+       {
+               case JSON_BOOL :
+                       if( obj->value.b )
+                               return 1;
+                       else
+                               return 0;
+               case JSON_STRING :
+                       if( strcasecmp( obj->value.s, "true" ) )
+                               return 0;
+                       else
+                               return 1;
+                       case JSON_NUMBER :          // Support 1/0 for perl's sake
+                               if( jsonObjectGetNumber( obj ) == 1.0 )
+                                       return 1;
+                               else
+                                       return 0;
+               default :
+                       return 0;
+       }
+}
+
+static char* load_query( const char* filename ) {
+       FILE* fp;
+
+       // Sanity check
+       if( ! filename || ! *filename ) {
+               fprintf( stderr, "Name of query file is empty or missing\n" );
+               return NULL;
+       }
+
+       // Open query file, or use standard input
+       if( ! strcmp( filename, "-" ) )
+               fp = stdin;
+       else {
+               fp = fopen( filename, "r" );
+               if( !fp ) {
+                       fprintf( stderr, "Unable to open query file \"%s\"\n", filename );
+                       return NULL;
+               }
+       }
+
+       // Load file into a growing_buffer
+       size_t num_read;
+       char buf[ BUFSIZ + 1 ];
+       growing_buffer* gb = buffer_init( sizeof( buf ) );
+
+       while( ( num_read = fread( buf, 1, sizeof( buf ) - 1, fp ) ) ) {
+               buf[ num_read ] = '\0';
+               buffer_add( gb, buf );
+       }
+
+       if( fp != stdin )
+               fclose( fp );
+
+       return buffer_release( gb );
+}
diff --git a/src/c/test_qstore.c b/src/c/test_qstore.c
new file mode 100644 (file)
index 0000000..b65ddc7
--- /dev/null
@@ -0,0 +1,651 @@
+/**
+       @file test_qstore.c
+       @brief Test driver for routines to build queries from tables in the query schema.
+
+       This command-line utility exercises most of the code used in the qstore server, but
+       without the complications of sending and receiving OSRF messages.
+
+       Synopsis:
+
+       test_qstore  [options]  query_id
+
+       Query_id is the id of a row in the query.stored_query table, defining a stored query.
+
+       The program reads the specified row in query.stored_query, along with associated rows
+       in other tables, and displays the corresponding query as an SQL command.  Optionally it
+       may execute the query, display the column names of the query result, and/or display the
+       bind variables.
+
+       In order to connect to the database, test_qstore uses various connection parameters
+       that may be specified on the command line.  Any connection parameter not specified
+       reverts to a plausible default.
+
+       The database password may be read from a specified file or entered from the keyboard.
+
+       Options:
+
+       -b  Boolean; Display the name of any bind variables, and their default values.
+
+       -D  Specifies the name of the database driver; defaults to "pgsql".
+
+       -c  Boolean; display column names of the query results, as assigned by PostgreSQL.
+
+       -d  Specifies the database name; defaults to "evergreen".
+
+       -h  Specifies the hostname of the database; defaults to "localhost".
+
+       -i  Specifies the name of the IDL file; defaults to "/openils/conf/fm_IDL.xml".
+
+       -p  Specifies the port number of the database; defaults to 5432.
+
+       -u  Specifies the database user name; defaults to "evergreen".
+
+       -v  Boolean; Run in verbose mode, spewing various detailed messages.  This option is not
+               likely to be useful unless you are troubleshooting the code that loads the stored
+               query.
+
+       -w  Specifies the name of a file containing the database password (no default).
+
+       -x  Boolean: Execute the query and display the results.
+
+       Copyright (C) 2010  Equinox Software Inc.
+       Scott McKellar <scott@esilibrary.com>
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <ctype.h>
+#include <unistd.h>
+#include <termios.h>
+#include <dbi/dbi.h>
+#include "opensrf/utils.h"
+#include "opensrf/string_array.h"
+#include "opensrf/osrf_json.h"
+#include "openils/oils_idl.h"
+#include "openils/oils_buildq.h"
+
+typedef struct {
+       int new_argc;
+       char ** new_argv;
+
+       int   bind;
+       char* driver;
+       int   driver_found;
+       char* database;
+       int   database_found;
+       char* host;
+       int   host_found;
+       char* idl;
+       int   idl_found;
+       unsigned long port;
+       int   port_found;
+       char* user;
+       int   user_found;
+       char* password_file;
+       int   password_file_found;
+       int   verbose;
+       int   columns;
+       int   execute;
+} Opts;
+
+static void show_bind_variables( osrfHash* vars );
+static void show_msgs( const osrfStringArray* sa );
+static dbi_conn connect_db( Opts* opts );
+static int load_pw( growing_buffer* buf, FILE* in );
+static int prompt_password( growing_buffer* buf );
+static void initialize_opts( Opts * pOpts );
+static int get_Opts( int argc, char * argv[], Opts * pOpts );
+
+int main( int argc, char* argv[] ) {
+
+       // Parse the command line
+       printf( "\n" );
+       Opts opts;
+       if( get_Opts( argc, argv, &opts )) {
+               fprintf( stderr, "Unable to parse command line\n" );
+               return EXIT_FAILURE;
+       }
+
+       // Connect to the database
+       dbi_conn dbhandle = connect_db( &opts );
+       if( NULL == dbhandle )
+               return EXIT_FAILURE;
+
+       if( opts.verbose )
+               oilsStoredQSetVerbose();
+
+       osrfLogSetLevel( OSRF_LOG_WARNING );
+
+       // Load the IDL
+       if ( !oilsIDLInit( opts.idl )) {
+               fprintf( stderr, "Unable to load IDL at %s\n", opts.idl );
+               return EXIT_FAILURE;
+       }
+
+       // Load the stored query
+       BuildSQLState* state = buildSQLStateNew( dbhandle );
+       state->defaults_usable = 1;
+       state->values_required = 0;
+       StoredQ* sq = getStoredQuery( state, atoi( opts.new_argv[ 1 ] ));
+
+       if( !sq ) {
+               show_msgs( state->error_msgs );
+               printf( "Unable to build query\n" );
+       } else {
+               // If so requested, show the bind variables
+               if( opts.bind )
+                       show_bind_variables( state->bindvar_list );
+
+               // Build the SQL query
+               if( buildSQL( state, sq )) {
+                       show_msgs( state->error_msgs );
+                       fprintf( stderr, "Unable to build SQL statement\n" );
+               }
+               else {
+                       printf( "%s\n", OSRF_BUFFER_C_STR( state->sql ));
+
+                       // If so requested, get the column names and display them
+                       if( opts.columns ) {
+                               jsonObject* cols = oilsGetColNames( state, sq );
+                               if( cols ) {
+                                       printf( "Column names:\n" );
+                                       char* cols_str = jsonObjectToJSON( cols );
+                                       char* cols_out = jsonFormatString( cols_str );
+                                       printf( "%s\n\n", cols_out );
+                                       free( cols_out );
+                                       free( cols_str );
+                                       jsonObjectFree( cols );
+                               } else
+                                       fprintf( stderr, "Unable to get column names\n\n" );
+                       }
+
+                       // If so requested, execute the query and display the results
+                       if( opts.execute ) {
+                               jsonObject* row = oilsFirstRow( state );
+                               if( state->error ) {
+                                       show_msgs( state->error_msgs );
+                                       fprintf( stderr, "Unable to execute query\n" );
+                               } else {
+                                       printf( "[" );
+                                       int first = 1;         // boolean
+                                       while( row ) {
+
+                                               if( first ) {
+                                                       printf( "\n\t" );
+                                                       first = 0;
+                                               } else
+                                                       printf( ",\n\t" );
+
+                                               char* json = jsonObjectToJSON( row );
+                                               printf( "%s", json );
+                                               free( json );
+                                               row = oilsNextRow( state );
+                                       }
+                                       if( state->error ) {
+                                               show_msgs( state->error_msgs );
+                                               fprintf( stderr, "Unable to fetch row\n" );
+                                       }
+                                       printf( "\n]\n" );
+                               }
+                       }
+               }
+       }
+
+       storedQFree( sq );
+       buildSQLStateFree( state );
+
+       buildSQLCleanup();
+       if ( dbhandle )
+               dbi_conn_close( dbhandle );
+
+       return EXIT_SUCCESS;
+}
+
+/**
+       @brief Display the bind variables.
+       @param vars Pointer to a hash keyed on bind variable name.
+
+       The data for each hash entry is a BindVar, a C struct whose members define the
+       attributes of the bind variable.
+*/
+static void show_bind_variables( osrfHash* vars ) {
+       printf( "Bind variables:\n\n" );
+       BindVar* bind = NULL;
+       osrfHashIterator* iter = osrfNewHashIterator( vars );
+
+       // Traverse the hash of bind variables
+       while(( bind = osrfHashIteratorNext( iter ))) {
+               const char* type = NULL;
+               switch( bind->type ) {
+                       case BIND_STR :
+                               type = "string";
+                               break;
+                       case BIND_NUM :
+                               type = "number";
+                               break;
+                       case BIND_STR_LIST :
+                               type = "string list";
+                               break;
+                       case BIND_NUM_LIST :
+                               type = "number list";
+                               break;
+                       default :
+                               type = "(unrecognized)";
+                               break;
+               }
+
+               // The default and actual values are in the form of jsonObjects.
+               // Transform them back into raw JSON.
+               char* default_value = NULL;
+               if( bind->default_value )
+                       default_value = jsonObjectToJSONRaw( bind->default_value );
+
+               char* actual_value = NULL;
+               if( bind->actual_value )
+                       actual_value = jsonObjectToJSONRaw( bind->actual_value );
+
+               // Display the attributes of the current bind variable.
+               printf( "Name:    %s\n", bind->name );
+               printf( "Label:   %s\n", bind->label );
+               printf( "Type:    %s\n", type );
+               printf( "Desc:    %s\n", bind->description ? bind->description : "(none)" );
+               printf( "Default: %s\n", default_value ? default_value : "(none)" );
+               printf( "Actual:  %s\n", actual_value ? actual_value : "(none)" );
+               printf( "\n" );
+
+               if( default_value )
+                       free( default_value );
+
+               if( actual_value )
+                       free( actual_value );
+       } // end while
+
+       osrfHashIteratorFree( iter );
+}
+
+/**
+       @brief Write a series of strings to standard output.
+       @param sa Array of strings.
+
+       Display messages emitted by the query-building machinery.
+*/
+static void show_msgs( const osrfStringArray* sa ) {
+       if( sa ) {
+               int i;
+               for( i = 0; i < sa->size; ++i ) {
+                       const char* s = osrfStringArrayGetString( sa, i );
+                       if( s )
+                               printf( "%s\n", s );
+               }
+       }
+}
+
+/**
+       @brief Connect to the database.
+       @return If successful, a database handle; otherwise NULL;
+*/
+static dbi_conn connect_db( Opts* opts ) {
+       // Get a database handle
+       dbi_initialize( NULL );
+       dbi_conn dbhandle = dbi_conn_new( opts->driver );
+       if( !dbhandle ) {
+               fprintf( stderr, "Error loading database driver [%s]", opts->driver );
+               return NULL;
+       }
+
+       char* pw = NULL;
+       growing_buffer* buf = buffer_init( 32 );
+
+       // Get the database password, either from a designated file
+       // or from the terminal.
+       if( opts->password_file_found ) {
+               FILE* pwfile = fopen( opts->password_file, "r" );
+               if( !pwfile ) {
+                       fprintf( stderr, "Unable to open password file %s\n", opts->password_file );
+                       buffer_free( buf );
+                       return NULL;
+               } else {
+                       if( load_pw( buf, pwfile )) {
+                               fprintf( stderr, "Unable to load password file %s\n", opts->password_file );
+                               buffer_free( buf );
+                               return NULL;
+                       } else
+                               pw = buffer_release( buf );
+               }
+       } else {
+               if( prompt_password( buf )) {
+                       fprintf( stderr, "Unable to get password\n" );
+                       buffer_free( buf );
+                       return NULL;
+               } else
+                       pw = buffer_release( buf );
+       }
+
+       // Set database connection options
+       dbi_conn_set_option( dbhandle, "host", opts->host );
+       dbi_conn_set_option_numeric( dbhandle, "port", opts->port );
+       dbi_conn_set_option( dbhandle, "username", opts->user );
+       dbi_conn_set_option( dbhandle, "password", pw );
+       dbi_conn_set_option( dbhandle, "dbname", opts->database );
+
+       // Connect to the database
+       const char* err;
+       if( dbi_conn_connect( dbhandle) < 0 ) {
+               sleep( 1 );
+               if ( dbi_conn_connect( dbhandle ) < 0 ) {
+                       dbi_conn_error( dbhandle, &err );
+                       fprintf( stderr, "Error connecting to database: %s", err );
+                       dbi_conn_close( dbhandle );
+                       free( pw );
+                       return NULL;
+               }
+       }
+
+       free( pw );
+       return dbhandle;
+}
+
+/**
+       @brief Load one line from an input stream into a growing_buffer.
+       @param buf Pointer to the receiving buffer.
+       @param in Pointer to the input stream.
+       @return 0 in all cases.  If there's ever a way to fail, return 1 for failure.
+
+       Intended for use in loading a password.
+*/
+static int load_pw( growing_buffer* buf, FILE* in ) {
+       buffer_reset( buf );
+       while( 1 ) {
+               int c = getc( in );
+               if( '\n' == c || EOF == c )
+                       break;
+               else if( '\b' == c )
+                       buffer_chomp( buf );
+               else
+                       OSRF_BUFFER_ADD_CHAR( buf, c );
+       }
+       return 0;
+}
+
+/**
+       @brief Read a password from the terminal, with echo turned off.
+       @param buf Pointer to the receiving buffer.
+       @return 0 if successful, or 1 if not.
+
+       Read from /dev/tty if possible, or from stdin if not.
+*/
+static int prompt_password( growing_buffer* buf ) {
+       struct termios oldterm;
+
+       printf( "Password: " );
+       fflush( stdout );
+
+       FILE* term = fopen( "//dev//tty", "rw" );
+       if( NULL == term )
+               term = stdin;
+
+       // Capture the current state of the terminal
+       if( tcgetattr( fileno( term ), &oldterm ))
+               return 1;
+
+       // Turn off echo
+       struct termios newterm = oldterm;
+       newterm.c_lflag &= ~ECHO;
+       if( tcsetattr( fileno( term ), TCSAFLUSH, &newterm ))
+               return 1;
+
+       // Read the password
+       int rc = load_pw( buf, term );
+
+       // Turn echo back on
+       (void) tcsetattr( fileno( term ), TCSAFLUSH, &oldterm );  // restore echo
+
+       if( term != stdin )
+               fclose( term );
+
+       return rc;
+}
+
+/**
+       @brief Initialize an Opts structure.
+       @param pOpts Pointer to the Opts to be initialized.
+*/
+static void initialize_opts( Opts * pOpts ) {
+       pOpts->new_argc = 0;
+       pOpts->new_argv = NULL;
+
+       pOpts->bind = 0;
+       pOpts->driver_found = 0;
+       pOpts->driver = NULL;
+       pOpts->database_found = 0;
+       pOpts->database = NULL;
+       pOpts->host_found = 0;
+       pOpts->host = NULL;
+       pOpts->idl_found = 0;
+       pOpts->idl = NULL;
+       pOpts->port_found = 0;
+       pOpts->port = 0;
+       pOpts->user_found = 0;
+       pOpts->user = NULL;
+       pOpts->password_file_found = 0;
+       pOpts->password_file = NULL;
+       pOpts->verbose = 0;
+       pOpts->columns = 0;
+       pOpts->execute = 0;
+}
+
+/**
+       @brief Parse the command line.
+       @param argc argc from the command line.
+       @param argv argv from the command line.
+       @param pOpts Pointer to the Opts to be populated.
+       @return Zero if successful, or 1 if not.
+*/
+static int get_Opts( int argc, char * argv[], Opts * pOpts ) {
+       int rc = 0; /* return code */
+       unsigned long port_value = 0;
+       char * tail = NULL;
+       int opt;
+
+       /* Define valid option characters */
+
+       const char optstring[] = ":bD:cd:h:i:p:u:vw:x";
+
+       /* Initialize members of struct */
+
+       initialize_opts( pOpts );
+
+       /* Suppress error messages from getopt() */
+
+       opterr = 0;
+
+       /* Examine command line options */
+
+       while( ( opt = getopt( argc, argv, optstring )) != -1 )
+       {
+               switch( opt )
+               {
+                       case 'b' :   /* Display bind variables */
+                               pOpts->bind = 1;
+                               break;
+                       case 'c' :   /* Display column names */
+                               pOpts->columns = 1;
+                               break;
+                       case 'D' :   /* Get database driver */
+                               if( pOpts->driver_found )
+                               {
+                                       fprintf( stderr, "Only one occurrence of -D option allowed\n" );
+                                       rc = 1;
+                                       break;
+                               }
+                               pOpts->driver_found = 1;
+
+                               pOpts->driver = optarg;
+                               break;
+                       case 'd' :   /* Get database name */
+                               if( pOpts->database_found )
+                               {
+                                       fprintf( stderr, "Only one occurrence of -d option allowed\n" );
+                                       rc = 1;
+                                       break;
+                               }
+                               pOpts->database_found = 1;
+
+                               pOpts->database = optarg;
+                               break;
+                       case 'h' :   /* Get hostname of database */
+                               if( pOpts->host_found )
+                               {
+                                       fprintf( stderr, "Only one occurrence of -h option allowed\n" );
+                                       rc = 1;
+                                       break;
+                               }
+                               pOpts->host_found = 1;
+
+                               pOpts->host = optarg;
+                               break;
+                       case 'i' :   /* Get name of IDL file */
+                               if( pOpts->idl_found )
+                               {
+                                       fprintf( stderr, "Only one occurrence of -i option allowed\n" );
+                                       rc = 1;
+                                       break;
+                               }
+                               pOpts->idl_found = 1;
+
+                               pOpts->idl = optarg;
+                               break;
+                       case 'p' :   /* Get port number of database */
+                               if( pOpts->port_found )
+                               {
+                                       fprintf( stderr, "Only one occurrence of -p option allowed\n" );
+                                       rc = 1;
+                                       break;
+                               }
+                               pOpts->port_found = 1;
+
+                               /* Skip white space; check for negative */
+
+                               while( isspace( (unsigned char) *optarg ))
+                                       ++optarg;
+
+                               if( '-' == *optarg )
+                               {
+                                       fprintf( stderr, "Negative argument not allowed for "
+                                               "-p option: \"%s\"\n", optarg );
+                                       rc = 1;
+                                       break;
+                               }
+
+                               /* Convert to numeric value */
+
+                               errno = 0;
+                               port_value = strtoul( optarg, &tail, 10 );
+                               if( *tail != '\0' )
+                               {
+                                       fprintf( stderr, "Invalid or non-numeric argument "
+                                                       "to -p option: \"%s\"\n", optarg );
+                                       rc = 1;
+                                       break;
+                               }
+                               else if( errno != 0 )
+                               {
+                                       fprintf( stderr, "Too large argument "
+                                               "to -p option: \"%s\"\n", optarg );
+                                       rc = 1;
+                                       break;
+                               }
+
+                               pOpts->port = port_value;
+                               break;
+                       case 'u' :   /* Get username of database account */
+                               if( pOpts->user_found )
+                               {
+                                       fprintf( stderr, "Only one occurrence of -u option allowed\n" );
+                                       rc = 1;
+                                       break;
+                               }
+                               pOpts->user_found = 1;
+
+                               pOpts->user = optarg;
+                               break;
+                       case 'v' :   /* Set verbose mode */
+                               pOpts->verbose = 1;
+                               break;
+                       case 'w' :   /* Get name of password_file */
+                               if( pOpts->password_file_found )
+                               {
+                                       fprintf( stderr, "Only one occurrence of -w option allowed\n" );
+                                       rc = 1;
+                                       break;
+                               }
+                               pOpts->password_file_found = 1;
+
+                               pOpts->password_file = optarg;
+                               break;
+                       case 'x' :   /* Set execute */
+                               pOpts->execute = 1;
+                               break;
+                       case ':' : /* Missing argument */
+                               fprintf( stderr, "Required argument missing on -%c option\n",
+                                        (char) optopt );
+                               rc = 1;
+                               break;
+                       case '?' : /* Invalid option */
+                               fprintf( stderr, "Invalid option '-%c' on command line\n",
+                                       (char) optopt );
+                               rc = 1;
+                               break;
+                       default :  /* Programmer error */
+                               fprintf( stderr, "Internal error: unexpected value '-%c'"
+                                               "for optopt", (char) optopt );
+                               rc = 1;
+                               break;
+               } /* end switch */
+       } /* end while */
+
+       /* See if required options were supplied; apply defaults */
+
+       if( ! pOpts->driver_found )
+               pOpts->driver = "pgsql";
+
+       if( ! pOpts->database_found )
+               pOpts->database = "evergreen";
+
+       if( ! pOpts->host_found )
+               pOpts->host = "localhost";
+
+       if( ! pOpts->idl_found )
+               pOpts->idl = "/openils/conf/fm_IDL.xml";
+
+       if( ! pOpts->port_found )
+               pOpts->port = 5432;
+
+       if( ! pOpts->user_found )
+               pOpts->user = "evergreen";
+
+       if( optind > argc )
+       {
+               /* This should never happen! */
+
+               fprintf( stderr, "Program error: found more arguments than expected\n" );
+               rc = 1;
+       }
+       else
+       {
+               /* Calculate new_argcv and new_argc to reflect */
+               /* the number of arguments consumed */
+
+               pOpts->new_argc = argc - optind + 1;
+               pOpts->new_argv = argv + optind - 1;
+
+               if( pOpts->new_argc < 2UL )
+               {
+                       fprintf( stderr, "Not enough arguments beyond options; must be at least 1\n" );
+                       rc = 1;
+               }
+       }
+
+       return rc;
+}