]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/c-apps/oils_qstore.c
LP1642337: Reporter Boolean Filters
[Evergreen.git] / Open-ILS / src / c-apps / oils_qstore.c
1 /**
2         @file oils_qstore.c
3         @brief As a server, perform database queries as defined in the database itself.
4 */
5
6 #include <stdlib.h>
7 #include <string.h>
8 #include <ctype.h>
9 #include <dbi/dbi.h>
10 #include "opensrf/utils.h"
11 #include "opensrf/log.h"
12 #include "opensrf/osrf_json.h"
13 #include "opensrf/osrf_application.h"
14 #include "openils/oils_utils.h"
15 #include "openils/oils_sql.h"
16 #include "openils/oils_buildq.h"
17
18 /**
19         @brief Information about a previously prepared query.
20
21         We store an osrfHash of CachedQueries in the userData area of the application session,
22         keyed on query token.  That way we can fetch what a previous call to the prepare method
23         has prepared.
24 */
25 typedef struct {
26         BuildSQLState* state;
27         StoredQ*       query;
28 } CachedQuery;
29
30 static dbi_conn dbhandle; /* our db connection */
31
32 static const char modulename[] = "open-ils.qstore";
33
34 int doPrepare( osrfMethodContext* ctx );
35 int doExecute( osrfMethodContext* ctx );
36 int doSql( osrfMethodContext* ctx );
37
38 static const char* save_query(
39         osrfMethodContext* ctx, BuildSQLState* state, StoredQ* query );
40 static void free_cached_query( char* key, void* data );
41 static void userDataFree( void* blob );
42 static CachedQuery* search_token( osrfMethodContext* ctx, const char* token );
43
44 /**
45         @brief Disconnect from the database.
46
47         This function is called when the server drone is about to terminate.
48 */
49 void osrfAppChildExit() {
50         osrfLogDebug( OSRF_LOG_MARK, "Child is exiting, disconnecting from database..." );
51
52         if ( dbhandle ) {
53                 dbi_conn_query( dbhandle, "ROLLBACK;" );
54                 dbi_conn_close( dbhandle );
55                 dbhandle = NULL;
56         }
57 }
58
59 /**
60         @brief Initialize the application.
61         @return Zero if successful, or non-zero if not.
62
63         Load the IDL file into an internal data structure for future reference.  Each non-virtual
64         class in the IDL corresponds to a table or view in the database, or to a subquery defined
65         in the IDL.  Ignore all virtual tables and virtual fields.
66
67         Register the functions for remote procedure calls.
68
69         This function is called when the registering the application, and is executed by the
70         listener before spawning the drones.
71 */
72 int osrfAppInitialize() {
73
74         osrfLogInfo( OSRF_LOG_MARK, "Initializing the QStore Server..." );
75         osrfLogInfo( OSRF_LOG_MARK, "Finding XML file..." );
76
77         if ( !oilsIDLInit( osrf_settings_host_value( "/IDL" )))
78                 return 1; /* return non-zero to indicate error */
79
80         // Set the SQL options.  Here the second and third parameters are irrelevant, but we need
81         // to set the module name for use in error messages.
82         oilsSetSQLOptions( modulename, 0, 100 );
83
84         growing_buffer* method_name = buffer_init( 64 );
85
86         OSRF_BUFFER_ADD( method_name, modulename );
87         OSRF_BUFFER_ADD( method_name, ".prepare" );
88         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
89                 "doPrepare", "", 1, 0 );
90
91         buffer_reset( method_name );
92         OSRF_BUFFER_ADD( method_name, modulename );
93         OSRF_BUFFER_ADD( method_name, ".columns" );
94         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
95                 "doColumns", "", 1, 0 );
96
97         buffer_reset( method_name );
98         OSRF_BUFFER_ADD( method_name, modulename );
99         OSRF_BUFFER_ADD( method_name, ".param_list" );
100         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
101                 "doParamList", "", 1, 0 );
102
103         buffer_reset( method_name );
104         OSRF_BUFFER_ADD( method_name, modulename );
105         OSRF_BUFFER_ADD( method_name, ".bind_param" );
106         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
107                 "doBindParam", "", 2, 0 );
108
109         buffer_reset( method_name );
110         OSRF_BUFFER_ADD( method_name, modulename );
111         OSRF_BUFFER_ADD( method_name, ".execute" );
112         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
113                 "doExecute", "", 1, OSRF_METHOD_STREAMING );
114
115         buffer_reset( method_name );
116         OSRF_BUFFER_ADD( method_name, modulename );
117         OSRF_BUFFER_ADD( method_name, ".sql" );
118         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
119                 "doSql", "", 1, OSRF_METHOD_STREAMING );
120
121         buffer_reset( method_name );
122         OSRF_BUFFER_ADD( method_name, modulename );
123         OSRF_BUFFER_ADD( method_name, ".finish" );
124         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
125                 "doFinish", "", 1, 0 );
126
127         buffer_reset( method_name );
128         OSRF_BUFFER_ADD( method_name, modulename );
129         OSRF_BUFFER_ADD( method_name, ".messages" );
130         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
131                 "doMessages", "", 1, 0 );
132
133         return 0;
134 }
135
136 /**
137         @brief Initialize a server drone.
138         @return Zero if successful, -1 if not.
139
140         Connect to the database.  For each non-virtual class in the IDL, execute a dummy "SELECT * "
141         query to get the datatype of each column.  Record the datatypes in the loaded IDL.
142
143         This function is called by a server drone shortly after it is spawned by the listener.
144 */
145 int osrfAppChildInit( void ) {
146
147         dbhandle = oilsConnectDB( modulename );
148         if( !dbhandle )
149                 return -1;
150         else {
151                 oilsSetDBConnection( dbhandle );
152                 osrfLogInfo( OSRF_LOG_MARK, "%s successfully connected to the database", modulename );
153
154                 // Apply datatypes from database to the fields in the IDL
155                 //if( oilsExtendIDL() ) {
156                 //      osrfLogError( OSRF_LOG_MARK, "Error extending the IDL" );
157                 //      return -1;
158                 //}
159                 //else
160                 return 0;
161         }
162 }
163
164 /**
165         @brief Load a specified query from the database query tables.
166         @param ctx Pointer to the current method context.
167         @return Zero if successful, or -1 if not.
168
169         Method parameters:
170         - query id (key of query.stored_query table)
171
172         Returns: a hash with two entries:
173         - "token": A character string serving as a token for future references to the query.
174         - "bind_variables" A hash of bind variables; see notes for doParamList().
175 */
176 int doPrepare( osrfMethodContext* ctx ) {
177         if(osrfMethodVerifyContext( ctx )) {
178                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
179                 return -1;
180         }
181
182         // Get the query id from a method parameter
183         const jsonObject* query_id_obj = jsonObjectGetIndex( ctx->params, 0 );
184         if( query_id_obj->type != JSON_NUMBER ) {
185                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
186                         ctx->request, "Invalid parameter; query id must be a number" );
187                 return -1;
188         }
189
190         int query_id = atoi( jsonObjectGetString( query_id_obj ));
191         if( query_id <= 0 ) {
192                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
193                         ctx->request, "Invalid parameter: query id must be greater than zero" );
194                 return -1;
195         }
196
197         osrfLogInfo( OSRF_LOG_MARK, "Loading query for id # %d", query_id );
198
199         BuildSQLState* state = buildSQLStateNew( dbhandle );
200         state->defaults_usable = 1;
201         state->values_required = 0;
202         StoredQ* query = getStoredQuery( state, query_id );
203         if( state->error ) {
204                 osrfLogWarning( OSRF_LOG_MARK, "Unable to load stored query # %d", query_id );
205                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
206                         ctx->request, "Unable to load stored query" );
207                 if( state->panic ) {
208                         osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state, 
209                                 "Database connection isn't working" ));
210                         osrfAppSessionPanic( ctx->session );
211                 }
212                 return -1;
213         }
214
215         const char* token = save_query( ctx, state, query );
216
217         osrfLogInfo( OSRF_LOG_MARK, "Token for query id # %d is \"%s\"", query_id, token );
218
219         // Build an object to return.  It will be a hash containing the query token and a
220         // list of bind variables.
221         jsonObject* returned_obj = jsonNewObjectType( JSON_HASH );
222         jsonObjectSetKey( returned_obj, "token", jsonNewObject( token ));
223         jsonObjectSetKey( returned_obj, "bind_variables",
224                 oilsBindVarList( state->bindvar_list ));
225
226         osrfAppRespondComplete( ctx, returned_obj );
227         return 0;
228 }
229
230 /**
231         @brief Return a list of column names for the SELECT list.
232         @param ctx Pointer to the current method context.
233         @return Zero if successful, or -1 if not.
234
235         Method parameters:
236         - query token, as previously returned by the .prepare method.
237
238         Returns: An array of column names; unavailable names are represented as nulls.
239 */
240 int doColumns( osrfMethodContext* ctx ) {
241         if(osrfMethodVerifyContext( ctx )) {
242                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
243                 return -1;
244         }
245
246         // Get the query token from a method parameter
247         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
248         if( token_obj->type != JSON_STRING ) {
249                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
250                         ctx->request, "Invalid parameter; query token must be a string" );
251                 return -1;
252         }
253         const char* token = jsonObjectGetString( token_obj );
254
255         // Look up the query token in the session-level userData
256         CachedQuery* query = search_token( ctx, token );
257         if( !query ) {
258                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
259                         ctx->request, "Invalid query token" );
260                 return -1;
261         }
262
263         osrfLogInfo( OSRF_LOG_MARK, "Listing column names for token %s", token );
264
265         jsonObject* col_list = oilsGetColNames( query->state, query->query );
266         if( query->state->error ) {
267                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
268                         ctx->request, "Unable to get column names" );
269                 if( query->state->panic ) {
270                         osrfLogError( OSRF_LOG_MARK, sqlAddMsg( query->state,
271                                 "Database connection isn't working" ));
272                         osrfAppSessionPanic( ctx->session );
273                 }
274                 return -1;
275         } else {
276                 osrfAppRespondComplete( ctx, col_list );
277                 return 0;
278         }
279 }
280
281 /**
282         @brief Implement the param_list method.
283         @param ctx Pointer to the current method context.
284         @return Zero if successful, or -1 if not.
285
286         Provide a list of bind variables for a specified query, along with their various
287         attributes.
288
289         Method parameters:
290         - query token, as previously returned by the .prepare method.
291
292         Returns: A (possibly empty) JSON_HASH, keyed on the names of the bind variables.
293         The data for each is another level of JSON_HASH with a fixed set of tags:
294         - "label"
295         - "type"
296         - "description"
297         - "default_value" (as a jsonObject)
298         - "actual_value" (as a jsonObject)
299
300         Any non-existent values are represented as JSON_NULLs.
301 */
302 int doParamList( osrfMethodContext* ctx ) {
303         if(osrfMethodVerifyContext( ctx )) {
304                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
305                 return -1;
306         }
307
308         // Get the query token from a method parameter
309         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
310         if( token_obj->type != JSON_STRING ) {
311                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
312                         ctx->request, "Invalid parameter; query token must be a string" );
313                 return -1;
314         }
315         const char* token = jsonObjectGetString( token_obj );
316
317         // Look up the query token in the session-level userData
318         CachedQuery* query = search_token( ctx, token );
319         if( !query ) {
320                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
321                         ctx->request, "Invalid query token" );
322                 return -1;
323         }
324
325         osrfLogInfo( OSRF_LOG_MARK, "Returning list of bind variables for token %s", token );
326
327         osrfAppRespondComplete( ctx, oilsBindVarList( query->state->bindvar_list ) );
328         return 0;
329 }
330
331 /**
332         @brief Implement the bind_param method.
333         @param ctx Pointer to the current method context.
334         @return Zero if successful, or -1 if not.
335
336         Apply values to bind variables, overriding the defaults, if any.
337
338         Method parameters:
339         - query token, as previously returned by the .prepare method.
340         - hash of bind variable values, keyed on bind variable names.
341
342         Returns: Nothing.
343 */
344 int doBindParam( osrfMethodContext* ctx ) {
345         if(osrfMethodVerifyContext( ctx )) {
346                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
347                 return -1;
348         }
349
350         // Get the query token from a method parameter
351         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
352         if( token_obj->type != JSON_STRING ) {
353                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
354                         ctx->request, "Invalid parameter; query token must be a string" );
355                 return -1;
356         }
357         const char* token = jsonObjectGetString( token_obj );
358
359         // Look up the query token in the session-level userData
360         CachedQuery* query = search_token( ctx, token );
361         if( !query ) {
362                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
363                         ctx->request, "Invalid query token" );
364                 return -1;
365         }
366
367         osrfLogInfo( OSRF_LOG_MARK, "Binding parameter(s) for token %s", token );
368
369         jsonObject* bindings = jsonObjectGetIndex( ctx->params, 1 );
370         if( !bindings ) {
371                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
372                         ctx->request, "No parameter provided for bind variable values" );
373                 return -1;
374         } else if( bindings->type != JSON_HASH ) {
375                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
376                         ctx->request, "Invalid parameter for bind variable values: not a hash" );
377                 return -1;
378         }
379
380         if( 0 == bindings->size ) {
381                 // No values to assign; we're done.
382                 osrfAppRespondComplete( ctx, NULL );
383                 return 0;
384         }
385
386         osrfHash* bindvar_list = query->state->bindvar_list;
387         if( !bindvar_list || osrfHashGetCount( bindvar_list ) == 0 ) {
388                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
389                         ctx->request, "There are no bind variables to which to assign values" );
390                 return -1;
391         }
392
393         if( oilsApplyBindValues( query->state, bindings )) {
394                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
395                         ctx->request, "Unable to apply values to bind variables" );
396                 return -1;
397         } else {
398                 osrfAppRespondComplete( ctx, NULL );
399                 return 0;
400         }
401 }
402
403 /**
404         @brief Execute an SQL query and return a result set.
405         @param ctx Pointer to the current method context.
406         @return Zero if successful, or -1 if not.
407
408         Method parameters:
409         - query token, as previously returned by the .prepare method.
410
411         Returns: A series of responses, each of them a row represented as an array of column values.
412 */
413 int doExecute( osrfMethodContext* ctx ) {
414         if(osrfMethodVerifyContext( ctx )) {
415                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
416                 return -1;
417         }
418
419         // Get the query token
420         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
421         if( token_obj->type != JSON_STRING ) {
422                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
423                         ctx->request, "Invalid parameter; query token must be a string" );
424                 return -1;
425         }
426         const char* token = jsonObjectGetString( token_obj );
427
428         // Look up the query token in the session-level userData
429         CachedQuery* query = search_token( ctx, token );
430         if( !query ) {
431                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
432                         ctx->request, "Invalid query token" );
433                 return -1;
434         }
435
436         osrfLogInfo( OSRF_LOG_MARK, "Executing query for token \"%s\"", token );
437         if( query->state->error ) {
438                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
439                         "No valid prepared query available for query id # %d", query->query->id ));
440                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
441                                                           ctx->request, "No valid prepared query available" );
442                 return -1;
443         } else if( buildSQL( query->state, query->query )) {
444                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
445                         "Unable to build SQL statement for query id # %d", query->query->id ));
446                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
447                         ctx->request, "Unable to build SQL statement" );
448                 return -1;
449         }
450
451         jsonObject* row = oilsFirstRow( query->state );
452         while( row ) {
453                 osrfAppRespond( ctx, row );
454                 row = oilsNextRow( query->state );
455         }
456
457         if( query->state->error ) {
458                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
459                         "Unable to execute SQL statement for query id # %d", query->query->id ));
460                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
461                         ctx->request, "Unable to execute SQL statement" );
462                 if( query->state->panic ) {
463                         osrfLogError( OSRF_LOG_MARK, sqlAddMsg( query->state,
464                                 "Database connection isn't working" ));
465                         osrfAppSessionPanic( ctx->session );
466                 }
467                 return -1;
468         }
469
470         osrfAppRespondComplete( ctx, NULL );
471         return 0;
472 }
473
474 /**
475         @brief Construct an SQL query, but without executing it.
476         @param ctx Pointer to the current method context.
477         @return Zero if successful, or -1 if not.
478
479         Method parameters:
480         - query token, as previously returned by the .prepare method.
481
482         Returns: A string containing an SQL query..
483 */
484 int doSql( osrfMethodContext* ctx ) {
485         if(osrfMethodVerifyContext( ctx )) {
486                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
487                 return -1;
488         }
489
490         // Get the query token
491         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
492         if( token_obj->type != JSON_STRING ) {
493                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
494                         ctx->request, "Invalid parameter; query token must be a string" );
495                 return -1;
496         }
497         const char* token = jsonObjectGetString( token_obj );
498
499         // Look up the query token in the session-level userData
500         CachedQuery* query = search_token( ctx, token );
501         if( !query ) {
502                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
503                         ctx->request, "Invalid query token" );
504                 return -1;
505         }
506
507         osrfLogInfo( OSRF_LOG_MARK, "Returning SQL for token \"%s\"", token );
508         if( query->state->error ) {
509                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
510                         "No valid prepared query available for query id # %d", query->query->id ));
511                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
512                         ctx->request, "No valid prepared query available" );
513                 return -1;
514         } else if( buildSQL( query->state, query->query )) {
515                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
516                         "Unable to build SQL statement for query id # %d", query->query->id ));
517                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
518                         ctx->request, "Unable to build SQL statement" );
519                 return -1;
520         }
521
522         osrfAppRespondComplete( ctx, jsonNewObject( OSRF_BUFFER_C_STR( query->state->sql )));
523         return 0;
524 }
525
526 /**
527         @brief Return a list of previously generated error messages for a specified query.
528         @param ctx Pointer to the current method context.
529         @return Zero if successful, or -1 if not.
530
531         Method parameters:
532         - query token, as previously returned by the .prepare method.
533
534         Returns: A (possibly empty) array of strings, each one an error message generated during
535         previous operations in connection with the specified query.
536 */
537 int doMessages( osrfMethodContext* ctx ) {
538         if(osrfMethodVerifyContext( ctx )) {
539                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
540                 return -1;
541         }
542
543         // Get the query token from a method parameter
544         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
545         if( token_obj->type != JSON_STRING ) {
546                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
547                         ctx->request, "Invalid parameter; query token must be a string" );
548                 return -1;
549         }
550         const char* token = jsonObjectGetString( token_obj );
551
552         // Look up the query token in the session-level userData
553         CachedQuery* query = search_token( ctx, token );
554         if( !query ) {
555                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
556                         ctx->request, "Invalid query token" );
557                 return -1;
558         }
559
560         osrfLogInfo( OSRF_LOG_MARK, "Returning messages for token %s", token );
561
562         jsonObject* msgs = jsonNewObjectType( JSON_ARRAY );
563         const osrfStringArray* error_msgs = query->state->error_msgs;
564         int i;
565         for( i = 0; i < error_msgs->size; ++i ) {
566                 jsonObject* msg = jsonNewObject( osrfStringArrayGetString( error_msgs, i ));
567                 jsonObjectPush( msgs, msg );
568         }
569
570         osrfAppRespondComplete( ctx, msgs );
571         return 0;
572 }
573
574 /**
575         @brief Discard a previously stored query, as identified by a token.
576         @param ctx Pointer to the current method context.
577         @return Zero if successful, or -1 if not.
578
579         Method parameters:
580         - query token, as previously returned by the .prepare method.
581
582         Returns: Nothing.
583 */
584 int doFinish( osrfMethodContext* ctx ) {
585         if(osrfMethodVerifyContext( ctx )) {
586                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
587                 return -1;
588         }
589
590         // Get the query token.
591         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
592         if( token_obj->type != JSON_STRING ) {
593                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
594                                                           ctx->request, "Invalid parameter; query token must be a string" );
595                 return -1;
596         }
597         const char* token = jsonObjectGetString( token_obj );
598
599         // Delete the corresponding entry from the cache.  If there is no cache, or no such entry,
600         // just ignore the problem and report success.
601         osrfHash* cache = ctx->session->userData;
602         if( cache )
603                 osrfHashRemove( cache, token );
604
605         osrfAppRespondComplete( ctx, NULL );
606         return 0;
607 }
608
609 /**
610         @brief Save a query in session-level userData for reference in future method calls.
611         @param ctx Pointer to the current method context.
612         @param state Pointer to the state of the query.
613         @param query Pointer to the abstract representation of the query.
614         @return Pointer to an identifying token to be returned to the client.
615 */
616 static const char* save_query(
617         osrfMethodContext* ctx, BuildSQLState* state, StoredQ* query ) {
618
619         CachedQuery* cached_query = safe_malloc( sizeof( CachedQuery ));
620         cached_query->state       = state;
621         cached_query->query       = query;
622
623         // Get the cache.  If we don't have one yet, make one.
624         osrfHash* cache = ctx->session->userData;
625         if( !cache ) {
626                 cache = osrfNewHash();
627                 osrfHashSetCallback( cache, free_cached_query );
628                 ctx->session->userData = cache;
629                 ctx->session->userDataFree = userDataFree;  // arrange to free it at end of session
630         }
631
632         // Create a token string to be used as a key
633         static unsigned int token_count = 0;
634         char* token = va_list_to_string(
635                 "%u_%ld_%ld", ++token_count, (long) time( NULL ), (long) getpid() );
636
637         osrfHashSet( cache, cached_query, token );
638         return token;
639 }
640
641 /**
642         @brief Free a CachedQuery
643         @param Pointer to the CachedQuery to be freed.
644 */
645 static void free_cached_query( char* key, void* data ) {
646         if( data ) {
647                 CachedQuery* cached_query = data;
648                 buildSQLStateFree( cached_query->state );
649                 storedQFree( cached_query->query );
650         }
651 }
652
653 /**
654         @brief Callback for freeing session-level userData.
655         @param blob Opaque pointer t userData.
656 */
657 static void userDataFree( void* blob ) {
658         osrfHashFree( (osrfHash*) blob );
659 }
660
661 /**
662         @brief Search for the cached query corresponding to a given token.
663         @param ctx Pointer to the current method context.
664         @param token Token string from a previous call to the prepare method.
665         @return A pointer to the cached query, if found, or NULL if not.
666 */
667 static CachedQuery* search_token( osrfMethodContext* ctx, const char* token ) {
668         if( ctx && ctx->session->userData && token ) {
669                 osrfHash* cache = ctx->session->userData;
670                 return osrfHashGet( cache, token );
671         } else
672                 return NULL;
673 }