3 @brief As a server, perform database queries as defined in the database itself.
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"
19 @brief Information about a previously prepared query.
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
30 static dbi_conn dbhandle; /* our db connection */
32 static const char modulename[] = "open-ils.qstore";
34 int doPrepare( osrfMethodContext* ctx );
35 int doExecute( osrfMethodContext* ctx );
36 int doSql( osrfMethodContext* ctx );
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 );
45 @brief Disconnect from the database.
47 This function is called when the server drone is about to terminate.
49 void osrfAppChildExit() {
50 osrfLogDebug( OSRF_LOG_MARK, "Child is exiting, disconnecting from database..." );
53 dbi_conn_query( dbhandle, "ROLLBACK;" );
54 dbi_conn_close( dbhandle );
60 @brief Initialize the application.
61 @return Zero if successful, or non-zero if not.
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.
67 Register the functions for remote procedure calls.
69 This function is called when the registering the application, and is executed by the
70 listener before spawning the drones.
72 int osrfAppInitialize() {
74 osrfLogInfo( OSRF_LOG_MARK, "Initializing the QStore Server..." );
75 osrfLogInfo( OSRF_LOG_MARK, "Finding XML file..." );
77 if ( !oilsIDLInit( osrf_settings_host_value( "/IDL" )))
78 return 1; /* return non-zero to indicate error */
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 );
84 growing_buffer* method_name = buffer_init( 64 );
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 );
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 );
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 );
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 );
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 );
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 );
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 );
131 @brief Initialize a server drone.
132 @return Zero if successful, -1 if not.
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.
137 This function is called by a server drone shortly after it is spawned by the listener.
139 int osrfAppChildInit( void ) {
141 dbhandle = oilsConnectDB( modulename );
145 oilsSetDBConnection( dbhandle );
146 osrfLogInfo( OSRF_LOG_MARK, "%s successfully connected to the database", modulename );
148 // Apply datatypes from database to the fields in the IDL
149 //if( oilsExtendIDL() ) {
150 // osrfLogError( OSRF_LOG_MARK, "Error extending the IDL" );
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.
164 - query id (key of query.stored_query table)
166 Returns: a character string serving as a token for future references to the query.
168 NB: the method return type is temporary. Eventually this method will return both a token
169 and a list of bind variables.
171 int doPrepare( osrfMethodContext* ctx ) {
172 if(osrfMethodVerifyContext( ctx )) {
173 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
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" );
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" );
192 osrfLogInfo( OSRF_LOG_MARK, "Loading query for id # %d", query_id );
194 BuildSQLState* state = buildSQLStateNew( dbhandle );
195 state->defaults_usable = 1;
196 state->values_required = 0;
197 StoredQ* query = getStoredQuery( state, query_id );
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" );
205 const char* token = save_query( ctx, state, query );
207 osrfLogInfo( OSRF_LOG_MARK, "Token for query id # %d is \"%s\"", query_id, token );
209 osrfAppRespondComplete( ctx, jsonNewObject( token ));
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.
219 - query token, as previously returned by the .prepare method.
221 Returns: An array of column names; unavailable names are represented as nulls.
223 int doColumns( osrfMethodContext* ctx ) {
224 if(osrfMethodVerifyContext( ctx )) {
225 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
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" );
236 const char* token = jsonObjectGetString( token_obj );
238 // Look up the query token in the session-level userData
239 CachedQuery* query = search_token( ctx, token );
241 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
242 ctx->request, "Invalid query token" );
246 osrfLogInfo( OSRF_LOG_MARK, "Listing column names for token %s", token );
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" );
254 osrfAppRespondComplete( ctx, col_list );
259 int doBindParam( osrfMethodContext* ctx ) {
260 if(osrfMethodVerifyContext( ctx )) {
261 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
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" );
272 const char* token = jsonObjectGetString( token_obj );
274 // Look up the query token in the session-level userData
275 CachedQuery* query = search_token( ctx, token );
277 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
278 ctx->request, "Invalid query token" );
282 osrfLogInfo( OSRF_LOG_MARK, "Binding parameter(s) for token %s", token );
284 jsonObject* bindings = jsonObjectGetIndex( ctx->params, 1 );
286 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
287 ctx->request, "No parameter provided for bind variable values" );
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" );
295 if( 0 == bindings->size ) {
296 // No values to assign; we're done.
297 osrfAppRespondComplete( ctx, NULL );
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" );
308 if( oilsApplyBindValues( query->state, bindings )) {
309 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
310 ctx->request, "Unable to apply values to bind variables" );
313 osrfAppRespondComplete( ctx, NULL );
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.
324 - query token, as previously returned by the .prepare method.
326 Returns: A series of responses, each of them a row represented as an array of column values.
328 int doExecute( osrfMethodContext* ctx ) {
329 if(osrfMethodVerifyContext( ctx )) {
330 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
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" );
341 const char* token = jsonObjectGetString( token_obj );
343 // Look up the query token in the session-level userData
344 CachedQuery* query = search_token( ctx, token );
346 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
347 ctx->request, "Invalid query token" );
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" );
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" );
366 jsonObject* row = oilsFirstRow( query->state );
368 osrfAppRespond( ctx, row );
369 row = oilsNextRow( query->state );
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" );
380 osrfAppRespondComplete( ctx, NULL );
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.
390 - query token, as previously returned by the .prepare method.
392 Returns: A string containing an SQL query..
394 int doSql( osrfMethodContext* ctx ) {
395 if(osrfMethodVerifyContext( ctx )) {
396 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
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" );
407 const char* token = jsonObjectGetString( token_obj );
409 // Look up the query token in the session-level userData
410 CachedQuery* query = search_token( ctx, token );
412 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
413 ctx->request, "Invalid query token" );
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" );
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" );
432 osrfAppRespondComplete( ctx, jsonNewObject( OSRF_BUFFER_C_STR( query->state->sql )));
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.
442 - query token, as previously returned by the .prepare method.
444 Returns: A (possibly empty) array of strings, each one an error message generated during
445 previous operations in connection with the specified query.
447 int doMessages( osrfMethodContext* ctx ) {
448 if(osrfMethodVerifyContext( ctx )) {
449 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
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" );
460 const char* token = jsonObjectGetString( token_obj );
462 // Look up the query token in the session-level userData
463 CachedQuery* query = search_token( ctx, token );
465 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
466 ctx->request, "Invalid query token" );
470 osrfLogInfo( OSRF_LOG_MARK, "Returning messages for token %s", token );
472 jsonObject* msgs = jsonNewObjectType( JSON_ARRAY );
473 const osrfStringArray* error_msgs = query->state->error_msgs;
475 for( i = 0; i < error_msgs->size; ++i ) {
476 jsonObject* msg = jsonNewObject( osrfStringArrayGetString( error_msgs, i ));
477 jsonObjectPush( msgs, msg );
480 osrfAppRespondComplete( ctx, msgs );
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.
490 - query token, as previously returned by the .prepare method.
494 int doFinish( osrfMethodContext* ctx ) {
495 if(osrfMethodVerifyContext( ctx )) {
496 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
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" );
507 const char* token = jsonObjectGetString( token_obj );
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;
513 osrfHashRemove( cache, token );
515 osrfAppRespondComplete( ctx, NULL );
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.
526 static const char* save_query(
527 osrfMethodContext* ctx, BuildSQLState* state, StoredQ* query ) {
529 CachedQuery* cached_query = safe_malloc( sizeof( CachedQuery ));
530 cached_query->state = state;
531 cached_query->query = query;
533 // Get the cache. If we don't have one yet, make one.
534 osrfHash* cache = ctx->session->userData;
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
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() );
547 osrfHashSet( cache, cached_query, token );
552 @brief Free a CachedQuery
553 @param Pointer to the CachedQuery to be freed.
555 static void free_cached_query( char* key, void* data ) {
557 CachedQuery* cached_query = data;
558 buildSQLStateFree( cached_query->state );
559 storedQFree( cached_query->query );
564 @brief Callback for freeing session-level userData.
565 @param blob Opaque pointer t userData.
567 static void userDataFree( void* blob ) {
568 osrfHashFree( (osrfHash*) blob );
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.
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 );