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