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