From b5233069195cb883edff6e3516ea294353c3cc28 Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Fri, 5 Oct 2018 15:01:18 -0700 Subject: [PATCH] Save meta data for the fetched result set (#855) * Save meta data on fetched result sets * Fixed a compilation error * Optimized some more -- metadata should be available when fetching * Skip conversion for strings of numeric values, integers, floats, decimals etc * Set encoding char for numeric data * Apply review --- source/pdo_sqlsrv/pdo_stmt.cpp | 9 ++- source/pdo_sqlsrv/php_pdo_sqlsrv.h | 2 - source/shared/core_sqlsrv.h | 4 + source/shared/core_stmt.cpp | 97 ++++++++++++++--------- source/sqlsrv/stmt.cpp | 123 ++++++++++++++++++----------- 5 files changed, 148 insertions(+), 87 deletions(-) diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index 3f6bc328..697ca40e 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -1377,6 +1377,7 @@ sqlsrv_phptype pdo_sqlsrv_stmt::sql_type_to_php_type( _In_ SQLINTEGER sql_type, } else { sqlsrv_phptype.typeinfo.type = SQLSRV_PHPTYPE_STRING; + sqlsrv_phptype.typeinfo.encoding = SQLSRV_ENCODING_CHAR; } break; case SQL_FLOAT: @@ -1386,6 +1387,7 @@ sqlsrv_phptype pdo_sqlsrv_stmt::sql_type_to_php_type( _In_ SQLINTEGER sql_type, } else { sqlsrv_phptype.typeinfo.type = SQLSRV_PHPTYPE_STRING; + sqlsrv_phptype.typeinfo.encoding = SQLSRV_ENCODING_CHAR; } break; case SQL_TYPE_DATE: @@ -1400,10 +1402,13 @@ sqlsrv_phptype pdo_sqlsrv_stmt::sql_type_to_php_type( _In_ SQLINTEGER sql_type, } break; case SQL_BIGINT: - case SQL_CHAR: case SQL_DECIMAL: - case SQL_GUID: case SQL_NUMERIC: + sqlsrv_phptype.typeinfo.type = SQLSRV_PHPTYPE_STRING; + sqlsrv_phptype.typeinfo.encoding = SQLSRV_ENCODING_CHAR; + break; + case SQL_CHAR: + case SQL_GUID: case SQL_WCHAR: case SQL_VARCHAR: case SQL_WVARCHAR: diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv.h b/source/pdo_sqlsrv/php_pdo_sqlsrv.h index c110a9c6..3b13953a 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv.h @@ -279,8 +279,6 @@ struct pdo_sqlsrv_stmt : public sqlsrv_stmt { size_t direct_query_subst_string_len; // length of query string used for direct queries HashTable* placeholders; // hashtable of named placeholders to keep track of params ordering in emulate prepare - // meta data for current result set - std::vector > current_meta_data; pdo_param_type* bound_column_param_types; bool fetch_numeric; bool fetch_datetime; diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index e6efb3a8..b623c1b4 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -1365,6 +1365,7 @@ struct sqlsrv_output_param { // forward decls struct sqlsrv_result_set; +struct field_meta_data; // *** parameter metadata struct *** struct param_meta_data @@ -1427,6 +1428,9 @@ struct sqlsrv_stmt : public sqlsrv_context { std::vector param_descriptions; + // meta data for current result set + std::vector> current_meta_data; + sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error_callback e, _In_opt_ void* drv TSRMLS_DC ); virtual ~sqlsrv_stmt( void ); diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index 46d60907..ddb1b981 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -124,7 +124,6 @@ void send_param_streams( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ); void sqlsrv_output_param_dtor( _Inout_ zval* data ); // called when a bound stream parameter is to be destroyed. void sqlsrv_stream_dtor( _Inout_ zval* data ); -bool is_streamable_type( _In_ SQLINTEGER sql_type ); } @@ -997,22 +996,24 @@ void core_sqlsrv_get_field( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i efree( field_value ); field_value = NULL; *field_len = 0; - } - } - } + } + } + } - // If the php type was not specified set the php type to be the default type. - if( sqlsrv_php_type.typeinfo.type == SQLSRV_PHPTYPE_INVALID ) { + // If the php type was not specified set the php type to be the default type. + if (sqlsrv_php_type.typeinfo.type == SQLSRV_PHPTYPE_INVALID) { + SQLSRV_ASSERT(stmt->current_meta_data.size() > field_index, "core_sqlsrv_get_field - meta data vector not in sync" ); + sql_field_type = stmt->current_meta_data[field_index]->field_type; + if (stmt->current_meta_data[field_index]->field_precision > 0) { + sql_field_len = stmt->current_meta_data[field_index]->field_precision; + } + else { + sql_field_len = stmt->current_meta_data[field_index]->field_size; + } - // Get the SQL type of the field. - core::SQLColAttributeW( stmt, field_index + 1, SQL_DESC_CONCISE_TYPE, NULL, 0, NULL, &sql_field_type TSRMLS_CC ); - - // Get the length of the field. - core::SQLColAttributeW( stmt, field_index + 1, SQL_DESC_LENGTH, NULL, 0, NULL, &sql_field_len TSRMLS_CC ); - - // Get the corresponding php type from the sql type. - sqlsrv_php_type = stmt->sql_type_to_php_type( static_cast( sql_field_type ), static_cast( sql_field_len ), prefer_string ); - } + // Get the corresponding php type from the sql type. + sqlsrv_php_type = stmt->sql_type_to_php_type(static_cast(sql_field_type), static_cast(sql_field_len), prefer_string); + } // Verify that we have an acceptable type to convert. CHECK_CUSTOM_ERROR( !is_valid_sqlsrv_phptype( sqlsrv_php_type ), stmt, SQLSRV_ERROR_INVALID_TYPE ) { @@ -1441,7 +1442,7 @@ void close_active_stream( _Inout_ sqlsrv_stmt* stmt TSRMLS_DC ) namespace { -bool is_streamable_type( _In_ SQLLEN sql_type ) +bool is_streamable_type( _In_ SQLSMALLINT sql_type ) { switch( sql_type ) { case SQL_CHAR: @@ -1460,6 +1461,25 @@ bool is_streamable_type( _In_ SQLLEN sql_type ) return false; } +bool is_a_numeric_type(_In_ SQLSMALLINT sql_type) +{ + switch (sql_type) { + case SQL_BIGINT: + case SQL_BIT: + case SQL_INTEGER: + case SQL_SMALLINT: + case SQL_TINYINT: + case SQL_FLOAT: + case SQL_DOUBLE: + case SQL_REAL: + case SQL_DECIMAL: + case SQL_NUMERIC: + return true; + } + + return false; +} + void calc_string_size( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_index, _In_ SQLLEN sql_type, _Inout_ SQLLEN& size TSRMLS_DC ) { try { @@ -1693,12 +1713,10 @@ void core_get_field_common( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_i { php_stream* stream = NULL; sqlsrv_stream* ss = NULL; - SQLLEN sql_type; + SQLSMALLINT sql_type; - SQLRETURN r = SQLColAttributeW( stmt->handle(), field_index + 1, SQL_DESC_TYPE, NULL, 0, NULL, &sql_type ); - CHECK_SQL_ERROR_OR_WARNING( r, stmt ) { - throw core::CoreException(); - } + SQLSRV_ASSERT(stmt->current_meta_data.size() > field_index, "core_get_field_common - meta data vector not in sync" ); + sql_type = stmt->current_meta_data[field_index]->field_type; CHECK_CUSTOM_ERROR( !is_streamable_type( sql_type ), stmt, SQLSRV_ERROR_STREAMABLE_TYPES_ONLY ) { throw core::CoreException(); @@ -2208,9 +2226,30 @@ void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_ind DEBUG_SQLSRV_ASSERT( sqlsrv_php_type.typeinfo.type == SQLSRV_PHPTYPE_STRING, "Type should be SQLSRV_PHPTYPE_STRING in get_field_as_string" ); + col_cache* cached = NULL; + if ( NULL != ( cached = static_cast< col_cache* >( zend_hash_index_find_ptr( Z_ARRVAL( stmt->col_cache ), static_cast< zend_ulong >( field_index ))))) { + sql_field_type = cached->sql_type; + sql_display_size = cached->display_size; + } + else { + SQLSRV_ASSERT(stmt->current_meta_data.size() > field_index, "get_field_as_string - meta data vector not in sync" ); + sql_field_type = stmt->current_meta_data[field_index]->field_type; + + // Calculate the field size. + calc_string_size( stmt, field_index, sql_field_type, sql_display_size TSRMLS_CC ); + + col_cache cache( sql_field_type, sql_display_size ); + core::sqlsrv_zend_hash_index_update_mem( *stmt, Z_ARRVAL( stmt->col_cache ), field_index, &cache, sizeof( col_cache ) TSRMLS_CC ); + } + + // Determine the correct encoding if( sqlsrv_php_type.typeinfo.encoding == SQLSRV_ENCODING_DEFAULT ) { sqlsrv_php_type.typeinfo.encoding = stmt->conn->encoding(); } + // For numbers, no need to convert + if (is_a_numeric_type(sql_field_type)) { + sqlsrv_php_type.typeinfo.encoding = SQLSRV_ENCODING_CHAR; + } // Set the C type and account for null characters at the end of the data. switch( sqlsrv_php_type.typeinfo.encoding ) { @@ -2228,22 +2267,6 @@ void get_field_as_string( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_ind break; } - col_cache* cached = NULL; - if ( NULL != ( cached = static_cast< col_cache* >( zend_hash_index_find_ptr( Z_ARRVAL( stmt->col_cache ), static_cast< zend_ulong >( field_index ))))) { - sql_field_type = cached->sql_type; - sql_display_size = cached->display_size; - } - else { - // Get the SQL type of the field. unixODBC 2.3.1 requires wide calls to support pooling - core::SQLColAttributeW( stmt, field_index + 1, SQL_DESC_CONCISE_TYPE, NULL, 0, NULL, &sql_field_type TSRMLS_CC ); - - // Calculate the field size. - calc_string_size( stmt, field_index, sql_field_type, sql_display_size TSRMLS_CC ); - - col_cache cache( sql_field_type, sql_display_size ); - core::sqlsrv_zend_hash_index_update_mem( *stmt, Z_ARRVAL( stmt->col_cache ), field_index, &cache, sizeof( col_cache ) TSRMLS_CC ); - } - // if this is a large type, then read the first few bytes to get the actual length from SQLGetData if( sql_display_size == 0 || sql_display_size == INT_MAX || sql_display_size == INT_MAX >> 1 || sql_display_size == UINT_MAX - 1 ) { diff --git a/source/sqlsrv/stmt.cpp b/source/sqlsrv/stmt.cpp index 3bbd4af6..3da4ebcc 100644 --- a/source/sqlsrv/stmt.cpp +++ b/source/sqlsrv/stmt.cpp @@ -91,7 +91,7 @@ const char SS_SQLSRV_WARNING_PARAM_VAR_NOT_REF[] = "Variable parameter %d not pa /* internal functions */ void convert_to_zval( _Inout_ sqlsrv_stmt* stmt, _In_ SQLSRV_PHPTYPE sqlsrv_php_type, _In_opt_ void* in_val, _In_ SQLLEN field_len, _Inout_ zval& out_zval ); - +SQLSMALLINT get_resultset_meta_data(_Inout_ sqlsrv_stmt* stmt); void fetch_fields_common( _Inout_ ss_sqlsrv_stmt* stmt, _In_ zend_long fetch_type, _Out_ zval& fields, _In_ bool allow_empty_field_names TSRMLS_DC ); bool determine_column_size_or_precision( sqlsrv_stmt const* stmt, _In_ sqlsrv_sqltype sqlsrv_type, _Inout_ SQLULEN* column_size, @@ -110,6 +110,15 @@ bool verify_and_set_encoding( _In_ const char* encoding_string, _Inout_ sqlsrv_p } +// internal helper function to free meta data structures allocated +void meta_data_free( _Inout_ field_meta_data* meta ) +{ + if( meta->field_name ) { + meta->field_name.reset(); + } + sqlsrv_free( meta ); +} + // query options for cursor types namespace SSCursorTypes { @@ -137,6 +146,9 @@ ss_sqlsrv_stmt::ss_sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ ss_sqlsrv_stmt::~ss_sqlsrv_stmt( void ) { + std::for_each(current_meta_data.begin(), current_meta_data.end(), meta_data_free); + current_meta_data.clear(); + if( fetch_field_names != NULL ) { for( int i=0; i < fetch_fields_count; ++i ) { @@ -459,27 +471,24 @@ PHP_FUNCTION( sqlsrv_field_metadata ) try { - // get the number of fields in the resultset - num_cols = core::SQLNumResultCols( stmt TSRMLS_CC ); + // get the number of fields in the resultset and its metadata if not exists + SQLSMALLINT num_cols = get_resultset_meta_data(stmt); zval result_meta_data; ZVAL_UNDEF( &result_meta_data ); core::sqlsrv_array_init( *stmt, &result_meta_data TSRMLS_CC ); for( SQLSMALLINT f = 0; f < num_cols; ++f ) { - - sqlsrv_malloc_auto_ptr core_meta_data; - core_meta_data = core_sqlsrv_field_metadata( stmt, f TSRMLS_CC ); - + field_meta_data* core_meta_data = stmt->current_meta_data[f]; + // initialize the array zval field_array; ZVAL_UNDEF( &field_array ); core::sqlsrv_array_init( *stmt, &field_array TSRMLS_CC ); - core::sqlsrv_add_assoc_string( *stmt, &field_array, FieldMetaData::NAME, - reinterpret_cast( core_meta_data->field_name.get() ), 0 TSRMLS_CC ); - - core_meta_data->field_name.transferred(); + // add the field name to the associative array but keep a copy + core::sqlsrv_add_assoc_string(*stmt, &field_array, FieldMetaData::NAME, + reinterpret_cast(core_meta_data->field_name.get()), 1 TSRMLS_CC); core::sqlsrv_add_assoc_long( *stmt, &field_array, FieldMetaData::TYPE, core_meta_data->field_type TSRMLS_CC ); @@ -519,9 +528,6 @@ PHP_FUNCTION( sqlsrv_field_metadata ) // add this field's meta data to the result set meta data core::sqlsrv_add_next_index_zval( *stmt, &result_meta_data, &field_array TSRMLS_CC ); - - // always good to call destructor for allocations done through placement new operator. - core_meta_data->~field_meta_data(); } // return our built collection and transfer ownership @@ -567,6 +573,10 @@ PHP_FUNCTION( sqlsrv_next_result ) core_sqlsrv_next_result( stmt TSRMLS_CC, true ); + // clear the current meta data since the new result will generate new meta data + std::for_each(stmt->current_meta_data.begin(), stmt->current_meta_data.end(), meta_data_free); + stmt->current_meta_data.clear(); + if( stmt->past_next_result_end ) { RETURN_NULL(); @@ -1084,7 +1094,7 @@ PHP_FUNCTION( sqlsrv_get_field ) try { // validate that the field index is within range - int num_cols = core::SQLNumResultCols( stmt TSRMLS_CC ); + SQLSMALLINT num_cols = get_resultset_meta_data(stmt); if( field_index < 0 || field_index >= num_cols ) { THROW_SS_ERROR( stmt, SS_SQLSRV_ERROR_INVALID_FUNCTION_PARAMETER, _FN_ ); @@ -1622,10 +1632,13 @@ sqlsrv_phptype determine_sqlsrv_php_type( _In_ ss_sqlsrv_stmt const* stmt, _In_ switch( sql_type ) { case SQL_BIGINT: - case SQL_CHAR: case SQL_DECIMAL: - case SQL_GUID: case SQL_NUMERIC: + sqlsrv_phptype.typeinfo.type = SQLSRV_PHPTYPE_STRING; + sqlsrv_phptype.typeinfo.encoding = SQLSRV_ENCODING_CHAR; + break; + case SQL_CHAR: + case SQL_GUID: case SQL_WCHAR: sqlsrv_phptype.typeinfo.type = SQLSRV_PHPTYPE_STRING; sqlsrv_phptype.typeinfo.encoding = stmt->encoding(); @@ -1647,6 +1660,7 @@ sqlsrv_phptype determine_sqlsrv_php_type( _In_ ss_sqlsrv_stmt const* stmt, _In_ case SQL_SMALLINT: case SQL_TINYINT: sqlsrv_phptype.typeinfo.type = SQLSRV_PHPTYPE_INT; + sqlsrv_phptype.typeinfo.encoding = SQLSRV_ENCODING_CHAR; break; case SQL_BINARY: case SQL_LONGVARBINARY: @@ -1676,6 +1690,7 @@ sqlsrv_phptype determine_sqlsrv_php_type( _In_ ss_sqlsrv_stmt const* stmt, _In_ case SQL_FLOAT: case SQL_REAL: sqlsrv_phptype.typeinfo.type = SQLSRV_PHPTYPE_FLOAT; + sqlsrv_phptype.typeinfo.encoding = SQLSRV_ENCODING_CHAR; break; case SQL_TYPE_DATE: case SQL_SS_TIMESTAMPOFFSET: @@ -1759,6 +1774,37 @@ void determine_stmt_has_rows( _Inout_ ss_sqlsrv_stmt* stmt TSRMLS_DC ) } } +SQLSMALLINT get_resultset_meta_data(_Inout_ sqlsrv_stmt * stmt) +{ + // get the numer of columns in the result set + SQLSMALLINT num_cols = -1; + + num_cols = stmt->current_meta_data.size(); + bool getMetaData = false; + + if (num_cols == 0) { + getMetaData = true; + num_cols = core::SQLNumResultCols(stmt TSRMLS_CC); + } + + try { + if (getMetaData) { + for (int i = 0; i < num_cols; i++) { + sqlsrv_malloc_auto_ptr core_meta_data; + core_meta_data = core_sqlsrv_field_metadata(stmt, i TSRMLS_CC); + stmt->current_meta_data.push_back(core_meta_data.get()); + core_meta_data.transferred(); + } + } + } catch( core::CoreException& ) { + throw; + } + + SQLSRV_ASSERT(num_cols > 0 && stmt->current_meta_data.size() == num_cols, "Meta data vector out of sync" ); + + return num_cols; +} + void fetch_fields_common( _Inout_ ss_sqlsrv_stmt* stmt, _In_ zend_long fetch_type, _Out_ zval& fields, _In_ bool allow_empty_field_names TSRMLS_DC ) { @@ -1772,40 +1818,25 @@ void fetch_fields_common( _Inout_ ss_sqlsrv_stmt* stmt, _In_ zend_long fetch_typ throw ss::SSException(); } - // get the numer of columns in the result set - SQLSMALLINT num_cols = core::SQLNumResultCols(stmt TSRMLS_CC); + // get the numer of columns in the result set and its metadata if not exists + SQLSMALLINT num_cols = get_resultset_meta_data(stmt); // if this is the first fetch in a new result set, then get the field names and // store them off for successive fetches. - if(( fetch_type & SQLSRV_FETCH_ASSOC ) && stmt->fetch_field_names == NULL ) { + if ((fetch_type & SQLSRV_FETCH_ASSOC) && stmt->fetch_field_names == NULL) { SQLLEN field_name_len = 0; - SQLSMALLINT field_name_len_w = 0; - SQLWCHAR field_name_w[( SS_MAXCOLNAMELEN + 1 ) * 2] = {L'\0'}; - sqlsrv_malloc_auto_ptr field_name; sqlsrv_malloc_auto_ptr field_names; - field_names = static_cast( sqlsrv_malloc( num_cols * sizeof( sqlsrv_fetch_field_name ))); - SQLSRV_ENCODING encoding = (( stmt->encoding() == SQLSRV_ENCODING_DEFAULT ) ? stmt->conn->encoding() : stmt->encoding()); - for( int i = 0; i < num_cols; ++i ) { - - core::SQLColAttributeW ( stmt, i + 1, SQL_DESC_NAME, field_name_w, ( SS_MAXCOLNAMELEN + 1 ) * 2, &field_name_len_w, NULL TSRMLS_CC ); - - //Conversion function expects size in characters - field_name_len_w = field_name_len_w / sizeof ( SQLWCHAR ); - bool converted = convert_string_from_utf16( encoding, field_name_w, - field_name_len_w, ( char** ) &field_name, field_name_len ); - - CHECK_CUSTOM_ERROR( !converted, stmt, SQLSRV_ERROR_FIELD_ENCODING_TRANSLATE, get_last_error_message() ) { - throw core::CoreException(); - } - - field_names[i].name = static_cast( sqlsrv_malloc( field_name_len, sizeof( char ), 1 )); - memcpy_s(( void* )field_names[i].name, ( field_name_len * sizeof( char )) , ( void* ) field_name, field_name_len ); - field_names[i].name[field_name_len] = '\0'; // null terminate the field name since SQLColAttribute doesn't. - field_names[i].len = field_name_len + 1; - field_name.reset(); + field_names = static_cast(sqlsrv_malloc(num_cols * sizeof(sqlsrv_fetch_field_name))); + for (int i = 0; i < num_cols; ++i) { + // The meta data field name is already null-terminated, and the field name len is correct. + field_name_len = stmt->current_meta_data[i]->field_name_len; + field_names[i].name = static_cast(sqlsrv_malloc(field_name_len, sizeof(char), 1)); + memcpy_s((void*)field_names[i].name, (field_name_len * sizeof(char)), (void*)stmt->current_meta_data[i]->field_name, field_name_len); + field_names[i].name[field_name_len] = '\0'; // null terminate the field name after the memcpy + field_names[i].len = field_name_len; // field_name_len should not need to include the null char } - + stmt->fetch_field_names = field_names; stmt->fetch_fields_count = num_cols; field_names.transferred(); @@ -1840,12 +1871,12 @@ void fetch_fields_common( _Inout_ ss_sqlsrv_stmt* stmt, _In_ zend_long fetch_typ if( fetch_type & SQLSRV_FETCH_ASSOC ) { - CHECK_CUSTOM_WARNING_AS_ERROR(( stmt->fetch_field_names[i].len == 1 && !allow_empty_field_names ), stmt, + CHECK_CUSTOM_WARNING_AS_ERROR(( stmt->fetch_field_names[i].len == 0 && !allow_empty_field_names ), stmt, SS_SQLSRV_WARNING_FIELD_NAME_EMPTY) { throw ss::SSException(); } - if( stmt->fetch_field_names[i].len > 1 || allow_empty_field_names ) { + if( stmt->fetch_field_names[i].len > 0 || allow_empty_field_names ) { zr = add_assoc_zval( &fields, stmt->fetch_field_names[i].name, &field ); CHECK_ZEND_ERROR( zr, stmt, SQLSRV_ERROR_ZEND_HASH ) {