From 528276d8ec811e37b5be663cc46784db7e1921b9 Mon Sep 17 00:00:00 2001 From: scottmk Date: Mon, 19 Apr 2010 16:04:26 +0000 Subject: [PATCH] Fleshing out the qstore server a bit: 1. Add a .bind_param method. 2. In the .prepare method: generate a query token, return it, and save it in session-level userDate. 3. In other methods: look up the supplied query token. A Open-ILS/include/openils/oils_buildq.h M Open-ILS/src/c-apps/oils_qstore.c git-svn-id: svn://svn.open-ils.org/ILS/trunk@16274 dcc99617-32d9-48b4-a31d-7c20da2025e4 --- Open-ILS/include/openils/oils_buildq.h | 196 +++++++++++++++++++++++++ Open-ILS/src/c-apps/oils_qstore.c | 155 +++++++++++++++++-- 2 files changed, 340 insertions(+), 11 deletions(-) create mode 100644 Open-ILS/include/openils/oils_buildq.h diff --git a/Open-ILS/include/openils/oils_buildq.h b/Open-ILS/include/openils/oils_buildq.h new file mode 100644 index 0000000000..994eb715dc --- /dev/null +++ b/Open-ILS/include/openils/oils_buildq.h @@ -0,0 +1,196 @@ +/** + @file buildquery.h + @brief Header for routines for building database queries. +*/ + +#ifndef OILS_BUILDQ_H +#define OILS_BUILDQ_H + +#ifdef __cplusplus +extern "C" { +#endif + +struct StoredQ_; +typedef struct StoredQ_ StoredQ; + +struct FromRelation_; +typedef struct FromRelation_ FromRelation; + +struct SelectItem_; +typedef struct SelectItem_ SelectItem; + +struct Expression_; +typedef struct Expression_ Expression; + +struct QSeq_; +typedef struct QSeq_ QSeq; + +struct OrderItem_; +typedef struct OrderItem_ OrderItem; + +struct BuildSQLState_; +typedef struct BuildSQLState_ BuildSQLState; + +struct IdNode_; +typedef struct IdNode_ IdNode; + +/** + @brief Stores various things related to the construction of an SQL query. + + This struct carries around various bits and scraps of context for constructing an SQL + query. It also provides a way for buildSQLQuery() to return more than one kind of thing + to its caller. In particular it can return a status code, a list of error messages, and + (if there is no error) an SQL string. +*/ +struct BuildSQLState_ { + dbi_conn dbhandle; /**< Handle for the database connection */ + int error; /**< Boolean; true if an error has occurred */ + osrfStringArray* error_msgs; /**< Descriptions of errors, if any */ + growing_buffer* sql; /**< To hold the constructed query */ + IdNode* query_stack; /**< For avoiding infinite recursion of nested queries */ + IdNode* expr_stack; /**< For avoiding infinite recursion of nested expressions */ + IdNode* from_stack; /**< For avoiding infinite recursion of from clauses */ + int indent; /**< For prettifying output: level of indentation */ +}; + +typedef enum { + QT_SELECT, + QT_UNION, + QT_INTERSECT, + QT_EXCEPT +} QueryType; + +struct StoredQ_ { + StoredQ* next; + int id; + QueryType type; + int use_all; /**< Boolean */ + int use_distinct; /**< Boolean */ + FromRelation* from_clause; + Expression* where_clause; + SelectItem* select_list; + QSeq* child_list; + OrderItem* order_by_list; +}; + +typedef enum { + FRT_RELATION, + FRT_SUBQUERY, + FRT_FUNCTION +} FromRelationType; + +typedef enum { + JT_NONE, + JT_INNER, + JT_LEFT, + JT_RIGHT, + JT_FULL +} JoinType; + +struct FromRelation_ { + FromRelation* next; + int id; + FromRelationType type; + char* table_name; + char* class_name; + int subquery_id; + StoredQ* subquery; + int function_call_id; + char* table_alias; + int parent_relation_id; + int seq_no; + JoinType join_type; + Expression* on_clause; + FromRelation* join_list; +}; + +struct SelectItem_ { + SelectItem* next; + int id; + int stored_query_id; + int seq_no; + Expression* expression; + char* column_alias; + int grouped_by; // Boolean +}; + +typedef enum { + EXP_BETWEEN, + EXP_BOOL, + EXP_CASE, + EXP_CAST, + EXP_COLUMN, + EXP_EXIST, + EXP_FIELD, + EXP_FUNCTION, + EXP_IN, + EXP_NOT_BETWEEN, + EXP_NOT_EXIST, + EXP_NOT_IN, + EXP_NULL, + EXP_NUMBER, + EXP_OPERATOR, + EXP_STRING, + EXP_SUBQUERY +} ExprType; + +struct Expression_ { + Expression* next; + int id; + ExprType type; + int parenthesize; // Boolean + int parent_expr_id; + int seq_no; + char* literal; + char* table_alias; + char* column_name; + Expression* left_operand; + char* op; + Expression* right_operand; + int function_id; + int subquery_id; + StoredQ* subquery; + int cast_type_id; +}; + +struct QSeq_ { + QSeq* next; + int id; + int parent_query_id; + int seq_no; + StoredQ* child_query; +}; + +struct OrderItem_ { + OrderItem* next; + int id; + int stored_query_id; + int seq_no; + Expression* expression; +}; + +BuildSQLState* buildSQLStateNew( dbi_conn dbhandle ); + +void buildSQLStateFree( BuildSQLState* state ); + +void buildSQLCleanup( void ); + +const char* sqlAddMsg( BuildSQLState* state, const char* msg, ... ); + +StoredQ* getStoredQuery( BuildSQLState* state, int query_id ); + +void pop_id( IdNode** stack ); + +void storedQFree( StoredQ* sq ); + +void storedQCleanup( void ); + +int buildSQL( BuildSQLState* state, StoredQ* query ); + +void oilsStoredQSetVerbose( void ); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/Open-ILS/src/c-apps/oils_qstore.c b/Open-ILS/src/c-apps/oils_qstore.c index e22e5d4e66..1ff05a33e7 100644 --- a/Open-ILS/src/c-apps/oils_qstore.c +++ b/Open-ILS/src/c-apps/oils_qstore.c @@ -13,6 +13,19 @@ #include "opensrf/osrf_application.h" #include "openils/oils_utils.h" #include "openils/oils_sql.h" +#include "openils/oils_buildq.h" + +/** + @brief Information about a previously prepared query. + + We store an osrfHash of CachedQueries in the userData area of the application session, + keyed on query token. That way we can fetch what a previous call to the prepare method + has prepared. +*/ +typedef struct { + StoredQ* query; + jsonObject* bind_map; +} CachedQuery; static dbi_conn dbhandle; /* our db connection */ @@ -22,6 +35,11 @@ int doPrepare( osrfMethodContext* ctx ); int doExecute( osrfMethodContext* ctx ); int doSql( osrfMethodContext* ctx ); +static const char* save_query( osrfMethodContext* ctx, StoredQ* query, jsonObject* bind_map ); +static void free_cached_query( char* key, void* data ); +static void userDataFree( void* blob ); +static CachedQuery* search_token( osrfMethodContext* ctx, const char* token ); + /** @brief Disconnect from the database. @@ -63,7 +81,13 @@ int osrfAppInitialize() { OSRF_BUFFER_ADD( method_name, modulename ); OSRF_BUFFER_ADD( method_name, ".prepare" ); osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ), - "doBuild", "", 1, 0 ); + "doPrepare", "", 1, 0 ); + + buffer_reset( method_name ); + OSRF_BUFFER_ADD( method_name, modulename ); + OSRF_BUFFER_ADD( method_name, ".bind_param" ); + osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ), + "doBindParam", "", 2, 0 ); buffer_reset( method_name ); OSRF_BUFFER_ADD( method_name, modulename ); @@ -148,6 +172,19 @@ int osrfAppChildInit() { return 0; } +/** + @brief Load a specified query from the database query tables. + @param ctx Pointer to the current method context. + @return Zero if successful, or -1 if not. + + Method parameters: + - query id (key of query.stored_query table) + + Returns: a character string serving as a token for future references to the query. + + NB: the method return type is temporary. Eventually this method will return both a token + and a list of bind variables. +*/ int doPrepare( osrfMethodContext* ctx ) { if(osrfMethodVerifyContext( ctx )) { osrfLogError( OSRF_LOG_MARK, "Invalid method context" ); @@ -161,6 +198,7 @@ int doPrepare( osrfMethodContext* ctx ) { ctx->request, "Invalid parameter; query id must be a number" ); return -1; } + int query_id = atoi( jsonObjectGetString( query_id_obj )); if( query_id <= 0 ) { osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException", @@ -170,6 +208,42 @@ int doPrepare( osrfMethodContext* ctx ) { osrfLogInfo( OSRF_LOG_MARK, "Building query for id # %d", query_id ); + // To do: prepare query + StoredQ* query = NULL; + jsonObject* bind_map = NULL; + const char* token = save_query( ctx, query, bind_map ); + + osrfLogInfo( OSRF_LOG_MARK, "Token for query id # %d is \"%s\"", query_id, token ); + + osrfAppRespondComplete( ctx, jsonNewObject( token )); + return 0; +} + +int doBindParam( osrfMethodContext* ctx ) { + if(osrfMethodVerifyContext( ctx )) { + osrfLogError( OSRF_LOG_MARK, "Invalid method context" ); + return -1; + } + + // Get the query token from a method parameter + const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 ); + if( token_obj->type != JSON_STRING ) { + osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException", + ctx->request, "Invalid parameter; query token must be a string" ); + return -1; + } + const char* token = jsonObjectGetString( token_obj ); + + // Look up the query token in the session-level userData + CachedQuery* query = search_token( ctx, token ); + if( !query ) { + osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException", + ctx->request, "Invalid query token" ); + return -1; + } + + osrfLogInfo( OSRF_LOG_MARK, "Binding parameter(s) for token %s", token ); + osrfAppRespondComplete( ctx, jsonNewObject( "build method not yet implemented" )); return 0; } @@ -184,16 +258,16 @@ int doExecute( osrfMethodContext* ctx ) { const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 ); if( token_obj->type != JSON_STRING ) { osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException", - ctx->request, "Invalid parameter; query id must be a string" ); + ctx->request, "Invalid parameter; query token must be a string" ); return -1; } const char* token = jsonObjectGetString( token_obj ); - // Get the list of bind variables, if there is one - jsonObject* bind_map = jsonObjectGetIndex( ctx->params, 1 ); - if( bind_map && bind_map->type != JSON_HASH ) { + // Look up the query token in the session-level userData + CachedQuery* query = search_token( ctx, token ); + if( !query ) { osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException", - ctx->request, "Invalid parameter; bind map must be a JSON object" ); + ctx->request, "Invalid query token" ); return -1; } @@ -213,16 +287,16 @@ int doSql( osrfMethodContext* ctx ) { const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 ); if( token_obj->type != JSON_STRING ) { osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException", - ctx->request, "Invalid parameter; query id must be a string" ); + ctx->request, "Invalid parameter; query token must be a string" ); return -1; } const char* token = jsonObjectGetString( token_obj ); - // Get the list of bind variables, if there is one - jsonObject* bind_map = jsonObjectGetIndex( ctx->params, 1 ); - if( bind_map && bind_map->type != JSON_HASH ) { + // Look up the query token in the session-level userData + CachedQuery* query = search_token( ctx, token ); + if( !query ) { osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException", - ctx->request, "Invalid parameter; bind map must be a JSON object" ); + ctx->request, "Invalid query token" ); return -1; } @@ -231,3 +305,62 @@ int doSql( osrfMethodContext* ctx ) { osrfAppRespondComplete( ctx, jsonNewObject( "sql method not yet implemented" )); return 0; } + +static const char* save_query( osrfMethodContext* ctx, StoredQ* query, jsonObject* bind_map ) { + + CachedQuery* cached_query = safe_malloc( sizeof( CachedQuery )); + cached_query->query = query; + cached_query->bind_map = bind_map; + + // Get the cache. If we don't have one yet, make one. + osrfHash* cache = ctx->session->userData; + if( !cache ) { + cache = osrfNewHash(); + osrfHashSetCallback( cache, free_cached_query ); + ctx->session->userData = cache; + ctx->session->userDataFree = userDataFree; // arrange to free it at end of session + } + + // Create a token string to be used as a key + static unsigned int token_count = 0; + char* token = va_list_to_string( + "%u_%ld_%ld", ++token_count, (long) time( NULL ), (long) getpid() ); + + osrfHashSet( cache, cached_query, token ); + return token; +} + +/** + @brief Free a CachedQuery + @param Pointer to the CachedQuery to be freed. +*/ +static void free_cached_query( char* key, void* data ) { + if( data ) { + CachedQuery* cached_query = data; + //storedQFree( cached_query->query ); + if( cached_query->bind_map ) + jsonObjectFree( cached_query->bind_map ); + } +} + +/** + @brief Callback for freeing session-level userData. + @param blob Opaque pointer t userData. +*/ +static void userDataFree( void* blob ) { + osrfHashFree( (osrfHash*) blob ); +} + +/** + @brief Search for the cached query corresponding to a given token. + @param ctx Pointer to the current method context. + @param token Token string from a previous call to the prepare method. + @return A pointer to the cached query, if found, or NULL if not. +*/ +static CachedQuery* search_token( osrfMethodContext* ctx, const char* token ) { + if( ctx && ctx->session->userData && token ) { + osrfHash* cache = ctx->session->userData; + return osrfHashGet( cache, token ); + } else + return NULL; +} -- 2.43.2