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 growing_buffer* method_name = buffer_init( 64 );
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 );
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 );
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 );
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 );
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 );
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 );
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 );
127 @brief Initialize a server drone.
128 @return Zero if successful, -1 if not.
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.
133 This function is called by a server drone shortly after it is spawned by the listener.
135 int osrfAppChildInit( void ) {
137 dbhandle = oilsConnectDB( modulename );
141 oilsSetDBConnection( dbhandle );
142 osrfLogInfo( OSRF_LOG_MARK, "%s successfully connected to the database", modulename );
144 // Apply datatypes from database to the fields in the IDL
145 //if( oilsExtendIDL() ) {
146 // osrfLogError( OSRF_LOG_MARK, "Error extending the IDL" );
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.
160 - query id (key of query.stored_query table)
162 Returns: a character string serving as a token for future references to the query.
164 NB: the method return type is temporary. Eventually this method will return both a token
165 and a list of bind variables.
167 int doPrepare( osrfMethodContext* ctx ) {
168 if(osrfMethodVerifyContext( ctx )) {
169 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
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" );
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" );
188 osrfLogInfo( OSRF_LOG_MARK, "Loading query for id # %d", query_id );
190 BuildSQLState* state = buildSQLStateNew( dbhandle );
191 StoredQ* query = getStoredQuery( state, query_id );
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" );
199 const char* token = save_query( ctx, state, query );
201 osrfLogInfo( OSRF_LOG_MARK, "Token for query id # %d is \"%s\"", query_id, token );
203 osrfAppRespondComplete( ctx, jsonNewObject( token ));
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.
213 - query token, as previously returned by the .prepare method.
215 Returns: An array of column names; unavailable names are represented as nulls.
217 int doColumns( osrfMethodContext* ctx ) {
218 if(osrfMethodVerifyContext( ctx )) {
219 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
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" );
230 const char* token = jsonObjectGetString( token_obj );
232 // Look up the query token in the session-level userData
233 CachedQuery* query = search_token( ctx, token );
235 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
236 ctx->request, "Invalid query token" );
240 osrfLogInfo( OSRF_LOG_MARK, "Listing column names for token %s", token );
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" );
248 osrfAppRespondComplete( ctx, col_list );
253 int doBindParam( osrfMethodContext* ctx ) {
254 if(osrfMethodVerifyContext( ctx )) {
255 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
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" );
266 const char* token = jsonObjectGetString( token_obj );
268 // Look up the query token in the session-level userData
269 CachedQuery* query = search_token( ctx, token );
271 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
272 ctx->request, "Invalid query token" );
276 osrfLogInfo( OSRF_LOG_MARK, "Binding parameter(s) for token %s", token );
278 osrfAppRespondComplete( ctx, jsonNewObject( "build method not yet implemented" ));
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.
288 - query token, as previously returned by the .prepare method.
290 Returns: A series of responses, each of them a row represented as an array of column values.
292 int doExecute( osrfMethodContext* ctx ) {
293 if(osrfMethodVerifyContext( ctx )) {
294 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
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" );
305 const char* token = jsonObjectGetString( token_obj );
307 // Look up the query token in the session-level userData
308 CachedQuery* query = search_token( ctx, token );
310 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
311 ctx->request, "Invalid query token" );
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" );
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" );
330 jsonObject* row = oilsFirstRow( query->state );
332 osrfAppRespond( ctx, row );
333 row = oilsNextRow( query->state );
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" );
344 osrfAppRespondComplete( ctx, NULL );
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.
354 - query token, as previously returned by the .prepare method.
356 Returns: A string containing an SQL query..
358 int doSql( osrfMethodContext* ctx ) {
359 if(osrfMethodVerifyContext( ctx )) {
360 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
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" );
371 const char* token = jsonObjectGetString( token_obj );
373 // Look up the query token in the session-level userData
374 CachedQuery* query = search_token( ctx, token );
376 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
377 ctx->request, "Invalid query token" );
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" );
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" );
396 osrfAppRespondComplete( ctx, jsonNewObject( OSRF_BUFFER_C_STR( query->state->sql )));
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.
406 - query token, as previously returned by the .prepare method.
408 Returns: A (possibly empty) array of strings, each one an error message generated during
409 previous operations in connection with the specified query.
411 int doMessages( osrfMethodContext* ctx ) {
412 if(osrfMethodVerifyContext( ctx )) {
413 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
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" );
424 const char* token = jsonObjectGetString( token_obj );
426 // Look up the query token in the session-level userData
427 CachedQuery* query = search_token( ctx, token );
429 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
430 ctx->request, "Invalid query token" );
434 osrfLogInfo( OSRF_LOG_MARK, "Returning messages for token %s", token );
436 jsonObject* msgs = jsonNewObjectType( JSON_ARRAY );
437 const osrfStringArray* error_msgs = query->state->error_msgs;
439 for( i = 0; i < error_msgs->size; ++i ) {
440 jsonObject* msg = jsonNewObject( osrfStringArrayGetString( error_msgs, i ));
441 jsonObjectPush( msgs, msg );
444 osrfAppRespondComplete( ctx, msgs );
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.
454 - query token, as previously returned by the .prepare method.
458 int doFinish( osrfMethodContext* ctx ) {
459 if(osrfMethodVerifyContext( ctx )) {
460 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
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" );
471 const char* token = jsonObjectGetString( token_obj );
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;
477 osrfHashRemove( cache, token );
479 osrfAppRespondComplete( ctx, NULL );
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.
490 static const char* save_query(
491 osrfMethodContext* ctx, BuildSQLState* state, StoredQ* query ) {
493 CachedQuery* cached_query = safe_malloc( sizeof( CachedQuery ));
494 cached_query->state = state;
495 cached_query->query = query;
497 // Get the cache. If we don't have one yet, make one.
498 osrfHash* cache = ctx->session->userData;
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
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() );
511 osrfHashSet( cache, cached_query, token );
516 @brief Free a CachedQuery
517 @param Pointer to the CachedQuery to be freed.
519 static void free_cached_query( char* key, void* data ) {
521 CachedQuery* cached_query = data;
522 buildSQLStateFree( cached_query->state );
523 storedQFree( cached_query->query );
528 @brief Callback for freeing session-level userData.
529 @param blob Opaque pointer t userData.
531 static void userDataFree( void* blob ) {
532 osrfHashFree( (osrfHash*) blob );
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.
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 );