From 32a8104f189a35fc32a7708b83e300d1a94841a1 Mon Sep 17 00:00:00 2001 From: scottmk Date: Wed, 19 May 2010 12:46:18 +0000 Subject: [PATCH] Add partial support for bind variables: load them from the database, and use default values if available. There is no mechanism yet to override the defaults. git-svn-id: svn://svn.open-ils.org/ILS/trunk@16450 dcc99617-32d9-48b4-a31d-7c20da2025e4 --- Open-ILS/include/openils/oils_buildq.h | 27 ++++ Open-ILS/src/c-apps/buildSQL.c | 159 ++++++++++++++++++- Open-ILS/src/c-apps/oils_buildq.c | 26 +++- Open-ILS/src/c-apps/oils_qstore.c | 6 + Open-ILS/src/c-apps/oils_storedq.c | 204 ++++++++++++++++++++++++- 5 files changed, 409 insertions(+), 13 deletions(-) diff --git a/Open-ILS/include/openils/oils_buildq.h b/Open-ILS/include/openils/oils_buildq.h index 756b3f8b8c..5ddc7b7864 100644 --- a/Open-ILS/include/openils/oils_buildq.h +++ b/Open-ILS/include/openils/oils_buildq.h @@ -21,6 +21,9 @@ typedef struct FromRelation_ FromRelation; struct SelectItem_; typedef struct SelectItem_ SelectItem; +struct BindVar_; +typedef struct BindVar_ BindVar; + struct Expression_; typedef struct Expression_ Expression; @@ -50,10 +53,15 @@ struct BuildSQLState_ { 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 */ + osrfHash* bindvar_list; /**< List of bind variables used by this query, each with + a pointer to the corresponding BindVar. */ 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 SQL output: level of indentation */ + int defaults_usable; /**< Boolean; if true, we can use unconfirmed default + values for bind variables */ + int values_required; /**< Boolean: if true, we need values for a bind variables */ }; typedef enum { @@ -118,8 +126,26 @@ struct SelectItem_ { int grouped_by; // Boolean }; +typedef enum { + BIND_STR, + BIND_NUM, + BIND_STR_LIST, + BIND_NUM_LIST +} BindVarType; + +struct BindVar_ { + BindVar* next; + char* name; + char* label; + BindVarType type; + char* description; + jsonObject* default_value; + jsonObject* actual_value; +}; + typedef enum { EXP_BETWEEN, + EXP_BIND, EXP_BOOL, EXP_CASE, EXP_CAST, @@ -153,6 +179,7 @@ struct Expression_ { StoredQ* subquery; int cast_type_id; int negate; // Boolean + BindVar* bind; }; struct QSeq_ { diff --git a/Open-ILS/src/c-apps/buildSQL.c b/Open-ILS/src/c-apps/buildSQL.c index 5f5052106d..cc1c5b8010 100644 --- a/Open-ILS/src/c-apps/buildSQL.c +++ b/Open-ILS/src/c-apps/buildSQL.c @@ -9,6 +9,7 @@ #include #include "opensrf/utils.h" #include "opensrf/string_array.h" +#include "opensrf/osrf_hash.h" #include "opensrf/osrf_application.h" #include "openils/oils_idl.h" #include "openils/oils_sql.h" @@ -22,6 +23,8 @@ static void buildJoin( BuildSQLState* state, FromRelation* join ); static void buildSelectList( BuildSQLState* state, SelectItem* item ); static void buildOrderBy( BuildSQLState* state, OrderItem* ord_list ); static void buildExpression( BuildSQLState* state, Expression* expr ); +static void buildBindVar( BuildSQLState* state, BindVar* bind ); +static void buildScalar( BuildSQLState* state, int numeric, const jsonObject* obj ); static void add_newline( BuildSQLState* state ); static inline void incr_indent( BuildSQLState* state ); @@ -171,9 +174,9 @@ static void buildSelect( BuildSQLState* state, StoredQ* query ) { decr_indent( state ); } - // To do: build GROUP BY clause, if there is one + // To do: build GROUP BY clause, if there is one - // Build HAVING clause, if there is one + // Build HAVING clause, if there is one if( query->having_clause ) { add_newline( state ); buffer_add( state->sql, "HAVING" ); @@ -187,7 +190,7 @@ static void buildSelect( BuildSQLState* state, StoredQ* query ) { } decr_indent( state ); } - + // Build ORDER BY clause, if there is one if( query->order_by_list ) { buildOrderBy( state, query->order_by_list ); @@ -453,6 +456,14 @@ static void buildExpression( BuildSQLState* state, Expression* expr ) { sqlAddMsg( state, "BETWEEN expressions not yet supported" ); state->error = 1; break; + case EXP_BIND : + if( !expr->bind ) { // Sanity check + osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state, + "Internal error: no variable for bind variable expression" )); + state->error = 1; + } else + buildBindVar( state, expr->bind ); + break; case EXP_BOOL : if( expr->negate ) buffer_add( state->sql, "NOT " ); @@ -471,6 +482,9 @@ static void buildExpression( BuildSQLState* state, Expression* expr ) { state->error = 1; break; case EXP_CAST : // Type cast + if( expr->negate ) + buffer_add( state->sql, "NOT " ); + sqlAddMsg( state, "Cast expressions not yet supported" ); state->error = 1; break; @@ -509,6 +523,9 @@ static void buildExpression( BuildSQLState* state, Expression* expr ) { } break; case EXP_FIELD : + if( expr->negate ) + buffer_add( state->sql, "NOT " ); + sqlAddMsg( state, "Field expressions not yet supported" ); state->error = 1; break; @@ -589,6 +606,7 @@ static void buildExpression( BuildSQLState* state, Expression* expr ) { "Internal error: No string value in string expression # %d", expr->id )); state->error = 1; } else { + // To do: escape special characters in the string buffer_add_char( state->sql, '\'' ); buffer_add( state->sql, expr->literal ); buffer_add_char( state->sql, '\'' ); @@ -617,6 +635,141 @@ static void buildExpression( BuildSQLState* state, Expression* expr ) { buffer_add_char( state->sql, ')' ); } +/** + @brief Add the value of a bind variable to an SQL statement. + @param state Pointer to the query-building context. + @param bind Pointer to the bind variable whose value is to be added to the SQL. + + The value may be a null, a scalar, or an array of nulls and/or scalars, depending on + the type of the bind variable. +*/ +static void buildBindVar( BuildSQLState* state, BindVar* bind ) { + + // Decide where to get the value, if any + const jsonObject* value = NULL; + if( bind->actual_value ) + value = bind->actual_value; + else if( bind->default_value ) { + if( state->defaults_usable ) + value = bind->default_value; + else { + sqlAddMsg( state, "No confirmed value available for bind variable \"%s\"", + bind->name ); + state->error = 1; + return; + } + } else if( state->values_required ) { + sqlAddMsg( state, "No value available for bind variable \"%s\"", bind->name ); + state->error = 1; + return; + } else { + // No value available, and that's okay. Emit the name of the bind variable. + buffer_add_char( state->sql, ':' ); + buffer_add( state->sql, bind->name ); + return; + } + + // If we get to this point, we know that a value is available. Carry on. + + int numeric = 0; // Boolean + if( BIND_NUM == bind->type || BIND_NUM_LIST == bind->type ) + numeric = 1; + + // Emit the value + switch( bind->type ) { + case BIND_STR : + case BIND_NUM : + buildScalar( state, numeric, value ); + break; + case BIND_STR_LIST : + case BIND_NUM_LIST : + if( JSON_ARRAY == value->type ) { + // Iterate over array, emit each value + int first = 1; // Boolean + unsigned long max = value->size; + unsigned long i = 0; + while( i < max ) { + if( first ) + first = 0; + else + buffer_add( state->sql, ", " ); + + buildScalar( state, numeric, jsonObjectGetIndex( value, i )); + ++i; + } + } else { + osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state, + "Invalid value for bind variable; expected a list of values" )); + state->error = 1; + } + break; + default : + osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state, + "Internal error: invalid type for bind variable" )); + state->error = 1; + break; + } + + if( state->error ) + osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state, + "Unable to emit value of bind variable \"%s\"", bind->name )); +} + +/** + @brief Add a number or quoted string to an SQL statement. + @param state Pointer to the query-building context. + @param numeric Boolean; true if the value is expected to be a number + @param obj Pointer to the jsonObject whose value is to be added to the SQL. +*/ +static void buildScalar( BuildSQLState* state, int numeric, const jsonObject* obj ) { + switch( obj->type ) { + case JSON_HASH : + osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state, + "Internal error: hash value for bind variable" )); + state->error = 1; + break; + case JSON_ARRAY : + osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state, + "Internal error: array value for bind variable" )); + state->error = 1; + break; + case JSON_STRING : + if( numeric ) { + sqlAddMsg( state, + "Invalid value for bind variable: expected a string, found a number" ); + state->error = 1; + } else { + // To do: escape special characters in the string + buffer_add_char( state->sql, '\'' ); + buffer_add( state->sql, jsonObjectGetString( obj )); + buffer_add_char( state->sql, '\'' ); + } + break; + case JSON_NUMBER : + if( numeric ) { + buffer_add( state->sql, jsonObjectGetString( obj )); + } else { + sqlAddMsg( state, + "Invalid value for bind variable: expected a number, found a string" ); + state->error = 1; + } + break; + case JSON_NULL : + buffer_add( state->sql, "NULL" ); + break; + case JSON_BOOL : + osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state, + "Internal error: boolean value for bind variable" )); + state->error = 1; + break; + default : + osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state, + "Internal error: corrupted value for bind variable" )); + state->error = 1; + break; + } +} + static void add_newline( BuildSQLState* state ) { buffer_add_char( state->sql, '\n' ); diff --git a/Open-ILS/src/c-apps/oils_buildq.c b/Open-ILS/src/c-apps/oils_buildq.c index f460c64e6d..2f8fd23d51 100644 --- a/Open-ILS/src/c-apps/oils_buildq.c +++ b/Open-ILS/src/c-apps/oils_buildq.c @@ -23,14 +23,18 @@ */ BuildSQLState* buildSQLStateNew( dbi_conn dbhandle ) { - BuildSQLState* state = safe_malloc( sizeof( BuildSQLState ) ); - state->dbhandle = dbhandle; - state->error = 0; - state->error_msgs = osrfNewStringArray( 16 ); - state->sql = buffer_init( 128 ); - state->query_stack = NULL; - state->expr_stack = NULL; - state->indent = 0; + BuildSQLState* state = safe_malloc( sizeof( BuildSQLState ) ); + state->dbhandle = dbhandle; + state->result = NULL; + state->error = 0; + state->error_msgs = osrfNewStringArray( 16 ); + state->sql = buffer_init( 128 ); + state->bindvar_list = NULL; // Don't build it until we need it + state->query_stack = NULL; + state->expr_stack = NULL; + state->indent = 0; + state->defaults_usable = 0; + state->values_required = 0; return state; } @@ -53,8 +57,14 @@ const char* sqlAddMsg( BuildSQLState* state, const char* msg, ... ) { void buildSQLStateFree( BuildSQLState* state ){ if( state ) { + if( state->result ) { + dbi_result_free( state->result ); + state->result = NULL; + } osrfStringArrayFree( state->error_msgs ); buffer_free( state->sql ); + if( state->bindvar_list ) + osrfHashFree( state->bindvar_list ); while( state->query_stack ) pop_id( &state->query_stack ); while( state->expr_stack ) diff --git a/Open-ILS/src/c-apps/oils_qstore.c b/Open-ILS/src/c-apps/oils_qstore.c index 395b16b4de..cf37a33a35 100644 --- a/Open-ILS/src/c-apps/oils_qstore.c +++ b/Open-ILS/src/c-apps/oils_qstore.c @@ -77,6 +77,10 @@ int osrfAppInitialize() { if ( !oilsIDLInit( osrf_settings_host_value( "/IDL" ))) return 1; /* return non-zero to indicate error */ + // Set the SQL options. Here the second and third parameters are irrelevant, but we need + // to set the module name for use in error messages. + oilsSetSQLOptions( modulename, 0, 100 ); + growing_buffer* method_name = buffer_init( 64 ); OSRF_BUFFER_ADD( method_name, modulename ); @@ -188,6 +192,8 @@ int doPrepare( osrfMethodContext* ctx ) { osrfLogInfo( OSRF_LOG_MARK, "Loading query for id # %d", query_id ); BuildSQLState* state = buildSQLStateNew( dbhandle ); + state->defaults_usable = 1; + state->values_required = 0; StoredQ* query = getStoredQuery( state, query_id ); if( state->error ) { osrfLogWarning( OSRF_LOG_MARK, "Unable to load stored query # %d", query_id ); diff --git a/Open-ILS/src/c-apps/oils_storedq.c b/Open-ILS/src/c-apps/oils_storedq.c index 8a6a516b2a..6eac1326eb 100644 --- a/Open-ILS/src/c-apps/oils_storedq.c +++ b/Open-ILS/src/c-apps/oils_storedq.c @@ -9,6 +9,7 @@ #include "opensrf/utils.h" #include "opensrf/log.h" #include "opensrf/string_array.h" +#include "opensrf/osrf_hash.h" #include "openils/oils_buildq.h" #define PRINT if( verbose ) printf @@ -36,6 +37,10 @@ static SelectItem* getSelectList( BuildSQLState* state, int query_id ); static SelectItem* constructSelectItem( BuildSQLState* state, dbi_result result ); static void selectListFree( SelectItem* sel ); +static BindVar* getBindVar( BuildSQLState* state, const char* name ); +static BindVar* constructBindVar( BuildSQLState* state, dbi_result result ); +static void bindVarFree( char* name, void* p ); + static Expression* getExpression( BuildSQLState* state, int id ); static Expression* constructExpression( BuildSQLState* state, dbi_result result ); static void expressionFree( Expression* exp ); @@ -52,6 +57,7 @@ static const IdNode* searchIdStack( const IdNode* stack, int id, const char* ali static StoredQ* free_storedq_list = NULL; static FromRelation* free_from_relation_list = NULL; static SelectItem* free_select_item_list = NULL; +static BindVar* free_bindvar_list = NULL; static Expression* free_expression_list = NULL; static IdNode* free_id_node_list = NULL; static QSeq* free_qseq_list = NULL; @@ -221,6 +227,7 @@ static StoredQ* constructStoredQ( BuildSQLState* state, dbi_result result ) { freeQSeqList( child_list ); fromRelationFree( from_clause ); selectListFree( select_list ); + state->error = 1; return NULL; } } @@ -236,6 +243,7 @@ static StoredQ* constructStoredQ( BuildSQLState* state, dbi_result result ) { freeQSeqList( child_list ); fromRelationFree( from_clause ); selectListFree( select_list ); + state->error = 1; return NULL; } } @@ -613,6 +621,7 @@ static FromRelation* constructFromRelation( BuildSQLState* state, dbi_result res osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state, "Unable to load ON condition for FROM relation # %d", id )); joinListFree( join_list ); + state->error = 1; return NULL; } else @@ -810,6 +819,7 @@ static SelectItem* constructSelectItem( BuildSQLState* state, dbi_result result if( !expression ) { osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state, "Unable to fetch expression for id = %d", expression_id )); + state->error = 1; return NULL; }; @@ -833,6 +843,13 @@ static SelectItem* constructSelectItem( BuildSQLState* state, dbi_result result return sel; } +/** + @brief Free a list of SelectItems. + @param sel Pointer to the first item in the list to be freed. + + Free the column alias and expression owned by each item. Put the entire list into a free + list of SelectItems. +*/ static void selectListFree( SelectItem* sel ) { if( !sel ) return; // Nothing to free @@ -855,6 +872,152 @@ static void selectListFree( SelectItem* sel ) { free_select_item_list = first; } +/** + @brief Given the name of a bind variable, build a corresponding BindVar. + @param state Pointer to the query-building context. + @param name Name of the bind variable. + @return Pointer to the newly-built BindVar. + + Since the same bind variable may appear multiple times, we load it only once for the + entire query, and reference the one copy wherever needed. +*/ +static BindVar* getBindVar( BuildSQLState* state, const char* name ) { + BindVar* bind = NULL; + if( state->bindvar_list ) { + bind = osrfHashGet( state->bindvar_list, name ); + if( bind ) + return bind; // Already loaded it... + } + + // Load a BindVar from the Database. + dbi_result result = dbi_conn_queryf( state->dbhandle, + "SELECT name, type, description, default_value, label " + "FROM query.bind_variable WHERE name = \'%s\';", name ); + if( result ) { + if( dbi_result_first_row( result ) ) { + bind = constructBindVar( state, result ); + if( bind ) { + PRINT( "Got a bind variable for %s\n", name ); + } else + osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state, + "Unable to load bind variable \"%s\"", name )); + } else { + osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state, + "No bind variable found with name \"%s\"", name )); + } + } else { + const char* msg; + int errnum = dbi_conn_error( state->dbhandle, &msg ); + osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state, + "Unable to query query.bind_variable table for \"%s\": #%d %s", + name, errnum, msg ? msg : "No description available" )); + } + + if( bind ) { + // Add the new bind variable to the list + if( !state->bindvar_list ) { + // Don't have a list yet? Start one. + state->bindvar_list = osrfNewHash(); + osrfHashSetCallback( state->bindvar_list, bindVarFree ); + } + osrfHashSet( state->bindvar_list, bind, name ); + } else + state->error = 1; + + return bind; +} + +/** + @brief Construct a BindVar to represent a bind variable. + @param Pointer to the query-building context. + @param result Database cursor positioned at a row in query.bind_variable. + @return Pointer to a newly constructed BindVar, if successful, or NULL if not. + + The calling code is responsible for freeing the BindVar by calling bindVarFree(). +*/ +static BindVar* constructBindVar( BuildSQLState* state, dbi_result result ) { + + const char* name = dbi_result_get_string_idx( result, 1 ); + + const char* type_str = dbi_result_get_string_idx( result, 2 ); + BindVarType type; + if( !strcmp( type_str, "string" )) + type = BIND_STR; + else if( !strcmp( type_str, "number" )) + type = BIND_NUM; + else if( !strcmp( type_str, "string_list" )) + type = BIND_STR_LIST; + else if( !strcmp( type_str, "number_list" )) + type = BIND_NUM_LIST;; + + const char* description = dbi_result_get_string_idx( result, 3 ); + + // The default value is encoded as JSON. Translate it into a jsonObject. + const char* default_value_str = dbi_result_get_string_idx( result, 4 ); + jsonObject* default_value = NULL; + if( default_value_str ) { + default_value = jsonParse( default_value_str ); + if( !default_value ) { + osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state, + "Unable to parse JSON string for default value of bind variable \"%s\"", + name )); + state->error = 1; + return NULL; + } + } + + const char* label = dbi_result_get_string_idx( result, 5 ); + + // Allocate a BindVar: from the free list if possible, from the heap if necessary + BindVar* bind = NULL; + if( free_bindvar_list ) { + bind = free_bindvar_list; + free_bindvar_list = free_bindvar_list->next; + } else + bind = safe_malloc( sizeof( BindVar ) ); + + bind->next = NULL; + bind->name = strdup( name ); + bind->label = strdup( label ); + bind->type = type; + bind->description = strdup( description ); + bind->default_value = default_value; + bind->actual_value = NULL; + + return bind; +} + +/** + @brief Deallocate a BindVar. + @param key Pointer to the bind variable name (not used). + @param p Pointer to the BindVar to be deallocated, cast to a void pointer. + + Free the strings and jsonObjects owned by the BindVar, and put the BindVar itself into a + free list. + + This function is a callback installed in an osrfHash; hence the peculiar signature. +*/ +static void bindVarFree( char* key, void* p ) { + if( p ) { + BindVar* bind = p; + free( bind->name ); + free( bind->label ); + free( bind->description ); + if( bind->default_value ) { + jsonObjectFree( bind->default_value ); + bind->default_value = NULL; + } + if( bind->actual_value ) { + jsonObjectFree( bind->actual_value ); + bind->actual_value = NULL; + } + + // Prepend to free list + bind->next = free_bindvar_list; + free_bindvar_list = bind; + } +} + static Expression* getExpression( BuildSQLState* state, int id ) { // Check the stack to see if the current expression is nested inside itself. If it is, @@ -868,10 +1031,11 @@ static Expression* getExpression( BuildSQLState* state, int id ) { } else push_id( &state->expr_stack, id, NULL ); - Expression* exp = NULL; + Expression* exp = NULL; dbi_result result = dbi_conn_queryf( state->dbhandle, "SELECT id, type, parenthesize, parent_expr, seq_no, literal, table_alias, column_name, " - "left_operand, operator, right_operand, function_id, subquery, cast_type, negate " + "left_operand, operator, right_operand, function_id, subquery, cast_type, negate, " + "bind_variable " "FROM query.expression WHERE id = %d;", id ); if( result ) { if( dbi_result_first_row( result ) ) { @@ -914,6 +1078,8 @@ static Expression* constructExpression( BuildSQLState* state, dbi_result result ExprType type; if( !strcmp( type_str, "xbet" )) type = EXP_BETWEEN; + else if( !strcmp( type_str, "xbind" )) + type = EXP_BIND; else if( !strcmp( type_str, "xbool" )) type = EXP_BOOL; else if( !strcmp( type_str, "xcase" )) @@ -989,10 +1155,12 @@ static Expression* constructExpression( BuildSQLState* state, dbi_result result cast_type_id = dbi_result_get_int_idx( result, 14 ); int negate = oils_result_get_bool_idx( result, 15 ); + const char* bind_variable = dbi_result_get_string_idx( result, 16 ); Expression* left_operand = NULL; Expression* right_operand = NULL; StoredQ* subquery = NULL; + BindVar* bind = NULL; if( EXP_OPERATOR == type ) { // Load left and/or right operands @@ -1092,6 +1260,25 @@ static Expression* constructExpression( BuildSQLState* state, dbi_result result } PRINT( "\tExpression is subquery %d\n", subquery_id ); } + } else if( EXP_BIND == type ) { + if( bind_variable ) { + // To do: Build a BindVar + bind = getBindVar( state, bind_variable ); + if( ! bind ) { + osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state, + "Unable to load bind variable \"%s\" for expression # %d", + bind_variable, id )); + state->error = 1; + return NULL; + } + PRINT( "\tBind variable is \"%s\"\n", bind_variable ); + } else { + osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state, + "No variable specified for bind variable expression # %d", + bind_variable, id )); + state->error = 1; + return NULL; + } } // Allocate an Expression: from the free list if possible, from the heap if necessary @@ -1120,6 +1307,7 @@ static Expression* constructExpression( BuildSQLState* state, dbi_result result exp->subquery = subquery; exp->cast_type_id = subquery_id; exp->negate = negate; + exp->bind = bind; return exp; } @@ -1152,7 +1340,10 @@ static void expressionFree( Expression* exp ) { storedQFree( exp->subquery ); exp->subquery = NULL; } + // We don't free the bind member here because the Expression doesn't own it; + // the bindvar_list hash owns it, so that multiple Expressions can reference it. + // Prepend to the free list exp->next = free_expression_list; free_expression_list = exp; } @@ -1225,6 +1416,7 @@ static OrderItem* constructOrderItem( BuildSQLState* state, dbi_result result ) if( !expression ) { osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state, "Unable to fetch ORDER BY expression for id = %d", expression_id )); + state->error = 1; return NULL; }; @@ -1470,6 +1662,14 @@ void storedQCleanup( void ) { free( ord ); ord = free_order_item_list; } + + // Free all the nodes in the bind variable free list + BindVar* bind = free_bindvar_list; + while( bind ) { + free_bindvar_list = bind->next; + free( bind ); + bind = free_bindvar_list; + } } /** -- 2.43.2