]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/c-apps/oils_qstore.c
Add partial support for bind variables: load them from the
[working/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 Execute an SQL query and return a result set.
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         osrfAppRespondComplete( ctx, jsonNewObject( "build method not yet implemented" ));
285         return 0;
286 }
287
288 /**
289         @brief Execute an SQL query and return a result set.
290         @param ctx Pointer to the current method context.
291         @return Zero if successful, or -1 if not.
292
293         Method parameters:
294         - query token, as previously returned by the .prepare method.
295
296         Returns: A series of responses, each of them a row represented as an array of column values.
297 */
298 int doExecute( osrfMethodContext* ctx ) {
299         if(osrfMethodVerifyContext( ctx )) {
300                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
301                 return -1;
302         }
303
304         // Get the query token
305         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
306         if( token_obj->type != JSON_STRING ) {
307                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
308                         ctx->request, "Invalid parameter; query token must be a string" );
309                 return -1;
310         }
311         const char* token = jsonObjectGetString( token_obj );
312
313         // Look up the query token in the session-level userData
314         CachedQuery* query = search_token( ctx, token );
315         if( !query ) {
316                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
317                         ctx->request, "Invalid query token" );
318                 return -1;
319         }
320
321         osrfLogInfo( OSRF_LOG_MARK, "Executing query for token \"%s\"", token );
322         if( query->state->error ) {
323                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
324                         "No valid prepared query available for query id # %d", query->query->id ));
325                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
326                                                           ctx->request, "No valid prepared query available" );
327                 return -1;
328         } else if( buildSQL( query->state, query->query )) {
329                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
330                         "Unable to build SQL statement for query id # %d", query->query->id ));
331                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
332                         ctx->request, "Unable to build SQL statement" );
333                 return -1;
334         }
335
336         jsonObject* row = oilsFirstRow( query->state );
337         while( row ) {
338                 osrfAppRespond( ctx, row );
339                 row = oilsNextRow( query->state );
340         }
341
342         if( query->state->error ) {
343                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
344                         "Unable to execute SQL statement for query id # %d", query->query->id ));
345                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
346                         ctx->request, "Unable to execute SQL statement" );
347                 return -1;
348         }
349
350         osrfAppRespondComplete( ctx, NULL );
351         return 0;
352 }
353
354 /**
355         @brief Construct an SQL query, but without executing it.
356         @param ctx Pointer to the current method context.
357         @return Zero if successful, or -1 if not.
358
359         Method parameters:
360         - query token, as previously returned by the .prepare method.
361
362         Returns: A string containing an SQL query..
363 */
364 int doSql( osrfMethodContext* ctx ) {
365         if(osrfMethodVerifyContext( ctx )) {
366                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
367                 return -1;
368         }
369
370         // Get the query token
371         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
372         if( token_obj->type != JSON_STRING ) {
373                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
374                         ctx->request, "Invalid parameter; query token must be a string" );
375                 return -1;
376         }
377         const char* token = jsonObjectGetString( token_obj );
378
379         // Look up the query token in the session-level userData
380         CachedQuery* query = search_token( ctx, token );
381         if( !query ) {
382                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
383                         ctx->request, "Invalid query token" );
384                 return -1;
385         }
386
387         osrfLogInfo( OSRF_LOG_MARK, "Returning SQL for token \"%s\"", token );
388         if( query->state->error ) {
389                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
390                         "No valid prepared query available for query id # %d", query->query->id ));
391                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
392                         ctx->request, "No valid prepared query available" );
393                 return -1;
394         } else if( buildSQL( query->state, query->query )) {
395                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
396                         "Unable to build SQL statement for query id # %d", query->query->id ));
397                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
398                         ctx->request, "Unable to build SQL statement" );
399                 return -1;
400         }
401
402         osrfAppRespondComplete( ctx, jsonNewObject( OSRF_BUFFER_C_STR( query->state->sql )));
403         return 0;
404 }
405
406 /**
407         @brief Return a list of previously generated error messages for a specified query.
408         @param ctx Pointer to the current method context.
409         @return Zero if successful, or -1 if not.
410
411         Method parameters:
412         - query token, as previously returned by the .prepare method.
413
414         Returns: A (possibly empty) array of strings, each one an error message generated during
415         previous operations in connection with the specified query.
416 */
417 int doMessages( osrfMethodContext* ctx ) {
418         if(osrfMethodVerifyContext( ctx )) {
419                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
420                 return -1;
421         }
422
423         // Get the query token from a method parameter
424         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
425         if( token_obj->type != JSON_STRING ) {
426                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
427                         ctx->request, "Invalid parameter; query token must be a string" );
428                 return -1;
429         }
430         const char* token = jsonObjectGetString( token_obj );
431
432         // Look up the query token in the session-level userData
433         CachedQuery* query = search_token( ctx, token );
434         if( !query ) {
435                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
436                         ctx->request, "Invalid query token" );
437                 return -1;
438         }
439
440         osrfLogInfo( OSRF_LOG_MARK, "Returning messages for token %s", token );
441
442         jsonObject* msgs = jsonNewObjectType( JSON_ARRAY );
443         const osrfStringArray* error_msgs = query->state->error_msgs;
444         int i;
445         for( i = 0; i < error_msgs->size; ++i ) {
446                 jsonObject* msg = jsonNewObject( osrfStringArrayGetString( error_msgs, i ));
447                 jsonObjectPush( msgs, msg );
448         }
449
450         osrfAppRespondComplete( ctx, msgs );
451         return 0;
452 }
453
454 /**
455         @brief Discard a previously stored query, as identified by a token.
456         @param ctx Pointer to the current method context.
457         @return Zero if successful, or -1 if not.
458
459         Method parameters:
460         - query token, as previously returned by the .prepare method.
461
462         Returns: Nothing.
463 */
464 int doFinish( osrfMethodContext* ctx ) {
465         if(osrfMethodVerifyContext( ctx )) {
466                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
467                 return -1;
468         }
469
470         // Get the query token.
471         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
472         if( token_obj->type != JSON_STRING ) {
473                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
474                                                           ctx->request, "Invalid parameter; query token must be a string" );
475                 return -1;
476         }
477         const char* token = jsonObjectGetString( token_obj );
478
479         // Delete the corresponding entry from the cache.  If there is no cache, or no such entry,
480         // just ignore the problem and report success.
481         osrfHash* cache = ctx->session->userData;
482         if( cache )
483                 osrfHashRemove( cache, token );
484
485         osrfAppRespondComplete( ctx, NULL );
486         return 0;
487 }
488
489 /**
490         @brief Save a query in session-level userData for reference in future method calls.
491         @param ctx Pointer to the current method context.
492         @param state Pointer to the state of the query.
493         @param query Pointer to the abstract representation of the query.
494         @return Pointer to an identifying token to be returned to the client.
495 */
496 static const char* save_query(
497         osrfMethodContext* ctx, BuildSQLState* state, StoredQ* query ) {
498
499         CachedQuery* cached_query = safe_malloc( sizeof( CachedQuery ));
500         cached_query->state       = state;
501         cached_query->query       = query;
502
503         // Get the cache.  If we don't have one yet, make one.
504         osrfHash* cache = ctx->session->userData;
505         if( !cache ) {
506                 cache = osrfNewHash();
507                 osrfHashSetCallback( cache, free_cached_query );
508                 ctx->session->userData = cache;
509                 ctx->session->userDataFree = userDataFree;  // arrange to free it at end of session
510         }
511
512         // Create a token string to be used as a key
513         static unsigned int token_count = 0;
514         char* token = va_list_to_string(
515                 "%u_%ld_%ld", ++token_count, (long) time( NULL ), (long) getpid() );
516
517         osrfHashSet( cache, cached_query, token );
518         return token;
519 }
520
521 /**
522         @brief Free a CachedQuery
523         @param Pointer to the CachedQuery to be freed.
524 */
525 static void free_cached_query( char* key, void* data ) {
526         if( data ) {
527                 CachedQuery* cached_query = data;
528                 buildSQLStateFree( cached_query->state );
529                 storedQFree( cached_query->query );
530         }
531 }
532
533 /**
534         @brief Callback for freeing session-level userData.
535         @param blob Opaque pointer t userData.
536 */
537 static void userDataFree( void* blob ) {
538         osrfHashFree( (osrfHash*) blob );
539 }
540
541 /**
542         @brief Search for the cached query corresponding to a given token.
543         @param ctx Pointer to the current method context.
544         @param token Token string from a previous call to the prepare method.
545         @return A pointer to the cached query, if found, or NULL if not.
546 */
547 static CachedQuery* search_token( osrfMethodContext* ctx, const char* token ) {
548         if( ctx && ctx->session->userData && token ) {
549                 osrfHash* cache = ctx->session->userData;
550                 return osrfHashGet( cache, token );
551         } else
552                 return NULL;
553 }