]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/c-apps/oils_qstore.c
Implement new param_list method, which returns a list of
[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 character string serving as a token for future references to the query.
173
174         NB: the method return type is temporary.  Eventually this method will return both a token
175         and a list of bind variables.
176 */
177 int doPrepare( osrfMethodContext* ctx ) {
178         if(osrfMethodVerifyContext( ctx )) {
179                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
180                 return -1;
181         }
182
183         // Get the query id from a method parameter
184         const jsonObject* query_id_obj = jsonObjectGetIndex( ctx->params, 0 );
185         if( query_id_obj->type != JSON_NUMBER ) {
186                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
187                         ctx->request, "Invalid parameter; query id must be a number" );
188                 return -1;
189         }
190
191         int query_id = atoi( jsonObjectGetString( query_id_obj ));
192         if( query_id <= 0 ) {
193                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
194                         ctx->request, "Invalid parameter: query id must be greater than zero" );
195                 return -1;
196         }
197
198         osrfLogInfo( OSRF_LOG_MARK, "Loading query for id # %d", query_id );
199
200         BuildSQLState* state = buildSQLStateNew( dbhandle );
201         state->defaults_usable = 1;
202         state->values_required = 0;
203         StoredQ* query = getStoredQuery( state, query_id );
204         if( state->error ) {
205                 osrfLogWarning( OSRF_LOG_MARK, "Unable to load stored query # %d", query_id );
206                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
207                         ctx->request, "Unable to load stored query" );
208                 return -1;
209         }
210
211         const char* token = save_query( ctx, state, query );
212
213         osrfLogInfo( OSRF_LOG_MARK, "Token for query id # %d is \"%s\"", query_id, token );
214
215         osrfAppRespondComplete( ctx, jsonNewObject( token ));
216         return 0;
217 }
218
219 /**
220         @brief Return a list of column names for the SELECT list.
221         @param ctx Pointer to the current method context.
222         @return Zero if successful, or -1 if not.
223
224         Method parameters:
225         - query token, as previously returned by the .prepare method.
226
227         Returns: An array of column names; unavailable names are represented as nulls.
228 */
229 int doColumns( osrfMethodContext* ctx ) {
230         if(osrfMethodVerifyContext( ctx )) {
231                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
232                 return -1;
233         }
234
235         // Get the query token from a method parameter
236         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
237         if( token_obj->type != JSON_STRING ) {
238                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
239                         ctx->request, "Invalid parameter; query token must be a string" );
240                 return -1;
241         }
242         const char* token = jsonObjectGetString( token_obj );
243
244         // Look up the query token in the session-level userData
245         CachedQuery* query = search_token( ctx, token );
246         if( !query ) {
247                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
248                         ctx->request, "Invalid query token" );
249                 return -1;
250         }
251
252         osrfLogInfo( OSRF_LOG_MARK, "Listing column names for token %s", token );
253         
254         jsonObject* col_list = oilsGetColNames( query->state, query->query );
255         if( query->state->error ) {
256                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
257                         ctx->request, "Unable to get column names" );
258                 return -1;
259         } else {
260                 osrfAppRespondComplete( ctx, col_list );
261                 return 0;
262         }
263 }
264
265 int doParamList( osrfMethodContext* ctx ) {
266         if(osrfMethodVerifyContext( ctx )) {
267                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
268                 return -1;
269         }
270
271         // Get the query token from a method parameter
272         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
273         if( token_obj->type != JSON_STRING ) {
274                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
275                         ctx->request, "Invalid parameter; query token must be a string" );
276                 return -1;
277         }
278         const char* token = jsonObjectGetString( token_obj );
279
280         // Look up the query token in the session-level userData
281         CachedQuery* query = search_token( ctx, token );
282         if( !query ) {
283                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
284                         ctx->request, "Invalid query token" );
285                 return -1;
286         }
287
288         osrfLogInfo( OSRF_LOG_MARK, "Returning list of bind variables for token %s", token );
289
290         osrfAppRespondComplete( ctx, oilsBindVarList( query->state->bindvar_list ) );
291         return 0;
292 }
293
294 /**
295         @brief Implement the bind_param method.
296         @param ctx Pointer to the current method context.
297         @return Zero if successful, or -1 if not.
298
299         Method parameters:
300         - query token, as previously returned by the .prepare method.
301         - hash of bind variable values, keyed on bind variable names.
302
303         Returns: Nothing.
304 */
305 int doBindParam( osrfMethodContext* ctx ) {
306         if(osrfMethodVerifyContext( ctx )) {
307                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
308                 return -1;
309         }
310
311         // Get the query token from a method parameter
312         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
313         if( token_obj->type != JSON_STRING ) {
314                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
315                         ctx->request, "Invalid parameter; query token must be a string" );
316                 return -1;
317         }
318         const char* token = jsonObjectGetString( token_obj );
319
320         // Look up the query token in the session-level userData
321         CachedQuery* query = search_token( ctx, token );
322         if( !query ) {
323                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
324                         ctx->request, "Invalid query token" );
325                 return -1;
326         }
327
328         osrfLogInfo( OSRF_LOG_MARK, "Binding parameter(s) for token %s", token );
329
330         jsonObject* bindings = jsonObjectGetIndex( ctx->params, 1 );
331         if( !bindings ) {
332                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
333                         ctx->request, "No parameter provided for bind variable values" );
334                 return -1;
335         } else if( bindings->type != JSON_HASH ) {
336                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
337                         ctx->request, "Invalid parameter for bind variable values: not a hash" );
338                 return -1;
339         }
340
341         if( 0 == bindings->size ) {
342                 // No values to assign; we're done.
343                 osrfAppRespondComplete( ctx, NULL );
344                 return 0;
345         }
346
347         osrfHash* bindvar_list = query->state->bindvar_list;
348         if( !bindvar_list || osrfHashGetCount( bindvar_list ) == 0 ) {
349                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
350                         ctx->request, "There are no bind variables to which to assign values" );
351                 return -1;
352         }
353
354         if( oilsApplyBindValues( query->state, bindings )) {
355                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
356                         ctx->request, "Unable to apply values to bind variables" );
357                 return -1;
358         } else {
359                 osrfAppRespondComplete( ctx, NULL );
360                 return 0;
361         }
362 }
363
364 /**
365         @brief Execute an SQL query and return a result set.
366         @param ctx Pointer to the current method context.
367         @return Zero if successful, or -1 if not.
368
369         Method parameters:
370         - query token, as previously returned by the .prepare method.
371
372         Returns: A series of responses, each of them a row represented as an array of column values.
373 */
374 int doExecute( osrfMethodContext* ctx ) {
375         if(osrfMethodVerifyContext( ctx )) {
376                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
377                 return -1;
378         }
379
380         // Get the query token
381         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
382         if( token_obj->type != JSON_STRING ) {
383                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
384                         ctx->request, "Invalid parameter; query token must be a string" );
385                 return -1;
386         }
387         const char* token = jsonObjectGetString( token_obj );
388
389         // Look up the query token in the session-level userData
390         CachedQuery* query = search_token( ctx, token );
391         if( !query ) {
392                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
393                         ctx->request, "Invalid query token" );
394                 return -1;
395         }
396
397         osrfLogInfo( OSRF_LOG_MARK, "Executing query for token \"%s\"", token );
398         if( query->state->error ) {
399                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
400                         "No valid prepared query available for query id # %d", query->query->id ));
401                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
402                                                           ctx->request, "No valid prepared query available" );
403                 return -1;
404         } else if( buildSQL( query->state, query->query )) {
405                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
406                         "Unable to build SQL statement for query id # %d", query->query->id ));
407                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
408                         ctx->request, "Unable to build SQL statement" );
409                 return -1;
410         }
411
412         jsonObject* row = oilsFirstRow( query->state );
413         while( row ) {
414                 osrfAppRespond( ctx, row );
415                 row = oilsNextRow( query->state );
416         }
417
418         if( query->state->error ) {
419                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
420                         "Unable to execute SQL statement for query id # %d", query->query->id ));
421                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
422                         ctx->request, "Unable to execute SQL statement" );
423                 return -1;
424         }
425
426         osrfAppRespondComplete( ctx, NULL );
427         return 0;
428 }
429
430 /**
431         @brief Construct an SQL query, but without executing it.
432         @param ctx Pointer to the current method context.
433         @return Zero if successful, or -1 if not.
434
435         Method parameters:
436         - query token, as previously returned by the .prepare method.
437
438         Returns: A string containing an SQL query..
439 */
440 int doSql( osrfMethodContext* ctx ) {
441         if(osrfMethodVerifyContext( ctx )) {
442                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
443                 return -1;
444         }
445
446         // Get the query token
447         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
448         if( token_obj->type != JSON_STRING ) {
449                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
450                         ctx->request, "Invalid parameter; query token must be a string" );
451                 return -1;
452         }
453         const char* token = jsonObjectGetString( token_obj );
454
455         // Look up the query token in the session-level userData
456         CachedQuery* query = search_token( ctx, token );
457         if( !query ) {
458                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
459                         ctx->request, "Invalid query token" );
460                 return -1;
461         }
462
463         osrfLogInfo( OSRF_LOG_MARK, "Returning SQL for token \"%s\"", token );
464         if( query->state->error ) {
465                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
466                         "No valid prepared query available for query id # %d", query->query->id ));
467                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
468                         ctx->request, "No valid prepared query available" );
469                 return -1;
470         } else if( buildSQL( query->state, query->query )) {
471                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
472                         "Unable to build SQL statement for query id # %d", query->query->id ));
473                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
474                         ctx->request, "Unable to build SQL statement" );
475                 return -1;
476         }
477
478         osrfAppRespondComplete( ctx, jsonNewObject( OSRF_BUFFER_C_STR( query->state->sql )));
479         return 0;
480 }
481
482 /**
483         @brief Return a list of previously generated error messages for a specified query.
484         @param ctx Pointer to the current method context.
485         @return Zero if successful, or -1 if not.
486
487         Method parameters:
488         - query token, as previously returned by the .prepare method.
489
490         Returns: A (possibly empty) array of strings, each one an error message generated during
491         previous operations in connection with the specified query.
492 */
493 int doMessages( osrfMethodContext* ctx ) {
494         if(osrfMethodVerifyContext( ctx )) {
495                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
496                 return -1;
497         }
498
499         // Get the query token from a method parameter
500         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
501         if( token_obj->type != JSON_STRING ) {
502                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
503                         ctx->request, "Invalid parameter; query token must be a string" );
504                 return -1;
505         }
506         const char* token = jsonObjectGetString( token_obj );
507
508         // Look up the query token in the session-level userData
509         CachedQuery* query = search_token( ctx, token );
510         if( !query ) {
511                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
512                         ctx->request, "Invalid query token" );
513                 return -1;
514         }
515
516         osrfLogInfo( OSRF_LOG_MARK, "Returning messages for token %s", token );
517
518         jsonObject* msgs = jsonNewObjectType( JSON_ARRAY );
519         const osrfStringArray* error_msgs = query->state->error_msgs;
520         int i;
521         for( i = 0; i < error_msgs->size; ++i ) {
522                 jsonObject* msg = jsonNewObject( osrfStringArrayGetString( error_msgs, i ));
523                 jsonObjectPush( msgs, msg );
524         }
525
526         osrfAppRespondComplete( ctx, msgs );
527         return 0;
528 }
529
530 /**
531         @brief Discard a previously stored query, as identified by a token.
532         @param ctx Pointer to the current method context.
533         @return Zero if successful, or -1 if not.
534
535         Method parameters:
536         - query token, as previously returned by the .prepare method.
537
538         Returns: Nothing.
539 */
540 int doFinish( osrfMethodContext* ctx ) {
541         if(osrfMethodVerifyContext( ctx )) {
542                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
543                 return -1;
544         }
545
546         // Get the query token.
547         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
548         if( token_obj->type != JSON_STRING ) {
549                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
550                                                           ctx->request, "Invalid parameter; query token must be a string" );
551                 return -1;
552         }
553         const char* token = jsonObjectGetString( token_obj );
554
555         // Delete the corresponding entry from the cache.  If there is no cache, or no such entry,
556         // just ignore the problem and report success.
557         osrfHash* cache = ctx->session->userData;
558         if( cache )
559                 osrfHashRemove( cache, token );
560
561         osrfAppRespondComplete( ctx, NULL );
562         return 0;
563 }
564
565 /**
566         @brief Save a query in session-level userData for reference in future method calls.
567         @param ctx Pointer to the current method context.
568         @param state Pointer to the state of the query.
569         @param query Pointer to the abstract representation of the query.
570         @return Pointer to an identifying token to be returned to the client.
571 */
572 static const char* save_query(
573         osrfMethodContext* ctx, BuildSQLState* state, StoredQ* query ) {
574
575         CachedQuery* cached_query = safe_malloc( sizeof( CachedQuery ));
576         cached_query->state       = state;
577         cached_query->query       = query;
578
579         // Get the cache.  If we don't have one yet, make one.
580         osrfHash* cache = ctx->session->userData;
581         if( !cache ) {
582                 cache = osrfNewHash();
583                 osrfHashSetCallback( cache, free_cached_query );
584                 ctx->session->userData = cache;
585                 ctx->session->userDataFree = userDataFree;  // arrange to free it at end of session
586         }
587
588         // Create a token string to be used as a key
589         static unsigned int token_count = 0;
590         char* token = va_list_to_string(
591                 "%u_%ld_%ld", ++token_count, (long) time( NULL ), (long) getpid() );
592
593         osrfHashSet( cache, cached_query, token );
594         return token;
595 }
596
597 /**
598         @brief Free a CachedQuery
599         @param Pointer to the CachedQuery to be freed.
600 */
601 static void free_cached_query( char* key, void* data ) {
602         if( data ) {
603                 CachedQuery* cached_query = data;
604                 buildSQLStateFree( cached_query->state );
605                 storedQFree( cached_query->query );
606         }
607 }
608
609 /**
610         @brief Callback for freeing session-level userData.
611         @param blob Opaque pointer t userData.
612 */
613 static void userDataFree( void* blob ) {
614         osrfHashFree( (osrfHash*) blob );
615 }
616
617 /**
618         @brief Search for the cached query corresponding to a given token.
619         @param ctx Pointer to the current method context.
620         @param token Token string from a previous call to the prepare method.
621         @return A pointer to the cached query, if found, or NULL if not.
622 */
623 static CachedQuery* search_token( osrfMethodContext* ctx, const char* token ) {
624         if( ctx && ctx->session->userData && token ) {
625                 osrfHash* cache = ctx->session->userData;
626                 return osrfHashGet( cache, token );
627         } else
628                 return NULL;
629 }