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() {
131 osrfLogDebug( OSRF_LOG_MARK, "Attempting to initialize libdbi..." );
132 dbi_initialize( NULL );
133 osrfLogDebug( OSRF_LOG_MARK, "... libdbi initialized." );
135 char* driver = osrf_settings_host_value( "/apps/%s/app_settings/driver", modulename );
136 char* user = osrf_settings_host_value( "/apps/%s/app_settings/database/user", modulename );
137 char* host = osrf_settings_host_value( "/apps/%s/app_settings/database/host", modulename );
138 char* port = osrf_settings_host_value( "/apps/%s/app_settings/database/port", modulename );
139 char* db = osrf_settings_host_value( "/apps/%s/app_settings/database/db", modulename );
140 char* pw = osrf_settings_host_value( "/apps/%s/app_settings/database/pw", modulename );
142 osrfLogDebug( OSRF_LOG_MARK, "Attempting to load the database driver [%s]...", driver );
143 dbhandle = dbi_conn_new( driver );
146 osrfLogError( OSRF_LOG_MARK, "Error loading database driver [%s]", driver );
149 osrfLogDebug( OSRF_LOG_MARK, "Database driver [%s] seems OK", driver );
151 osrfLogInfo(OSRF_LOG_MARK, "%s connecting to database. host=%s, "
152 "port=%s, user=%s, db=%s", modulename, host, port, user, db );
154 if( host ) dbi_conn_set_option( dbhandle, "host", host );
155 if( port ) dbi_conn_set_option_numeric( dbhandle, "port", atoi( port ));
156 if( user ) dbi_conn_set_option( dbhandle, "username", user );
157 if( pw ) dbi_conn_set_option( dbhandle, "password", pw );
158 if( db ) dbi_conn_set_option( dbhandle, "dbname", db );
167 if( dbi_conn_connect( dbhandle ) < 0 ) {
169 if( dbi_conn_connect( dbhandle ) < 0 ) {
170 dbi_conn_error( dbhandle, &err );
171 osrfLogError( OSRF_LOG_MARK, "Error connecting to database: %s", err );
176 oilsSetDBConnection( dbhandle );
177 osrfLogInfo( OSRF_LOG_MARK, "%s successfully connected to the database", modulename );
179 // Add datatypes from database to the fields in the IDL
180 //if( oilsExtendIDL() ) {
181 // osrfLogError( OSRF_LOG_MARK, "Error extending the IDL" );
189 @brief Load a specified query from the database query tables.
190 @param ctx Pointer to the current method context.
191 @return Zero if successful, or -1 if not.
194 - query id (key of query.stored_query table)
196 Returns: a character string serving as a token for future references to the query.
198 NB: the method return type is temporary. Eventually this method will return both a token
199 and a list of bind variables.
201 int doPrepare( osrfMethodContext* ctx ) {
202 if(osrfMethodVerifyContext( ctx )) {
203 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
207 // Get the query id from a method parameter
208 const jsonObject* query_id_obj = jsonObjectGetIndex( ctx->params, 0 );
209 if( query_id_obj->type != JSON_NUMBER ) {
210 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
211 ctx->request, "Invalid parameter; query id must be a number" );
215 int query_id = atoi( jsonObjectGetString( query_id_obj ));
216 if( query_id <= 0 ) {
217 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
218 ctx->request, "Invalid parameter: query id must be greater than zero" );
222 osrfLogInfo( OSRF_LOG_MARK, "Loading query for id # %d", query_id );
224 BuildSQLState* state = buildSQLStateNew( dbhandle );
225 StoredQ* query = getStoredQuery( state, query_id );
227 osrfLogWarning( OSRF_LOG_MARK, "Unable to load stored query # %d", query_id );
228 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
229 ctx->request, "Unable to load stored query" );
233 const char* token = save_query( ctx, state, query );
235 osrfLogInfo( OSRF_LOG_MARK, "Token for query id # %d is \"%s\"", query_id, token );
237 osrfAppRespondComplete( ctx, jsonNewObject( token ));
241 int doBindParam( osrfMethodContext* ctx ) {
242 if(osrfMethodVerifyContext( ctx )) {
243 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
247 // Get the query token from a method parameter
248 const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
249 if( token_obj->type != JSON_STRING ) {
250 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
251 ctx->request, "Invalid parameter; query token must be a string" );
254 const char* token = jsonObjectGetString( token_obj );
256 // Look up the query token in the session-level userData
257 CachedQuery* query = search_token( ctx, token );
259 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
260 ctx->request, "Invalid query token" );
264 osrfLogInfo( OSRF_LOG_MARK, "Binding parameter(s) for token %s", token );
266 osrfAppRespondComplete( ctx, jsonNewObject( "build method not yet implemented" ));
271 @brief Execute an SQL query and return a result set.
272 @param ctx Pointer to the current method context.
273 @return Zero if successful, or -1 if not.
276 - query token, as previously returned by the .prepare method.
278 Returns: A series of responses, each of them a row represented as an array of column values.
280 int doExecute( osrfMethodContext* ctx ) {
281 if(osrfMethodVerifyContext( ctx )) {
282 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
286 // Get the query token
287 const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
288 if( token_obj->type != JSON_STRING ) {
289 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
290 ctx->request, "Invalid parameter; query token must be a string" );
293 const char* token = jsonObjectGetString( token_obj );
295 // Look up the query token in the session-level userData
296 CachedQuery* query = search_token( ctx, token );
298 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
299 ctx->request, "Invalid query token" );
303 osrfLogInfo( OSRF_LOG_MARK, "Executing query for token \"%s\"", token );
304 if( query->state->error ) {
305 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
306 "No valid prepared query available for query id # %d", query->query->id ));
307 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
308 ctx->request, "No valid prepared query available" );
310 } else if( buildSQL( query->state, query->query )) {
311 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
312 "Unable to build SQL statement for query id # %d", query->query->id ));
313 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
314 ctx->request, "Unable to build SQL statement" );
318 jsonObject* row = oilsFirstRow( query->state );
320 osrfAppRespond( ctx, row );
321 row = oilsNextRow( query->state );
324 if( query->state->error ) {
325 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
326 "Unable to execute SQL statement for query id # %d", query->query->id ));
327 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
328 ctx->request, "Unable to execute SQL statement" );
332 osrfAppRespondComplete( ctx, NULL );
337 @brief Construct an SQL query, but without executing it.
338 @param ctx Pointer to the current method context.
339 @return Zero if successful, or -1 if not.
342 - query token, as previously returned by the .prepare method.
344 Returns: A string containing an SQL query..
346 int doSql( osrfMethodContext* ctx ) {
347 if(osrfMethodVerifyContext( ctx )) {
348 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
352 // Get the query token
353 const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
354 if( token_obj->type != JSON_STRING ) {
355 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
356 ctx->request, "Invalid parameter; query token must be a string" );
359 const char* token = jsonObjectGetString( token_obj );
361 // Look up the query token in the session-level userData
362 CachedQuery* query = search_token( ctx, token );
364 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
365 ctx->request, "Invalid query token" );
369 osrfLogInfo( OSRF_LOG_MARK, "Returning SQL for token \"%s\"", token );
370 if( query->state->error ) {
371 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
372 "No valid prepared query available for query id # %d", query->query->id ));
373 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
374 ctx->request, "No valid prepared query available" );
376 } else if( buildSQL( query->state, query->query )) {
377 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
378 "Unable to build SQL statement for query id # %d", query->query->id ));
379 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
380 ctx->request, "Unable to build SQL statement" );
384 osrfAppRespondComplete( ctx, jsonNewObject( OSRF_BUFFER_C_STR( query->state->sql )));
389 @brief Return a list of previously generated error messages for a specified query.
390 @param ctx Pointer to the current method context.
391 @return Zero if successful, or -1 if not.
394 - query token, as previously returned by the .prepare method.
396 Returns: A (possibly empty) array of strings, each one an error message generated during
397 previous operations in connection with the specified query.
399 int doMessages( osrfMethodContext* ctx ) {
400 if(osrfMethodVerifyContext( ctx )) {
401 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
405 // Get the query token from a method parameter
406 const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
407 if( token_obj->type != JSON_STRING ) {
408 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
409 ctx->request, "Invalid parameter; query token must be a string" );
412 const char* token = jsonObjectGetString( token_obj );
414 // Look up the query token in the session-level userData
415 CachedQuery* query = search_token( ctx, token );
417 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
418 ctx->request, "Invalid query token" );
422 osrfLogInfo( OSRF_LOG_MARK, "Returning messages for token %s", token );
424 jsonObject* msgs = jsonNewObjectType( JSON_ARRAY );
425 const osrfStringArray* error_msgs = query->state->error_msgs;
427 for( i = 0; i < error_msgs->size; ++i ) {
428 jsonObject* msg = jsonNewObject( osrfStringArrayGetString( error_msgs, i ));
429 jsonObjectPush( msgs, msg );
432 osrfAppRespondComplete( ctx, msgs );
437 @brief Discard a previously stored query, as identified by a token.
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.
446 int doFinish( osrfMethodContext* ctx ) {
447 if(osrfMethodVerifyContext( ctx )) {
448 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
452 // Get the query token.
453 const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
454 if( token_obj->type != JSON_STRING ) {
455 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
456 ctx->request, "Invalid parameter; query token must be a string" );
459 const char* token = jsonObjectGetString( token_obj );
461 // Delete the corresponding entry from the cache. If there is no cache, or no such entry,
462 // just ignore the problem and report success.
463 osrfHash* cache = ctx->session->userData;
465 osrfHashRemove( cache, token );
467 osrfAppRespondComplete( ctx, NULL );
472 @brief Save a query in session-level userData for reference in future method calls.
473 @param ctx Pointer to the current method context.
474 @param state Pointer to the state of the query.
475 @param query Pointer to the abstract representation of the query.
476 @return Pointer to an identifying token to be returned to the client.
478 static const char* save_query(
479 osrfMethodContext* ctx, BuildSQLState* state, StoredQ* query ) {
481 CachedQuery* cached_query = safe_malloc( sizeof( CachedQuery ));
482 cached_query->state = state;
483 cached_query->query = query;
485 // Get the cache. If we don't have one yet, make one.
486 osrfHash* cache = ctx->session->userData;
488 cache = osrfNewHash();
489 osrfHashSetCallback( cache, free_cached_query );
490 ctx->session->userData = cache;
491 ctx->session->userDataFree = userDataFree; // arrange to free it at end of session
494 // Create a token string to be used as a key
495 static unsigned int token_count = 0;
496 char* token = va_list_to_string(
497 "%u_%ld_%ld", ++token_count, (long) time( NULL ), (long) getpid() );
499 osrfHashSet( cache, cached_query, token );
504 @brief Free a CachedQuery
505 @param Pointer to the CachedQuery to be freed.
507 static void free_cached_query( char* key, void* data ) {
509 CachedQuery* cached_query = data;
510 buildSQLStateFree( cached_query->state );
511 storedQFree( cached_query->query );
516 @brief Callback for freeing session-level userData.
517 @param blob Opaque pointer t userData.
519 static void userDataFree( void* blob ) {
520 osrfHashFree( (osrfHash*) blob );
524 @brief Search for the cached query corresponding to a given token.
525 @param ctx Pointer to the current method context.
526 @param token Token string from a previous call to the prepare method.
527 @return A pointer to the cached query, if found, or NULL if not.
529 static CachedQuery* search_token( osrfMethodContext* ctx, const char* token ) {
530 if( ctx && ctx->session->userData && token ) {
531 osrfHash* cache = ctx->session->userData;
532 return osrfHashGet( cache, token );