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, ".bind_param" );
90 osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
91 "doBindParam", "", 2, 0 );
93 buffer_reset( method_name );
94 OSRF_BUFFER_ADD( method_name, modulename );
95 OSRF_BUFFER_ADD( method_name, ".execute" );
96 osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
97 "doExecute", "", 1, OSRF_METHOD_STREAMING );
99 buffer_reset( method_name );
100 OSRF_BUFFER_ADD( method_name, modulename );
101 OSRF_BUFFER_ADD( method_name, ".sql" );
102 osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
103 "doSql", "", 1, OSRF_METHOD_STREAMING );
105 buffer_reset( method_name );
106 OSRF_BUFFER_ADD( method_name, modulename );
107 OSRF_BUFFER_ADD( method_name, ".finish" );
108 osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
109 "doFinish", "", 1, 0 );
111 buffer_reset( method_name );
112 OSRF_BUFFER_ADD( method_name, modulename );
113 OSRF_BUFFER_ADD( method_name, ".messages" );
114 osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
115 "doMessages", "", 1, 0 );
121 @brief Initialize a server drone.
122 @return Zero if successful, -1 if not.
124 Connect to the database. For each non-virtual class in the IDL, execute a dummy "SELECT * "
125 query to get the datatype of each column. Record the datatypes in the loaded IDL.
127 This function is called by a server drone shortly after it is spawned by the listener.
129 int osrfAppChildInit( void ) {
131 dbhandle = oilsConnectDB( modulename );
135 oilsSetDBConnection( dbhandle );
136 osrfLogInfo( OSRF_LOG_MARK, "%s successfully connected to the database", modulename );
138 // Apply datatypes from database to the fields in the IDL
139 //if( oilsExtendIDL() ) {
140 // osrfLogError( OSRF_LOG_MARK, "Error extending the IDL" );
149 @brief Load a specified query from the database query tables.
150 @param ctx Pointer to the current method context.
151 @return Zero if successful, or -1 if not.
154 - query id (key of query.stored_query table)
156 Returns: a character string serving as a token for future references to the query.
158 NB: the method return type is temporary. Eventually this method will return both a token
159 and a list of bind variables.
161 int doPrepare( osrfMethodContext* ctx ) {
162 if(osrfMethodVerifyContext( ctx )) {
163 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
167 // Get the query id from a method parameter
168 const jsonObject* query_id_obj = jsonObjectGetIndex( ctx->params, 0 );
169 if( query_id_obj->type != JSON_NUMBER ) {
170 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
171 ctx->request, "Invalid parameter; query id must be a number" );
175 int query_id = atoi( jsonObjectGetString( query_id_obj ));
176 if( query_id <= 0 ) {
177 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
178 ctx->request, "Invalid parameter: query id must be greater than zero" );
182 osrfLogInfo( OSRF_LOG_MARK, "Loading query for id # %d", query_id );
184 BuildSQLState* state = buildSQLStateNew( dbhandle );
185 StoredQ* query = getStoredQuery( state, query_id );
187 osrfLogWarning( OSRF_LOG_MARK, "Unable to load stored query # %d", query_id );
188 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
189 ctx->request, "Unable to load stored query" );
193 const char* token = save_query( ctx, state, query );
195 osrfLogInfo( OSRF_LOG_MARK, "Token for query id # %d is \"%s\"", query_id, token );
197 osrfAppRespondComplete( ctx, jsonNewObject( token ));
201 int doBindParam( osrfMethodContext* ctx ) {
202 if(osrfMethodVerifyContext( ctx )) {
203 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
207 // Get the query token from a method parameter
208 const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
209 if( token_obj->type != JSON_STRING ) {
210 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
211 ctx->request, "Invalid parameter; query token must be a string" );
214 const char* token = jsonObjectGetString( token_obj );
216 // Look up the query token in the session-level userData
217 CachedQuery* query = search_token( ctx, token );
219 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
220 ctx->request, "Invalid query token" );
224 osrfLogInfo( OSRF_LOG_MARK, "Binding parameter(s) for token %s", token );
226 osrfAppRespondComplete( ctx, jsonNewObject( "build method not yet implemented" ));
231 @brief Execute an SQL query and return a result set.
232 @param ctx Pointer to the current method context.
233 @return Zero if successful, or -1 if not.
236 - query token, as previously returned by the .prepare method.
238 Returns: A series of responses, each of them a row represented as an array of column values.
240 int doExecute( osrfMethodContext* ctx ) {
241 if(osrfMethodVerifyContext( ctx )) {
242 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
246 // Get the query token
247 const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
248 if( token_obj->type != JSON_STRING ) {
249 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
250 ctx->request, "Invalid parameter; query token must be a string" );
253 const char* token = jsonObjectGetString( token_obj );
255 // Look up the query token in the session-level userData
256 CachedQuery* query = search_token( ctx, token );
258 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
259 ctx->request, "Invalid query token" );
263 osrfLogInfo( OSRF_LOG_MARK, "Executing query for token \"%s\"", token );
264 if( query->state->error ) {
265 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
266 "No valid prepared query available for query id # %d", query->query->id ));
267 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
268 ctx->request, "No valid prepared query available" );
270 } else if( buildSQL( query->state, query->query )) {
271 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
272 "Unable to build SQL statement for query id # %d", query->query->id ));
273 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
274 ctx->request, "Unable to build SQL statement" );
278 jsonObject* row = oilsFirstRow( query->state );
280 osrfAppRespond( ctx, row );
281 row = oilsNextRow( query->state );
284 if( query->state->error ) {
285 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
286 "Unable to execute SQL statement for query id # %d", query->query->id ));
287 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
288 ctx->request, "Unable to execute SQL statement" );
292 osrfAppRespondComplete( ctx, NULL );
297 @brief Construct an SQL query, but without executing it.
298 @param ctx Pointer to the current method context.
299 @return Zero if successful, or -1 if not.
302 - query token, as previously returned by the .prepare method.
304 Returns: A string containing an SQL query..
306 int doSql( osrfMethodContext* ctx ) {
307 if(osrfMethodVerifyContext( ctx )) {
308 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
312 // Get the query token
313 const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
314 if( token_obj->type != JSON_STRING ) {
315 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
316 ctx->request, "Invalid parameter; query token must be a string" );
319 const char* token = jsonObjectGetString( token_obj );
321 // Look up the query token in the session-level userData
322 CachedQuery* query = search_token( ctx, token );
324 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
325 ctx->request, "Invalid query token" );
329 osrfLogInfo( OSRF_LOG_MARK, "Returning SQL for token \"%s\"", token );
330 if( query->state->error ) {
331 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
332 "No valid prepared query available for query id # %d", query->query->id ));
333 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
334 ctx->request, "No valid prepared query available" );
336 } else if( buildSQL( query->state, query->query )) {
337 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
338 "Unable to build SQL statement for query id # %d", query->query->id ));
339 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
340 ctx->request, "Unable to build SQL statement" );
344 osrfAppRespondComplete( ctx, jsonNewObject( OSRF_BUFFER_C_STR( query->state->sql )));
349 @brief Return a list of previously generated error messages for a specified query.
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 (possibly empty) array of strings, each one an error message generated during
357 previous operations in connection with the specified query.
359 int doMessages( osrfMethodContext* ctx ) {
360 if(osrfMethodVerifyContext( ctx )) {
361 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
365 // Get the query token from a method parameter
366 const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
367 if( token_obj->type != JSON_STRING ) {
368 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
369 ctx->request, "Invalid parameter; query token must be a string" );
372 const char* token = jsonObjectGetString( token_obj );
374 // Look up the query token in the session-level userData
375 CachedQuery* query = search_token( ctx, token );
377 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
378 ctx->request, "Invalid query token" );
382 osrfLogInfo( OSRF_LOG_MARK, "Returning messages for token %s", token );
384 jsonObject* msgs = jsonNewObjectType( JSON_ARRAY );
385 const osrfStringArray* error_msgs = query->state->error_msgs;
387 for( i = 0; i < error_msgs->size; ++i ) {
388 jsonObject* msg = jsonNewObject( osrfStringArrayGetString( error_msgs, i ));
389 jsonObjectPush( msgs, msg );
392 osrfAppRespondComplete( ctx, msgs );
397 @brief Discard a previously stored query, as identified by a token.
398 @param ctx Pointer to the current method context.
399 @return Zero if successful, or -1 if not.
402 - query token, as previously returned by the .prepare method.
406 int doFinish( osrfMethodContext* ctx ) {
407 if(osrfMethodVerifyContext( ctx )) {
408 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
412 // Get the query token.
413 const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
414 if( token_obj->type != JSON_STRING ) {
415 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
416 ctx->request, "Invalid parameter; query token must be a string" );
419 const char* token = jsonObjectGetString( token_obj );
421 // Delete the corresponding entry from the cache. If there is no cache, or no such entry,
422 // just ignore the problem and report success.
423 osrfHash* cache = ctx->session->userData;
425 osrfHashRemove( cache, token );
427 osrfAppRespondComplete( ctx, NULL );
432 @brief Save a query in session-level userData for reference in future method calls.
433 @param ctx Pointer to the current method context.
434 @param state Pointer to the state of the query.
435 @param query Pointer to the abstract representation of the query.
436 @return Pointer to an identifying token to be returned to the client.
438 static const char* save_query(
439 osrfMethodContext* ctx, BuildSQLState* state, StoredQ* query ) {
441 CachedQuery* cached_query = safe_malloc( sizeof( CachedQuery ));
442 cached_query->state = state;
443 cached_query->query = query;
445 // Get the cache. If we don't have one yet, make one.
446 osrfHash* cache = ctx->session->userData;
448 cache = osrfNewHash();
449 osrfHashSetCallback( cache, free_cached_query );
450 ctx->session->userData = cache;
451 ctx->session->userDataFree = userDataFree; // arrange to free it at end of session
454 // Create a token string to be used as a key
455 static unsigned int token_count = 0;
456 char* token = va_list_to_string(
457 "%u_%ld_%ld", ++token_count, (long) time( NULL ), (long) getpid() );
459 osrfHashSet( cache, cached_query, token );
464 @brief Free a CachedQuery
465 @param Pointer to the CachedQuery to be freed.
467 static void free_cached_query( char* key, void* data ) {
469 CachedQuery* cached_query = data;
470 buildSQLStateFree( cached_query->state );
471 storedQFree( cached_query->query );
476 @brief Callback for freeing session-level userData.
477 @param blob Opaque pointer t userData.
479 static void userDataFree( void* blob ) {
480 osrfHashFree( (osrfHash*) blob );
484 @brief Search for the cached query corresponding to a given token.
485 @param ctx Pointer to the current method context.
486 @param token Token string from a previous call to the prepare method.
487 @return A pointer to the cached query, if found, or NULL if not.
489 static CachedQuery* search_token( osrfMethodContext* ctx, const char* token ) {
490 if( ctx && ctx->session->userData && token ) {
491 osrfHash* cache = ctx->session->userData;
492 return osrfHashGet( cache, token );