diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index 36d9856d..3f1703e7 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -1325,8 +1325,19 @@ int pdo_sqlsrv_stmt_param_hook( _Inout_ pdo_stmt_t *stmt, switch( php_out_type ) { case SQLSRV_PHPTYPE_NULL: case SQLSRV_PHPTYPE_STREAM: - THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_OUTPUT_PARAM_TYPE ); + { + zval *zv = ¶m->parameter; + if (Z_ISREF_P(zv)) { + ZVAL_DEREF(zv); + } + // Table-valued parameters are input-only + CHECK_CUSTOM_ERROR(Z_TYPE_P(zv) == IS_ARRAY, driver_stmt, SQLSRV_ERROR_TVP_INPUT_PARAM_ONLY) { + throw pdo::PDOException(); + } + // For other types, simply throw the following error + THROW_PDO_ERROR(driver_stmt, PDO_SQLSRV_ERROR_INVALID_OUTPUT_PARAM_TYPE); break; + } case SQLSRV_PHPTYPE_INT: column_size = SQLSRV_UNKNOWN_SIZE; break; @@ -1391,7 +1402,7 @@ int pdo_sqlsrv_stmt_param_hook( _Inout_ pdo_stmt_t *stmt, // and bind the parameter core_sqlsrv_bind_param( driver_stmt, static_cast( param->paramno ), direction, &(param->parameter) , php_out_type, encoding, - sql_type, column_size, decimal_digits ); + sql_type, column_size, decimal_digits); } break; // undo any work done by the core layer after the statement is executed diff --git a/source/pdo_sqlsrv/pdo_util.cpp b/source/pdo_sqlsrv/pdo_util.cpp index bab79c99..95147ea8 100644 --- a/source/pdo_sqlsrv/pdo_util.cpp +++ b/source/pdo_sqlsrv/pdo_util.cpp @@ -461,6 +461,42 @@ pdo_error PDO_ERRORS[] = { PDO_SQLSRV_ERROR_EXTENDED_STRING_TYPE_INVALID, { IMSSP, (SQLCHAR*) "Invalid extended string type specified. PDO_ATTR_DEFAULT_STR_PARAM can be either PDO_PARAM_STR_CHAR or PDO_PARAM_STR_NATL.", -97, false} }, + { + SQLSRV_ERROR_TVP_STRING_ENCODING_TRANSLATE, + { IMSSP, (SQLCHAR*) "An error occurred translating a string for Table-Valued Param %1!d! Column %2!d! to UTF-16: %3!s!", -98, true } + }, + { + SQLSRV_ERROR_TVP_INVALID_COLUMN_PHPTYPE, + { IMSSP, (SQLCHAR*) "An invalid type for Table-Valued Param %1!d! Column %2!d! was specified", -99, true } + }, + { + SQLSRV_ERROR_TVP_FETCH_METADATA, + { IMSSP, (SQLCHAR*) "Failed to get metadata for Table-Valued Param %1!d!", -100, true } + }, + { + SQLSRV_ERROR_TVP_INVALID_INPUTS, + { IMSSP, (SQLCHAR*) "Invalid inputs for Table-Valued Param %1!d!", -101, true } + }, + { + SQLSRV_ERROR_TVP_INVALID_TABLE_TYPE_NAME, + { IMSSP, (SQLCHAR*) "Expect a non-empty string for a Type Name for Table-Valued Param %1!d!", -102, true } + }, + { + SQLSRV_ERROR_TVP_ROWS_UNEXPECTED_SIZE, + { IMSSP, (SQLCHAR*) "For Table-Valued Param %1!d! the number of values in a row is expected to be %2!d!", -103, true } + }, + { + SQLSRV_ERROR_TVP_STRING_KEYS, + { IMSSP, (SQLCHAR*) "Associative arrays not allowed for Table-Valued Param %1!d!", -104, true } + }, + { + SQLSRV_ERROR_TVP_ROW_NOT_ARRAY, + { IMSSP, (SQLCHAR*) "Expect an array for each row for Table-Valued Param %1!d!", -105, true } + }, + { + SQLSRV_ERROR_TVP_INPUT_PARAM_ONLY, + { IMSSP, (SQLCHAR*) "You cannot return data in a table-valued parameter. Table-valued parameters are input-only.", -106, false } + }, { UINT_MAX, {} } }; diff --git a/source/shared/core_conn.cpp b/source/shared/core_conn.cpp index 94f8bba1..cc5d9f2f 100644 --- a/source/shared/core_conn.cpp +++ b/source/shared/core_conn.cpp @@ -955,7 +955,6 @@ const char* get_processor_arch( void ) #endif // !_WIN32 } - // some features require a server of a certain version or later // this function determines the version of the server we're connected to // and stores it in the connection. Any errors are logged before return. @@ -1008,8 +1007,8 @@ void load_azure_key_vault(_Inout_ sqlsrv_conn* conn) char *akv_id = conn->ce_option.akv_id.get(); char *akv_secret = conn->ce_option.akv_secret.get(); - unsigned int id_len = strnlen_s(akv_id); - unsigned int key_size = strnlen_s(akv_secret); + size_t id_len = strnlen_s(akv_id); + size_t key_size = strnlen_s(akv_secret); configure_azure_key_vault(conn, AKV_CONFIG_FLAGS, conn->ce_option.akv_mode, 0); configure_azure_key_vault(conn, AKV_CONFIG_PRINCIPALID, akv_id, id_len); diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index ce697654..eb6dff30 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -207,6 +207,7 @@ enum SQLSRV_PHPTYPE { SQLSRV_PHPTYPE_STRING, SQLSRV_PHPTYPE_DATETIME, SQLSRV_PHPTYPE_STREAM, + SQLSRV_PHPTYPE_TABLE, MAX_SQLSRV_PHPTYPE, // highest value for a php type SQLSRV_PHPTYPE_INVALID = MAX_SQLSRV_PHPTYPE // used to see if a type is invalid }; @@ -1427,11 +1428,11 @@ struct sqlsrv_param virtual void release_data(); bool derive_string_types_sizes(_In_ zval* param_z); - void preprocess_datetime_object(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z); + bool preprocess_datetime_object(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z); - // The following methods change the member placeholder_z - void convert_input_str_to_utf16(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z); - void convert_datetime_to_string(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z); + // The following methods change the member placeholder_z, and both will return false if conversions fail + bool convert_input_str_to_utf16(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z); + bool convert_datetime_to_string(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z); virtual bool prepare_param(_In_ zval* param_ref, _Inout_ zval* param_z); virtual void process_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z); @@ -1481,6 +1482,45 @@ struct sqlsrv_param_inout : public sqlsrv_param void finalize_output_string(); }; +// *** Table-valued parameter struct used for SQLBindParameter, inheriting sqlsrv_param +// *** A sqlsrv_param_tvp can be representing a table-valued parameter itself or one of +// *** its constituent columns. When it is a table-valued parameter, tvp_columns cannot +// *** be empty. When it is a TVP column, parent_tvp points to its table-valued parameter +// *** and tvp_columns must be empty. The member param_pos refers to the ordinal position +// *** of this column in the corresponding table type. +struct sqlsrv_param_tvp : public sqlsrv_param +{ + sqlsrv_param_tvp* parent_tvp; // For a TVP column to reference to the table-valued parameter. NULL if this is the TVP itself. + std::vector tvp_columns; // The constituent columns of the table-valued parameter + int num_rows; // The total number of rows + int current_row; // A counter to keep track of which row is to be processed + + sqlsrv_param_tvp(_In_ SQLUSMALLINT param_num, _In_ SQLSRV_ENCODING enc, _In_ SQLSMALLINT sql_type, _In_ SQLULEN col_size, _In_ SQLSMALLINT dec_digits, _In_ sqlsrv_param_tvp* tvp) : + sqlsrv_param(param_num, SQL_PARAM_INPUT, enc, sql_type, col_size, dec_digits), num_rows(0), current_row(0), parent_tvp(tvp) + { + ZVAL_UNDEF(&placeholder_z); + } + virtual ~sqlsrv_param_tvp() { release_data(); } + virtual void release_data(); + virtual void bind_param(_Inout_ sqlsrv_stmt* stmt); + virtual void process_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z); + + // The following methods are used to supply data to the server post execution + virtual void init_data_from_zval(_Inout_ sqlsrv_stmt* stmt) {} + virtual bool send_data_packet(_Inout_ sqlsrv_stmt* stmt); + + // Change the column encoding based on the sql data type + static void sql_type_to_encoding(_In_ SQLSMALLINT sql_type, _Inout_ SQLSRV_ENCODING* encoding); + + // The following methods are only applicable to a table-valued parameter or its individual columns + int parse_tv_param_arrays(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z); + void get_tvp_metadata(_In_ sqlsrv_stmt* stmt, _In_ SQLCHAR* table_type_name); + void process_param_column_value(_Inout_ sqlsrv_stmt* stmt); + void process_null_param_value(_Inout_ sqlsrv_stmt* stmt); + void populate_cell_placeholder(_Inout_ sqlsrv_stmt* stmt, _In_ int ordinal); + void send_string_data_in_batches(_Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z); +}; + // *** a container of all parameters used for SQLBindParameter *** struct sqlsrv_params_container { @@ -1717,7 +1757,7 @@ sqlsrv_stmt* core_sqlsrv_create_stmt( _Inout_ sqlsrv_conn* conn, _In_ driver_stm _In_opt_ const stmt_option valid_stmt_opts[], _In_ error_callback const err, _In_opt_ void* driver ); void core_sqlsrv_bind_param( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT param_num, _In_ SQLSMALLINT direction, _Inout_ zval* param_z, _In_ SQLSRV_PHPTYPE php_out_type, _Inout_ SQLSRV_ENCODING encoding, _Inout_ SQLSMALLINT sql_type, _Inout_ SQLULEN column_size, - _Inout_ SQLSMALLINT decimal_digits ); + _Inout_ SQLSMALLINT decimal_digits); SQLRETURN core_sqlsrv_execute( _Inout_ sqlsrv_stmt* stmt, _In_reads_bytes_(sql_len) const char* sql = NULL, _In_ int sql_len = 0 ); field_meta_data* core_sqlsrv_field_metadata( _Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT colno ); bool core_sqlsrv_fetch( _Inout_ sqlsrv_stmt* stmt, _In_ SQLSMALLINT fetch_orientation, _In_ SQLULEN fetch_offset ); @@ -1974,6 +2014,15 @@ enum SQLSRV_ERROR_CODES { SQLSRV_ERROR_DATA_CLASSIFICATION_PRE_EXECUTION, SQLSRV_ERROR_DATA_CLASSIFICATION_NOT_AVAILABLE, SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, + SQLSRV_ERROR_TVP_STRING_ENCODING_TRANSLATE, + SQLSRV_ERROR_TVP_INVALID_COLUMN_PHPTYPE, + SQLSRV_ERROR_TVP_FETCH_METADATA, + SQLSRV_ERROR_TVP_INVALID_INPUTS, + SQLSRV_ERROR_TVP_INVALID_TABLE_TYPE_NAME, + SQLSRV_ERROR_TVP_STRING_KEYS, + SQLSRV_ERROR_TVP_ROW_NOT_ARRAY, + SQLSRV_ERROR_TVP_ROWS_UNEXPECTED_SIZE, + SQLSRV_ERROR_TVP_INPUT_PARAM_ONLY, // Driver specific error codes starts from here. SQLSRV_ERROR_DRIVER_SPECIFIC = 1000, diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index 723868b9..fdaaec08 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -109,6 +109,7 @@ stmt_option const* get_stmt_option( sqlsrv_conn const* conn, _In_ zend_ulong key bool is_valid_sqlsrv_phptype( _In_ sqlsrv_phptype type ); void adjustDecimalPrecision(_Inout_ zval* param_z, _In_ SQLSMALLINT decimal_digits); bool is_a_numeric_type(_In_ SQLSMALLINT sql_type); +bool is_a_string_type(_In_ SQLSMALLINT sql_type); } // constructor for sqlsrv_stmt. Here so that we can use functions declared earlier. @@ -362,7 +363,7 @@ sqlsrv_stmt* core_sqlsrv_create_stmt( _Inout_ sqlsrv_conn* conn, _In_ driver_stm void core_sqlsrv_bind_param( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT param_num, _In_ SQLSMALLINT direction, _Inout_ zval* param_z, _In_ SQLSRV_PHPTYPE php_out_type, _Inout_ SQLSRV_ENCODING encoding, _Inout_ SQLSMALLINT sql_type, _Inout_ SQLULEN column_size, - _Inout_ SQLSMALLINT decimal_digits ) + _Inout_ SQLSMALLINT decimal_digits) { // check is only < because params are 0 based CHECK_CUSTOM_ERROR(param_num >= SQL_SERVER_MAX_PARAMS, stmt, SQLSRV_ERROR_MAX_PARAMS_EXCEEDED, param_num + 1) { @@ -380,7 +381,12 @@ void core_sqlsrv_bind_param( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT param_ if (param_ptr == NULL) { sqlsrv_malloc_auto_ptr new_param; if (direction == SQL_PARAM_INPUT) { - new_param = new (sqlsrv_malloc(sizeof(sqlsrv_param))) sqlsrv_param(param_num, direction, encoding, sql_type, column_size, decimal_digits); + // Check if it's a Table-Valued Parameter first + if (Z_TYPE_P(param_z) == IS_ARRAY) { + new_param = new (sqlsrv_malloc(sizeof(sqlsrv_param_tvp))) sqlsrv_param_tvp(param_num, encoding, SQL_SS_TABLE, 0, 0, NULL); + } else { + new_param = new (sqlsrv_malloc(sizeof(sqlsrv_param))) sqlsrv_param(param_num, direction, encoding, sql_type, column_size, decimal_digits); + } } else if (direction == SQL_PARAM_OUTPUT || direction == SQL_PARAM_INPUT_OUTPUT) { new_param = new (sqlsrv_malloc(sizeof(sqlsrv_param_inout))) sqlsrv_param_inout(param_num, direction, encoding, sql_type, column_size, decimal_digits, php_out_type); } else { @@ -1234,6 +1240,28 @@ bool is_a_numeric_type(_In_ SQLSMALLINT sql_type) return false; } +bool is_a_string_type(_In_ SQLSMALLINT sql_type) +{ + switch (sql_type) { + case SQL_BIGINT: + case SQL_DECIMAL: + case SQL_NUMERIC: + case SQL_SS_VARIANT: + case SQL_SS_UDT: + case SQL_GUID: + case SQL_SS_XML: + case SQL_CHAR: + case SQL_WCHAR: + case SQL_VARCHAR: + case SQL_WVARCHAR: + case SQL_LONGVARCHAR: + case SQL_WLONGVARCHAR: + return true; + } + + return false; +} + void calc_string_size( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT field_index, _In_ SQLLEN sql_type, _Inout_ SQLLEN& size ) { try { @@ -1906,6 +1934,7 @@ bool is_valid_sqlsrv_phptype( _In_ sqlsrv_phptype type ) case SQLSRV_PHPTYPE_INT: case SQLSRV_PHPTYPE_FLOAT: case SQLSRV_PHPTYPE_DATETIME: + case SQLSRV_PHPTYPE_TABLE: return true; case SQLSRV_PHPTYPE_STRING: case SQLSRV_PHPTYPE_STREAM: @@ -2326,7 +2355,7 @@ bool sqlsrv_param::derive_string_types_sizes(_In_ zval* param_z) return is_numeric; } -void sqlsrv_param::convert_input_str_to_utf16(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z) +bool sqlsrv_param::convert_input_str_to_utf16(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z) { // This converts the string in param_z and stores the wide string in the member placeholder_z char* str = Z_STRVAL_P(param_z); @@ -2337,8 +2366,8 @@ void sqlsrv_param::convert_input_str_to_utf16(_Inout_ sqlsrv_stmt* stmt, _In_ zv unsigned int wchar_size = 0; wide_buffer = utf16_string_from_mbcs_string(encoding, reinterpret_cast(str), static_cast(str_length), &wchar_size, true); - CHECK_CUSTOM_ERROR(wide_buffer == 0, stmt, SQLSRV_ERROR_INPUT_PARAM_ENCODING_TRANSLATE, param_pos + 1, get_last_error_message()) { - throw core::CoreException(); + if (wide_buffer == 0) { + return false; } wide_buffer[wchar_size] = L'\0'; core::sqlsrv_zval_stringl(&placeholder_z, reinterpret_cast(wide_buffer.get()), wchar_size * sizeof(SQLWCHAR)); @@ -2346,6 +2375,8 @@ void sqlsrv_param::convert_input_str_to_utf16(_Inout_ sqlsrv_stmt* stmt, _In_ zv // If the string is empty, then nothing needs to be done core::sqlsrv_zval_stringl(&placeholder_z, "", 0); } + + return true; } void sqlsrv_param::process_string_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z) @@ -2367,7 +2398,10 @@ void sqlsrv_param::process_string_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* throw core::CoreException(); } // This changes the member placeholder_z to hold the wide string - convert_input_str_to_utf16(stmt, param_z); + bool converted = convert_input_str_to_utf16(stmt, param_z); + CHECK_CUSTOM_ERROR(!converted, stmt, SQLSRV_ERROR_INPUT_PARAM_ENCODING_TRANSLATE, param_pos + 1, get_last_error_message()) { + throw core::CoreException(); + } // Bind the wide string in placeholder_z buffer = Z_STRVAL(placeholder_z); @@ -2426,7 +2460,7 @@ void sqlsrv_param::process_resource_param(_Inout_ zval* param_z) strlen_or_indptr = SQL_DATA_AT_EXEC; } -void sqlsrv_param::convert_datetime_to_string(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z) +bool sqlsrv_param::convert_datetime_to_string(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z) { // This changes the member placeholder_z to hold the converted string of the datetime object zval function_z; @@ -2440,23 +2474,23 @@ void sqlsrv_param::convert_datetime_to_string(_Inout_ sqlsrv_stmt* stmt, _In_ zv // meaning there is too much information in the character string. If the user specifies the 'datetimeoffset' // sql type, it lacks the timezone. if (sql_data_type == SQL_SS_TIMESTAMPOFFSET) { - core::sqlsrv_zval_stringl(&format_z, const_cast(DateTime::DATETIMEOFFSET_FORMAT), - DateTime::DATETIMEOFFSET_FORMAT_LEN); + ZVAL_STRINGL(&format_z, DateTime::DATETIMEOFFSET_FORMAT, DateTime::DATETIMEOFFSET_FORMAT_LEN); } else if (sql_data_type == SQL_TYPE_DATE) { - core::sqlsrv_zval_stringl(&format_z, const_cast(DateTime::DATE_FORMAT), DateTime::DATE_FORMAT_LEN); + ZVAL_STRINGL(&format_z, DateTime::DATE_FORMAT, DateTime::DATE_FORMAT_LEN); } else { - core::sqlsrv_zval_stringl(&format_z, const_cast(DateTime::DATETIME_FORMAT), DateTime::DATETIME_FORMAT_LEN); + ZVAL_STRINGL(&format_z, DateTime::DATETIME_FORMAT, DateTime::DATETIME_FORMAT_LEN); } // call the DateTime::format member function to convert the object to a string that SQL Server understands - core::sqlsrv_zval_stringl(&function_z, "format", sizeof("format") - 1); + ZVAL_STRINGL(&function_z, "format", sizeof("format") - 1); + //core::sqlsrv_zval_stringl(&function_z, "format", sizeof("format") - 1); params[0] = format_z; // If placeholder_z is a string, release it first before assigning a new string value if (Z_TYPE(placeholder_z) == IS_STRING && Z_STR(placeholder_z) != NULL) { zend_string_release(Z_STR(placeholder_z)); } - + // This is equivalent to the PHP code: $param_z->format($format_z); where param_z is the // DateTime object and $format_z is the format string. int zr = call_user_function(EG(function_table), param_z, &function_z, &placeholder_z, 1, params); @@ -2464,12 +2498,10 @@ void sqlsrv_param::convert_datetime_to_string(_Inout_ sqlsrv_stmt* stmt, _In_ zv zend_string_release(Z_STR(format_z)); zend_string_release(Z_STR(function_z)); - CHECK_CUSTOM_ERROR(zr == FAILURE, stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, param_pos + 1) { - throw core::CoreException(); - } + return (zr != FAILURE); } -void sqlsrv_param::preprocess_datetime_object(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z) +bool sqlsrv_param::preprocess_datetime_object(_Inout_ sqlsrv_stmt* stmt, _In_ zval* param_z) { bool valid_class_name_found = false; zend_class_entry *class_entry = Z_OBJCE_P(param_z); @@ -2486,8 +2518,8 @@ void sqlsrv_param::preprocess_datetime_object(_Inout_ sqlsrv_stmt* stmt, _In_ zv } } - CHECK_CUSTOM_ERROR(!valid_class_name_found, stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, param_pos + 1) { - throw core::CoreException(); + if (!valid_class_name_found) { + return false; } // Derive the param SQL type only if it is unknown @@ -2514,6 +2546,8 @@ void sqlsrv_param::preprocess_datetime_object(_Inout_ sqlsrv_stmt* stmt, _In_ zv decimal_digits = SQL_SERVER_2008_DEFAULT_DATETIME_SCALE; } } + + return true; } void sqlsrv_param::process_object_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z) @@ -2521,8 +2555,13 @@ void sqlsrv_param::process_object_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* // Assume the param refers to a DateTime object since it's the only type the drivers support. // Verification occurs in the calling function as the drivers convert the DateTime object // to a string before sending it to the server. - preprocess_datetime_object(stmt, param_z); - convert_datetime_to_string(stmt, param_z); + bool succeeded = preprocess_datetime_object(stmt, param_z); + if (succeeded) { + succeeded = convert_datetime_to_string(stmt, param_z); + } + CHECK_CUSTOM_ERROR(!succeeded, stmt, SQLSRV_ERROR_INVALID_PARAMETER_PHPTYPE, param_pos + 1) { + throw core::CoreException(); + } buffer = Z_STRVAL(placeholder_z); buffer_length = Z_STRLEN(placeholder_z) - 1; @@ -2745,7 +2784,7 @@ void sqlsrv_param_inout::process_string_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ } // If it's a UTF-8 input output parameter (signified by the C type being SQL_C_WCHAR) - // or if the PHP type is a binary encoded string with a N(VAR)CHAR/NTEXTSQL type, + // or if the PHP type is a binary encoded string with a N(VAR)CHAR/NTEXT SQL type, // convert it to wchar first if (direction == SQL_PARAM_INPUT_OUTPUT && (c_data_type == SQL_C_WCHAR || @@ -2890,7 +2929,7 @@ void sqlsrv_param_inout::finalize_output_string() throw core::CoreException(); } - // For ODBC 11+ see https://docs.microsoft.com/sql/relational-databases/native-client/features/odbc-driver-behavior-change-when-handling-character-conversions + // For ODBC 11+ see https://docs.microsoft.com/sql/relational-databases/native-client/features/odbc-driver-behavior-change-when-handling-character-conversions // A length value of SQL_NO_TOTAL for SQLBindParameter indicates that the buffer contains data up to the // original buffer_length and is NULL terminated. // The IF statement can be true when using connection pooling with unixODBC 2.3.4. @@ -2959,8 +2998,8 @@ void sqlsrv_param_inout::resize_output_string_buffer(_Inout_ zval* param_z, _In_ // With AE enabled, column_size is already retrieved from SQLDescribeParam, but column_size // does not include the negative sign or decimal place for numeric values - // Without AE, the same can happen as well, in particular with decimals and numerics - // with precision/scale specified (See VSO Bug 2913 for details) + // VSO Bug 2913: without AE, the same can happen as well, in particular to decimals + // and numerics with precision/scale specified if (is_numeric_type) { // Include the possible negative sign field_size += elem_size; @@ -3018,6 +3057,577 @@ void sqlsrv_param_inout::resize_output_string_buffer(_Inout_ zval* param_z, _In_ } } +// Change the column encoding based on the sql data type +/*static*/ void sqlsrv_param_tvp::sql_type_to_encoding(_In_ SQLSMALLINT sql_type, _Inout_ SQLSRV_ENCODING* encoding) +{ + switch (sql_type) { + case SQL_BIGINT: + case SQL_DECIMAL: + case SQL_NUMERIC: + case SQL_BIT: + case SQL_INTEGER: + case SQL_SMALLINT: + case SQL_TINYINT: + case SQL_FLOAT: + case SQL_REAL: + *encoding = SQLSRV_ENCODING_CHAR; + break; + case SQL_BINARY: + case SQL_LONGVARBINARY: + case SQL_VARBINARY: + case SQL_SS_UDT: + *encoding = SQLSRV_ENCODING_BINARY; + break; + default: + // Do nothing + break; + } +} + +void sqlsrv_param_tvp::get_tvp_metadata(_In_ sqlsrv_stmt* stmt, _In_ SQLCHAR* table_type_name) +{ + SQLHANDLE chstmt = SQL_NULL_HANDLE; + SQLRETURN rc; + SQLSMALLINT data_type, dec_digits; + SQLINTEGER col_size; + SQLLEN cb_data_type, cb_col_size, cb_dec_digits; + + core::SQLAllocHandle(SQL_HANDLE_STMT, *(stmt->conn), &chstmt); + + rc = SQLSetStmtAttr(chstmt, SQL_SOPT_SS_NAME_SCOPE, (SQLPOINTER)SQL_SS_NAME_SCOPE_TABLE_TYPE, SQL_IS_UINTEGER); + CHECK_CUSTOM_ERROR(!SQL_SUCCEEDED(rc), stmt, SQLSRV_ERROR_TVP_FETCH_METADATA, param_pos + 1) { + throw core::CoreException(); + } + + // Check table type name and see if the schema is specified. Otherwise, assume DBO + std::string type_name(reinterpret_cast(table_type_name)); + std::size_t pos = type_name.find_first_of("."); + if (pos != std::string::npos) { + std::string str1 = type_name.substr(0, pos); + std::string str2 = type_name.substr(pos + 1); + + char schema[SS_MAXCOLNAMELEN] = { '\0' }; + char type[SS_MAXCOLNAMELEN] = { '\0' }; + + strcpy_s(schema, SS_MAXCOLNAMELEN, str1.c_str()); + strcpy_s(type, SS_MAXCOLNAMELEN, str2.c_str()); + + rc = SQLColumns(chstmt, NULL, 0, reinterpret_cast(schema), SQL_NTS, reinterpret_cast(type), SQL_NTS, NULL, 0); + } else { + rc = SQLColumns(chstmt, NULL, 0, NULL, 0, table_type_name, SQL_NTS, NULL, 0); + } + + CHECK_CUSTOM_ERROR(!SQL_SUCCEEDED(rc), stmt, SQLSRV_ERROR_TVP_FETCH_METADATA, param_pos + 1) { + throw core::CoreException(); + } + + SQLSRV_ENCODING stmt_encoding = (stmt->encoding() == SQLSRV_ENCODING_DEFAULT) ? stmt->conn->encoding() : stmt->encoding(); + + if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) { + SQLBindCol(chstmt, 5, SQL_C_SSHORT, &data_type, 0, &cb_data_type); + SQLBindCol(chstmt, 7, SQL_C_SLONG, &col_size, 0, &cb_col_size); + SQLBindCol(chstmt, 9, SQL_C_SSHORT, &dec_digits, 0, &cb_dec_digits); + + SQLUSMALLINT pos = 0; + while (SQL_SUCCESS == rc) { + rc = SQLFetch(chstmt); + if (rc == SQL_NO_DATA) { + CHECK_CUSTOM_ERROR(tvp_columns.size() == 0, stmt, SQLSRV_ERROR_TVP_FETCH_METADATA, param_pos + 1) { + throw core::CoreException(); + } + break; + } + + sqlsrv_malloc_auto_ptr param_ptr; + + // The SQL data type is used to derive the column encoding + SQLSRV_ENCODING column_encoding = stmt_encoding; + sql_type_to_encoding(data_type, &column_encoding); + + param_ptr = new (sqlsrv_malloc(sizeof(sqlsrv_param_tvp))) sqlsrv_param_tvp(pos, column_encoding, data_type, col_size, dec_digits, this); + param_ptr->num_rows = this->num_rows; // Each column inherits the number of rows from the TVP + + tvp_columns.push_back(param_ptr); + param_ptr.transferred(); + + pos++; + } + } else { + THROW_CORE_ERROR(stmt, SQLSRV_ERROR_TVP_FETCH_METADATA, param_pos + 1); + } + + SQLCloseCursor(chstmt); + SQLFreeHandle(SQL_HANDLE_STMT, chstmt); +} + +void sqlsrv_param_tvp::release_data() +{ + // Clean up tvp_columns as well + for (int i = 0; i < tvp_columns.size(); i++) { + tvp_columns[i]->release_data(); + sqlsrv_free(tvp_columns[i]); + } + + sqlsrv_param::release_data(); +} + +void sqlsrv_param_tvp::process_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z) +{ + if (sql_data_type == SQL_SS_TABLE) { + // This is a table-valued parameter + param_php_type = IS_ARRAY; + c_data_type = SQL_C_DEFAULT; + + // The decimal_digits must be 0 for TVP + decimal_digits = 0; + + // The column_size for a TVP is the row array size + // The following method will verify the input array and also derive num_rows + this->num_rows = 0; + int num_columns = parse_tv_param_arrays(stmt, param_z); + column_size = num_rows; + + buffer = NULL; + buffer_length = NULL; + strlen_or_indptr = (num_columns == 0)? SQL_DEFAULT_PARAM : SQL_DATA_AT_EXEC; + } else { + // This is one of the constituent columns of the table-valued parameter + // The column value of the first row is already saved in member variable param_ptr_z + process_param_column_value(stmt); + } +} + +int sqlsrv_param_tvp::parse_tv_param_arrays(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* param_z) +{ + // If this is not a table-valued parameter, simply return + if (sql_data_type != SQL_SS_TABLE) { + return 0; + } + + // This method verifies if the table-valued parameter (i.e. param_z) provided by the user is valid. + // The number of columns in the given table-valued parameter is returned, which may be zero. + HashTable* inputs_ht = Z_ARRVAL_P(param_z); + zend_string *tvp_name = NULL; + zval *tvp_data_z = NULL; + HashPosition pos; + + zend_hash_internal_pointer_reset_ex(inputs_ht, &pos); + if (zend_hash_has_more_elements_ex(inputs_ht, &pos) == SUCCESS) { + + zend_ulong num_index = -1; + size_t key_len = 0; + + int key_type = zend_hash_get_current_key(inputs_ht, &tvp_name, &num_index); + if (key_type == HASH_KEY_IS_STRING) { + key_len = ZSTR_LEN(tvp_name); + tvp_data_z = zend_hash_get_current_data_ex(inputs_ht, &pos); + } + + CHECK_CUSTOM_ERROR((key_type == HASH_KEY_IS_LONG || key_len == 0), stmt, SQLSRV_ERROR_TVP_INVALID_TABLE_TYPE_NAME, param_pos + 1) { + throw core::CoreException(); + } + } + + // TODO: Find the docs page somewhere that says a TVP can not be null but it may have null columns?? + CHECK_CUSTOM_ERROR(tvp_data_z == NULL || Z_TYPE_P(tvp_data_z) == IS_NULL || Z_TYPE_P(tvp_data_z) != IS_ARRAY, stmt, SQLSRV_ERROR_TVP_INVALID_INPUTS, param_pos + 1) { + throw core::CoreException(); + } + + // Save the TVP multi-dim array data, which should be something like this + // [ + // [r1c1, r1c2, r1c3], + // [r2c1, r2c2, r2c3], + // [r3c1, r3c2, r3c3] + // ] + param_ptr_z = tvp_data_z; + HashTable* rows_ht = Z_ARRVAL_P(tvp_data_z); + this->num_rows = zend_hash_num_elements(rows_ht); + if (this->num_rows == 0) { + // TVP has no data + return 0; + } + + // Given the table type name, get its column meta data next + size_t total_num_columns = 0; + get_tvp_metadata(stmt, reinterpret_cast(ZSTR_VAL(tvp_name))); + total_num_columns = tvp_columns.size(); + + // (1) Is the array empty? + // (2) Check individual rows and see if their sizes are consistent? + zend_ulong id = -1; + zend_string *key = NULL; + zval* row_z = NULL; + int num_columns = 0; + int type = HASH_KEY_NON_EXISTENT; + + // Loop through the rows to check the number of columns + ZEND_HASH_FOREACH_KEY_VAL(rows_ht, id, key, row_z) { + type = key ? HASH_KEY_IS_STRING : HASH_KEY_IS_LONG; + CHECK_CUSTOM_ERROR(type == HASH_KEY_IS_STRING, stmt, SQLSRV_ERROR_TVP_STRING_KEYS, param_pos + 1) { + throw core::CoreException(); + } + + if (Z_ISREF_P(row_z)) { + ZVAL_DEREF(row_z); + } + + // Individual row must be an array + CHECK_CUSTOM_ERROR(Z_TYPE_P(row_z) != IS_ARRAY, stmt, SQLSRV_ERROR_TVP_ROW_NOT_ARRAY, param_pos + 1) { + throw core::CoreException(); + } + + // Are all the TVP's rows the same size + num_columns = zend_hash_num_elements(Z_ARRVAL_P(row_z)); + CHECK_CUSTOM_ERROR(num_columns != total_num_columns, stmt, SQLSRV_ERROR_TVP_ROWS_UNEXPECTED_SIZE, param_pos + 1, total_num_columns) { + throw core::CoreException(); + } + } ZEND_HASH_FOREACH_END(); + + // Return the number of columns + return num_columns; +} + +void sqlsrv_param_tvp::process_param_column_value(_Inout_ sqlsrv_stmt* stmt) +{ + // This is one of the constituent columns of the table-valued parameter + // The corresponding column value of the TVP's first row is already saved in + // the member variable param_ptr_z, which may be a NULL value + zval *data_z = param_ptr_z; + param_php_type = is_a_string_type(sql_data_type) ? IS_STRING : Z_TYPE_P(data_z); + + switch (param_php_type) { + case IS_TRUE: + case IS_FALSE: + case IS_LONG: + case IS_DOUBLE: + sqlsrv_param::process_param(stmt, data_z); + buffer = &placeholder_z.value; // use placeholder zval for binding later + break; + case IS_RESOURCE: + sqlsrv_param::process_resource_param(data_z); + break; + case IS_STRING: + case IS_OBJECT: + if (param_php_type == IS_STRING) { + derive_string_types_sizes(data_z); + } else { + // If preprocessing a datetime object fails, throw an error of invalid php type + bool succeeded = preprocess_datetime_object(stmt, data_z); + CHECK_CUSTOM_ERROR(!succeeded, stmt, SQLSRV_ERROR_TVP_INVALID_COLUMN_PHPTYPE, parent_tvp->param_pos + 1, param_pos + 1) { + throw core::CoreException(); + } + } + buffer = reinterpret_cast(this); + buffer_length = 0; + strlen_or_indptr = SQL_DATA_AT_EXEC; + break; + case IS_NULL: + process_null_param_value(stmt); + break; + default: + THROW_CORE_ERROR(stmt, SQLSRV_ERROR_TVP_INVALID_COLUMN_PHPTYPE, parent_tvp->param_pos + 1, param_pos + 1); + break; + } + + // Release the reference + param_ptr_z = NULL; +} + +void sqlsrv_param_tvp::process_null_param_value(_Inout_ sqlsrv_stmt* stmt) +{ + // This is one of the constituent columns of the table-valued parameter + // This method is called when the corresponding column value of the TVP's first row is NULL + // So keep looking in the subsequent rows and find the first non-NULL value in the same column + HashTable* rows_ht = Z_ARRVAL_P(parent_tvp->param_ptr_z); + zval* row_z = NULL; + zval* value_z = NULL; + int php_type = IS_NULL; + int row_id = 1; // Start from the second row + + while ((row_z = zend_hash_index_find(rows_ht, row_id++)) != NULL) { + if (Z_ISREF_P(row_z)) { + ZVAL_DEREF(row_z); + } + + value_z = zend_hash_index_find(Z_ARRVAL_P(row_z), param_pos); + php_type = Z_TYPE_P(value_z); + if (php_type != IS_NULL) { + // Save this non-NULL value before calling process_param_column_value() + param_ptr_z = value_z; + process_param_column_value(stmt); + break; + } + } + + if (php_type == IS_NULL) { + // This means that the entire column contains nothing but NULLs + sqlsrv_param::process_null_param(param_ptr_z); + } +} + +void sqlsrv_param_tvp::bind_param(_Inout_ sqlsrv_stmt* stmt) +{ + core::SQLBindParameter(stmt, param_pos + 1, direction, c_data_type, sql_data_type, column_size, decimal_digits, buffer, buffer_length, &strlen_or_indptr); + + // No need to continue if this is one of the constituent columns of the table-valued parameter + if (sql_data_type != SQL_SS_TABLE) { + return; + } + + if (num_rows == 0) { + // TVP has no data + return; + } + + // Bind the TVP columns one by one + // Register this object first using SQLSetDescField() for sending TVP data post execution + SQLHDESC desc; + core::SQLGetStmtAttr(stmt, SQL_ATTR_APP_PARAM_DESC, &desc, 0, 0); + SQLRETURN r = ::SQLSetDescField(desc, param_pos + 1, SQL_DESC_DATA_PTR, reinterpret_cast(this), 0); + CHECK_SQL_ERROR_OR_WARNING(r, stmt) { + throw core::CoreException(); + } + + // First set focus on this parameter + size_t ordinal = param_pos + 1; + core::SQLSetStmtAttr(stmt, SQL_SOPT_SS_PARAM_FOCUS, reinterpret_cast(ordinal), SQL_IS_INTEGER); + + // Bind the TVP columns + SQLSRV_ENCODING stmt_encoding = (stmt->encoding() == SQLSRV_ENCODING_DEFAULT) ? stmt->conn->encoding() : stmt->encoding(); + HashTable* rows_ht = Z_ARRVAL_P(param_ptr_z); + zval* row_z = zend_hash_index_find(rows_ht, 0); + + if (Z_ISREF_P(row_z)) { + ZVAL_DEREF(row_z); + } + + HashTable* cols_ht = Z_ARRVAL_P(row_z); + zend_ulong id = -1; + zend_string *key = NULL; + zval* data_z = NULL; + int num_columns = 0; + + // In case there are null values in the first row, have to loop + // through the entire first row of column values using the Zend macros. + ZEND_HASH_FOREACH_KEY_VAL(cols_ht, id, key, data_z) { + int type = key ? HASH_KEY_IS_STRING : HASH_KEY_IS_LONG; + CHECK_CUSTOM_ERROR(type == HASH_KEY_IS_STRING, stmt, SQLSRV_ERROR_TVP_STRING_KEYS, param_pos + 1) { + throw core::CoreException(); + } + + // Assume the user has supplied data for all columns in the right order + SQLUSMALLINT pos = static_cast(id); + sqlsrv_param* column_param = tvp_columns[pos]; + SQLSRV_ASSERT(column_param != NULL, "sqlsrv_param_tvp::bind_param -- column param should not be null"); + + // If data_z is NULL, will need to keep looking in the subsequent rows of + // the same column until a non-null value is found. Since Zend macros must be + // used to traverse the array items, nesting Zend macros in different directions + // does not work. + // Therefore, save data_z for later processing and binding. + column_param->param_ptr_z = data_z; + num_columns++; + } ZEND_HASH_FOREACH_END(); + + // Process the columns and bind each of them using the saved data + for (int i = 0; i < num_columns; i++) { + sqlsrv_param* column_param = tvp_columns[i]; + + column_param->process_param(stmt, NULL); + column_param->bind_param(stmt); + } + + // Reset focus + core::SQLSetStmtAttr(stmt, SQL_SOPT_SS_PARAM_FOCUS, reinterpret_cast(0), SQL_IS_INTEGER); +} + +// For each of the constituent columns of the table-valued parameter, check its PHP type +// For pure scalar types, map the cell value (based on current_row and ordinal) to the +// member placeholder_z +void sqlsrv_param_tvp::populate_cell_placeholder(_Inout_ sqlsrv_stmt* stmt, _In_ int ordinal) +{ + if (sql_data_type == SQL_SS_TABLE || ordinal >= num_rows) { + return; + } + + zval* row_z = NULL; + HashTable* values_ht = NULL; + zval* value_z = NULL; + int type = IS_NULL; + + switch (param_php_type) { + case IS_TRUE: + case IS_FALSE: + case IS_LONG: + case IS_DOUBLE: + // Find the row from the TVP data based on ordinal + row_z = zend_hash_index_find(Z_ARRVAL_P(parent_tvp->param_ptr_z), ordinal); + if (Z_ISREF_P(row_z)) { + ZVAL_DEREF(row_z); + } + // Now find the column value based on param_pos + value_z = zend_hash_index_find(Z_ARRVAL_P(row_z), param_pos); + type = Z_TYPE_P(value_z); + + // First check if value_z is NULL + if (type == IS_NULL) { + ZVAL_NULL(&placeholder_z); + strlen_or_indptr = SQL_NULL_DATA; + } else { + // Once the placeholder is bound with the correct value from the array, update current_row + if (param_php_type == IS_DOUBLE) { + if (type != IS_DOUBLE) { + // If value_z type is different from param_php_type convert first + convert_to_double(value_z); + } + strlen_or_indptr = sizeof(Z_DVAL_P(value_z)); + ZVAL_DOUBLE(&placeholder_z, Z_DVAL_P(value_z)); + } else { + if (type != IS_LONG) { + // If value_z type is different from param_php_type convert first + // Even for boolean values + convert_to_long(value_z); + } + strlen_or_indptr = sizeof(Z_LVAL_P(value_z)); + ZVAL_LONG(&placeholder_z, Z_LVAL_P(value_z)); + } + } + current_row++; + break; + default: + // Do nothing for non-scalar types + break; + } +} + +// If this is the table-valued parameter, loop through each parameter column +// and populate the cell's placeholder_z. +// If this is one of the constituent columns of the table-valued parameter, +// call SQLPutData() to send the cell value to the server (based on current_row +// and param_pos) +bool sqlsrv_param_tvp::send_data_packet(_Inout_ sqlsrv_stmt* stmt) +{ + if (sql_data_type != SQL_SS_TABLE) { + // This is one of the constituent columns of the table-valued parameter + // Check current_row first + if (current_row >= num_rows) { + return false; + } + + // Find the row from the TVP data based on current_row + zval* row_z = zend_hash_index_find(Z_ARRVAL_P(parent_tvp->param_ptr_z), current_row); + if (Z_ISREF_P(row_z)) { + ZVAL_DEREF(row_z); + } + // Now find the column value based on param_pos + zval* value_z = zend_hash_index_find(Z_ARRVAL_P(row_z), param_pos); + + // First check if value_z is NULL + if (Z_TYPE_P(value_z) == IS_NULL) { + core::SQLPutData(stmt, NULL, SQL_NULL_DATA); + current_row++; + } else { + switch (param_php_type) { + case IS_RESOURCE: + { + num_bytes_read = 0; + param_stream = NULL; + + // Get the stream from the zval value + core::sqlsrv_php_stream_from_zval_no_verify(*stmt, param_stream, value_z); + // Keep sending the packets until EOF is reached + while (sqlsrv_param::send_data_packet(stmt)) { + } + current_row++; + } + break; + case IS_OBJECT: + { + // This method updates placeholder_z as a string + bool succeeded = convert_datetime_to_string(stmt, value_z); + + // Conversion failed so assume the input was an invalid PHP type + CHECK_CUSTOM_ERROR(!succeeded, stmt, SQLSRV_ERROR_TVP_INVALID_COLUMN_PHPTYPE, parent_tvp->param_pos + 1, param_pos + 1) { + throw core::CoreException(); + } + + core::SQLPutData(stmt, Z_STRVAL(placeholder_z), SQL_NTS); + current_row++; + } + break; + case IS_STRING: + { + int type = Z_TYPE_P(value_z); + if (type != IS_STRING) { + convert_to_string(value_z); + } + SQLLEN value_len = Z_STRLEN_P(value_z); + if (value_len == 0) { + // If it's an empty string + core::SQLPutData(stmt, Z_STRVAL_P(value_z), 0); + } else { + if (encoding == CP_UTF8 && !is_a_numeric_type(sql_data_type)) { + if (value_len > INT_MAX) { + LOG(SEV_ERROR, "Convert input parameter to utf16: buffer length exceeded."); + throw core::CoreException(); + } + // This method would change the member placeholder_z + bool succeeded = convert_input_str_to_utf16(stmt, value_z); + CHECK_CUSTOM_ERROR(!succeeded, stmt, SQLSRV_ERROR_TVP_STRING_ENCODING_TRANSLATE, parent_tvp->param_pos + 1, param_pos + 1, get_last_error_message()) { + throw core::CoreException(); + } + + send_string_data_in_batches(stmt, &placeholder_z); + } else { + send_string_data_in_batches(stmt, value_z); + } + } + current_row++; + } + break; + default: + // Do nothing for basic types as they should be processed elsewhere + break; + } + } // else not IS_NULL + } else { + // This is the table-valued parameter + if (current_row < num_rows) { + // Loop through the table parameter columns and populate each cell's placeholder whenever applicable + for (size_t i = 0; i < tvp_columns.size(); i++) { + tvp_columns[i]->populate_cell_placeholder(stmt, current_row); + } + + // This indicates a TVP row is available + core::SQLPutData(stmt, reinterpret_cast(1), 1); + current_row++; + } else { + // This indicates there is no more TVP row + core::SQLPutData(stmt, reinterpret_cast(0), 0); + } + } + + // Return false to indicate that the current row has been sent + return false; +} + +// A helper method for sending large string data in batches +void sqlsrv_param_tvp::send_string_data_in_batches(_Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z) +{ + SQLLEN len = Z_STRLEN_P(value_z); + SQLLEN batch = (encoding == CP_UTF8) ? PHP_STREAM_BUFFER_SIZE / sizeof(SQLWCHAR) : PHP_STREAM_BUFFER_SIZE; + + char* p = Z_STRVAL_P(value_z); + while (len > batch) { + core::SQLPutData(stmt, p, batch); + len -= batch; + p += batch; + } + + // Put final batch + core::SQLPutData(stmt, p, len); +} + void sqlsrv_params_container::clean_up_param_data(_In_opt_ bool only_input/* = false*/) { current_param = NULL; @@ -3051,7 +3661,7 @@ sqlsrv_param* sqlsrv_params_container::find_param(_In_ SQLUSMALLINT param_num, _ } else { return output_params.at(param_num); } - } catch (std::out_of_range& e) { + } catch (std::out_of_range&) { // not found return NULL; } diff --git a/source/shared/core_util.cpp b/source/shared/core_util.cpp index 1b8a2d66..2cd9ef52 100644 --- a/source/shared/core_util.cpp +++ b/source/shared/core_util.cpp @@ -237,8 +237,8 @@ void convert_datetime_string_to_zval(_Inout_ sqlsrv_stmt* stmt, _In_opt_ char* i ZVAL_UNDEF(params); // Convert the datetime string to a PHP DateTime object - core::sqlsrv_zval_stringl(&value_temp_z, input, length); - core::sqlsrv_zval_stringl(&function_z, "date_create", sizeof("date_create") - 1); + ZVAL_STRINGL(&value_temp_z, input, length); + ZVAL_STRINGL(&function_z, "date_create", sizeof("date_create") - 1); params[0] = value_temp_z; if (call_user_function(EG(function_table), NULL, &function_z, &out_zval, 1, diff --git a/source/shared/msodbcsql.h b/source/shared/msodbcsql.h index 436fa7fa..f0b9ab5c 100644 --- a/source/shared/msodbcsql.h +++ b/source/shared/msodbcsql.h @@ -77,6 +77,8 @@ #define SQL_SOPT_SS_BASE 1225 #define SQL_SOPT_SS_TEXTPTR_LOGGING (SQL_SOPT_SS_BASE+0) // Text pointer logging #define SQL_SOPT_SS_NOBROWSETABLE (SQL_SOPT_SS_BASE+3) // Set NOBROWSETABLE option +#define SQL_SOPT_SS_PARAM_FOCUS (SQL_SOPT_SS_BASE+11)// Direct subsequent calls to parameter related methods to set properties on constituent columns/parameters of container types +#define SQL_SOPT_SS_NAME_SCOPE (SQL_SOPT_SS_BASE+12)// Sets name scope for subsequent catalog function calls #define SQL_SOPT_SS_COLUMN_ENCRYPTION (SQL_SOPT_SS_BASE+13)// Sets the column encryption mode // Define old names #define SQL_TEXTPTR_LOGGING SQL_SOPT_SS_TEXTPTR_LOGGING @@ -180,6 +182,10 @@ #define SQL_COLUMN_ENCRYPTION_DEFAULT SQL_COLUMN_ENCRYPTION_DISABLE // Defines for use with SQL_COPT_SS_CEKCACHETTL #define SQL_CEKCACHETTL_DEFAULT 7200L // TTL value in seconds (2 hours) +//SQL_SOPT_SS_NAME_SCOPE +#define SQL_SS_NAME_SCOPE_TABLE 0L +#define SQL_SS_NAME_SCOPE_TABLE_TYPE 1L +#define SQL_SS_NAME_SCOPE_DEFAULT SQL_SS_NAME_SCOPE_TABLE // SQL_COPT_SS_ENCRYPT #define SQL_EN_OFF 0L #define SQL_EN_ON 1L diff --git a/source/sqlsrv/init.cpp b/source/sqlsrv/init.cpp index ab8f282c..0a0f6a3f 100644 --- a/source/sqlsrv/init.cpp +++ b/source/sqlsrv/init.cpp @@ -335,6 +335,7 @@ PHP_MINIT_FUNCTION(sqlsrv) REGISTER_LONG_CONSTANT( "SQLSRV_PHPTYPE_INT", SQLSRV_PHPTYPE_INT, CONST_PERSISTENT | CONST_CS ); REGISTER_LONG_CONSTANT( "SQLSRV_PHPTYPE_FLOAT", SQLSRV_PHPTYPE_FLOAT, CONST_PERSISTENT | CONST_CS ); REGISTER_LONG_CONSTANT( "SQLSRV_PHPTYPE_DATETIME", SQLSRV_PHPTYPE_DATETIME, CONST_PERSISTENT | CONST_CS ); + REGISTER_LONG_CONSTANT( "SQLSRV_PHPTYPE_TABLE", SQLSRV_PHPTYPE_TABLE, CONST_PERSISTENT | CONST_CS); std::string bin = "binary"; std::string chr = "char"; @@ -377,6 +378,7 @@ PHP_MINIT_FUNCTION(sqlsrv) REGISTER_LONG_CONSTANT( "SQLSRV_SQLTYPE_TIMESTAMP", constant_type.value, CONST_PERSISTENT | CONST_CS ); REGISTER_LONG_CONSTANT( "SQLSRV_SQLTYPE_TINYINT", SQL_TINYINT, CONST_PERSISTENT | CONST_CS ); REGISTER_LONG_CONSTANT( "SQLSRV_SQLTYPE_UDT", SQL_SS_UDT, CONST_PERSISTENT | CONST_CS ); + REGISTER_LONG_CONSTANT( "SQLSRV_SQLTYPE_TABLE", SQL_SS_TABLE, CONST_PERSISTENT | CONST_CS); REGISTER_LONG_CONSTANT( "SQLSRV_SQLTYPE_UNIQUEIDENTIFIER", SQL_GUID, CONST_PERSISTENT | CONST_CS ); REGISTER_LONG_CONSTANT( "SQLSRV_SQLTYPE_XML", SQL_SS_XML, CONST_PERSISTENT | CONST_CS ); constant_type.typeinfo.type = SQL_TYPE_DATE; diff --git a/source/sqlsrv/stmt.cpp b/source/sqlsrv/stmt.cpp index 257697ec..594e4782 100644 --- a/source/sqlsrv/stmt.cpp +++ b/source/sqlsrv/stmt.cpp @@ -64,7 +64,7 @@ enum SQLSRV_PHPTYPE zend_to_sqlsrv_phptype[] = { SQLSRV_PHPTYPE_INT, SQLSRV_PHPTYPE_FLOAT, SQLSRV_PHPTYPE_STRING, - SQLSRV_PHPTYPE_INVALID, + SQLSRV_PHPTYPE_TABLE, SQLSRV_PHPTYPE_DATETIME, SQLSRV_PHPTYPE_STREAM, SQLSRV_PHPTYPE_INVALID, @@ -227,7 +227,9 @@ sqlsrv_phptype ss_sqlsrv_stmt::sql_type_to_php_type( _In_ SQLINTEGER sql_type, _ case SQL_REAL: ss_phptype.typeinfo.type = SQLSRV_PHPTYPE_FLOAT; break; - + case SQL_SS_TABLE: + ss_phptype.typeinfo.type = SQLSRV_PHPTYPE_TABLE; + break; case SQL_TYPE_DATE: case SQL_SS_TIMESTAMPOFFSET: case SQL_SS_TIME2: @@ -1244,6 +1246,11 @@ void bind_params( _Inout_ ss_sqlsrv_stmt* stmt ) throw core::CoreException(); } + // Table-valued parameters are input-only + CHECK_CUSTOM_ERROR(direction != SQL_PARAM_INPUT && (sql_type == SQL_SS_TABLE || php_out_type == SQLSRV_PHPTYPE_TABLE), stmt, SQLSRV_ERROR_TVP_INPUT_PARAM_ONLY) { + throw ss::SSException(); + } + // bind the parameter SQLSRV_ASSERT( value_z != NULL, "bind_params: value_z is null." ); core_sqlsrv_bind_param( stmt, static_cast( index ), direction, value_z, php_out_type, encoding, sql_type, column_size, @@ -1561,6 +1568,7 @@ bool determine_column_size_or_precision( sqlsrv_stmt const* stmt, _In_ sqlsrv_sq *column_size = INT_MAX >> 1; break; case SQL_SS_XML: + case SQL_SS_TABLE: *column_size = SQL_SS_LENGTH_UNLIMITED; break; case SQL_BINARY: @@ -1710,6 +1718,10 @@ sqlsrv_phptype determine_sqlsrv_php_type( _In_ ss_sqlsrv_stmt const* stmt, _In_ } break; } + case SQL_SS_TABLE: + sqlsrv_phptype.typeinfo.type = SQLSRV_PHPTYPE_TABLE; + sqlsrv_phptype.typeinfo.encoding = stmt->encoding(); + break; default: sqlsrv_phptype.typeinfo.type = PHPTYPE_INVALID; break; @@ -2076,6 +2088,7 @@ bool is_valid_sqlsrv_phptype( _In_ sqlsrv_phptype type ) case SQLSRV_PHPTYPE_INT: case SQLSRV_PHPTYPE_FLOAT: case SQLSRV_PHPTYPE_DATETIME: + case SQLSRV_PHPTYPE_TABLE: return true; case SQLSRV_PHPTYPE_STRING: case SQLSRV_PHPTYPE_STREAM: @@ -2120,6 +2133,7 @@ bool is_valid_sqlsrv_sqltype( _In_ sqlsrv_sqltype sql_type ) case SQL_TYPE_DATE: case SQL_SS_TIME2: case SQL_SS_TIMESTAMPOFFSET: + case SQL_SS_TABLE: break; default: return false; diff --git a/source/sqlsrv/util.cpp b/source/sqlsrv/util.cpp index bb57c2d2..fd086444 100644 --- a/source/sqlsrv/util.cpp +++ b/source/sqlsrv/util.cpp @@ -445,6 +445,42 @@ ss_error SS_ERRORS[] = { SQLSRV_ERROR_DATA_CLASSIFICATION_FAILED, { IMSSP, (SQLCHAR*) "Failed to retrieve Data Classification Sensitivity Metadata: %1!s!", -121, true} }, + { + SQLSRV_ERROR_TVP_STRING_ENCODING_TRANSLATE, + { IMSSP, (SQLCHAR*) "An error occurred translating a string for Table-Valued Param %1!d! Column %2!d! to UTF-16: %3!s!", -122, true } + }, + { + SQLSRV_ERROR_TVP_INVALID_COLUMN_PHPTYPE, + { IMSSP, (SQLCHAR*) "An invalid type for Table-Valued Param %1!d! Column %2!d! was specified", -123, true } + }, + { + SQLSRV_ERROR_TVP_FETCH_METADATA, + { IMSSP, (SQLCHAR*) "Failed to get metadata for Table-Valued Param %1!d!", -124, true } + }, + { + SQLSRV_ERROR_TVP_INVALID_INPUTS, + { IMSSP, (SQLCHAR*) "Invalid inputs for Table-Valued Param %1!d!", -125, true } + }, + { + SQLSRV_ERROR_TVP_INVALID_TABLE_TYPE_NAME, + { IMSSP, (SQLCHAR*) "Expect a non-empty string for a Type Name for Table-Valued Param %1!d!", -126, true } + }, + { + SQLSRV_ERROR_TVP_ROWS_UNEXPECTED_SIZE, + { IMSSP, (SQLCHAR*) "For Table-Valued Param %1!d! the number of values in a row is expected to be %2!d!", -127, true } + }, + { + SQLSRV_ERROR_TVP_STRING_KEYS, + { IMSSP, (SQLCHAR*) "Associative arrays not allowed for Table-Valued Param %1!d!", -128, true } + }, + { + SQLSRV_ERROR_TVP_ROW_NOT_ARRAY, + { IMSSP, (SQLCHAR*) "Expect an array for each row for Table-Valued Param %1!d!", -129, true } + }, + { + SQLSRV_ERROR_TVP_INPUT_PARAM_ONLY, + { IMSSP, (SQLCHAR*) "You cannot return data in a table-valued parameter. Table-valued parameters are input-only.", -130, false } + }, // terminate the list of errors/warnings { UINT_MAX, {} } diff --git a/test/functional/inc/awc_tee_male_large.gif b/test/functional/inc/awc_tee_male_large.gif new file mode 100644 index 00000000..2191927e Binary files /dev/null and b/test/functional/inc/awc_tee_male_large.gif differ diff --git a/test/functional/inc/silver_chain_large.gif b/test/functional/inc/silver_chain_large.gif new file mode 100644 index 00000000..b65d143b Binary files /dev/null and b/test/functional/inc/silver_chain_large.gif differ diff --git a/test/functional/inc/superlight_black_f_large.gif b/test/functional/inc/superlight_black_f_large.gif new file mode 100644 index 00000000..59b7ca61 Binary files /dev/null and b/test/functional/inc/superlight_black_f_large.gif differ diff --git a/test/functional/inc/test_tvp_data.php b/test/functional/inc/test_tvp_data.php new file mode 100644 index 00000000..f1f31131 --- /dev/null +++ b/test/functional/inc/test_tvp_data.php @@ -0,0 +1,209 @@ + \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc b/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc index a6edb8d8..c5b79a22 100644 --- a/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc +++ b/test/functional/pdo_sqlsrv/MsCommon_mid-refactor.inc @@ -8,6 +8,10 @@ */ +$tvpIncPath = dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'inc'.DIRECTORY_SEPARATOR; + +require_once($tvpIncPath. 'test_tvp_data.php'); + // // looks like an additional file (in addition to pdo_test_base.inc) may be needed for these PHPTs // to be runnable from the MSSQL teams' internal proprietary test running system @@ -1794,3 +1798,11 @@ function PhpVersionComponents(&$major, &$minor, &$sub) $minor = strtok("."); $sub = strtok("."); } + +function getTodayDateAsString($conn) +{ + $tsql = 'SELECT CONVERT (VARCHAR(20), GETDATE())'; + $stmt = $conn->query($tsql); + $row = $stmt->fetch(PDO::FETCH_NUM); + return $row[0]; +} diff --git a/test/functional/pdo_sqlsrv/pdo_test_TVP_bind_binary_values.phpt b/test/functional/pdo_sqlsrv/pdo_test_TVP_bind_binary_values.phpt new file mode 100644 index 00000000..9f235fde --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_test_TVP_bind_binary_values.phpt @@ -0,0 +1,95 @@ +--TEST-- +Test Table-valued parameter using bindValue() and random null inputs +--DESCRIPTION-- +Test Table-valued parameter using bindValue() instead of bindParam() with random null values. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + dropProc($conn, 'SelectTVP2'); + + $tvpType = 'TestTVP2'; + $dropTableType = dropTableTypeSQL($conn, $tvpType); + $conn->exec($dropTableType); + + // Create table type and a stored procedure + $conn->exec($createTestTVP2); + $conn->exec($createSelectTVP2); + + // Create column arrays + $str1 = "Šỡოē šâოрĺẻ ÅŚÇÏЇ-ťếхţ"; + $longStr1 = str_repeat($str1, 1500); + $str2 = pack("H*", '49006427500048005000' ); // I'LOVE_SYMBOL'PHP + $longStr2 = str_repeat($str2, 2000); + + $bin1 = pack('H*', '0FD1CEFACE'); + $bin2 = pack('H*', '0001020304'); + $bin3 = hex2bin('616263646566676869'); // abcdefghi + $bin4 = pack('H*', '7A61CC86C7BDCEB2F18FB3BF'); + + $xml = "The quick brown fox jumps over the lazy dog0123456789"; + + $c01 = [null, $str1, $str2]; + $c02 = [null, $longStr1, $longStr2]; + $c03 = [null, null, 999]; + $c04 = [null, 3.1415927, null]; + $c05 = [$bin1, null, $bin2]; + $c06 = [null, $bin3, $bin4]; + $c07 = [null, '1234.56', '9876.54']; + $c08 = [null, null, $xml]; + $c09 = [4.321, 'CF43B0B3-E645-48C4-9F25-1A2BB4CE581A', (0xCBAB)]; + + // Create a TVP input array + $nrows = 3; + $ncols = 8; + $params = array(); + for ($i = 0; $i < $nrows; $i++) { + $rowValues = array($c01[$i], $c02[$i], $c03[$i], $c04[$i], $c05[$i], $c06[$i], $c07[$i], $c08[$i], $c09[$i]); + array_push($params, $rowValues); + } + + $tvpInput = array($tvpType => $params); + + // Prepare to call the stored procedure + $stmt = $conn->prepare($callSelectTVP2); + + // Bind parameters for the stored procedure + $stmt->bindValue(1, $tvpInput, PDO::PARAM_LOB); + $stmt->execute(); + + // Verify the results + $row = 0; + while ($result = $stmt->fetch(PDO::FETCH_NUM)) { + // Compare the values against the inputs + for ($col = 0; $col < $ncols; $col++) { + if ($result[$col] != $params[$row][$col]) { + echo 'Unexpected data at row ' . ($row + 1) . ' and col ' . ($col + 1) . PHP_EOL; + echo 'Expected: ' . $params[$row][$col] . PHP_EOL; + echo 'Fetched: ' . $result[$col] . PHP_EOL; + } + } + $row++; + } + unset($stmt); + + dropProc($conn, 'SelectTVP2'); + $conn->exec($dropTableType); + + unset($conn); + echo "Done" . PHP_EOL; + +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_test_TVP_bind_params.phpt b/test/functional/pdo_sqlsrv/pdo_test_TVP_bind_params.phpt new file mode 100644 index 00000000..b08535ea --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_test_TVP_bind_params.phpt @@ -0,0 +1,152 @@ +--TEST-- +Test Table-valued parameter using bindParam and no null values +--DESCRIPTION-- +Test Table-valued parameter using bindParam and no null values. This test verifies the fetched results of the all columns. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $tvpType = 'TVPParam'; + + dropProc($conn, 'TVPOrderEntry'); + dropTable($conn, 'TVPOrd'); + dropTable($conn, 'TVPItem'); + + $dropTableType = dropTableTypeSQL($conn, $tvpType); + $conn->exec($dropTableType); + + // Create tables and a stored procedure + $conn->exec($createTVPOrd); + $conn->exec($createTVPItem); + $conn->exec($createTVPParam); + $conn->exec($createTVPOrderEntry); + + $custCode = 'PDO_123'; + $ordNo = 0; + $ordDate = null; + + $image1 = fopen($tvpIncPath. $gif1, 'rb'); + $image2 = fopen($tvpIncPath. $gif2, 'rb'); + $image3 = fopen($tvpIncPath. $gif3, 'rb'); + $images = [$image1, $image2, $image3]; + + for ($i = 0; $i < count($items); $i++) { + array_push($items[$i], $images[$i]); + } + + // Create a TVP input array + $tvpInput = array($tvpType => $items); + + // Prepare to call the stored procedure + $stmt = $conn->prepare($callTVPOrderEntry); + + // Bind parameters for the stored procedure + $stmt->bindParam(1, $custCode); + $stmt->bindParam(2, $tvpInput, PDO::PARAM_LOB); + $stmt->bindParam(3, $ordNo, PDO::PARAM_INT, 10); + $stmt->bindParam(4, $ordDate, PDO::PARAM_STR, 20); + $stmt->execute(); + $stmt->closeCursor(); + + // Verify the results + echo "Order Number: $ordNo" . PHP_EOL; + + $today = getTodayDateAsString($conn); + if ($ordDate != $today) { + echo "Order Date unexpected: "; + var_dump($ordDate); + } + + // Fetch a random inserted image from the table and verify them + $n = rand(10,100); + $index = $n % count($images); + + $tsql = 'SELECT Photo FROM TVPItem WHERE ItemNo = ' . ($index + 1); + $stmt = $conn->query($tsql); + $stmt->bindColumn('Photo', $photo, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + if ($row = $stmt->fetch(PDO::FETCH_BOUND)) { + if (!verifyBinaryData($images[$index], $photo)) { + echo 'Image data corrupted for row '. ($index + 1) . PHP_EOL; + } + } else { + echo 'Failed in calling bindColumn' . PHP_EOL; + } + unset($photo); + + fclose($image1); + fclose($image2); + fclose($image3); + + // Fetch CustID + $tsql = 'SELECT CustID FROM TVPOrd'; + $stmt = $conn->query($tsql); + $row = $stmt->fetch(PDO::FETCH_NUM); + $id = $row[0]; + if ($id != $custCode) { + echo "Customer ID unexpected: " . PHP_EOL; + var_dump($id); + } + + // Fetch other basic types + $stmt = $conn->query($selectTVPItemQuery); + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + print_r($row); + } + unset($stmt); + + dropProc($conn, 'TVPOrderEntry'); + dropTable($conn, 'TVPOrd'); + dropTable($conn, 'TVPItem'); + $conn->exec($dropTableType); + + unset($conn); + echo "Done" . PHP_EOL; + +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +Order Number: 1 +Array +( + [OrdNo] => 1 + [ItemNo] => 1 + [ProductCode] => 0062836700 + [OrderQty] => 367 + [PackedOn] => 2009-03-12 + [Label] => AWC Tee Male Shirt + [Price] => 20.75 +) +Array +( + [OrdNo] => 1 + [ItemNo] => 2 + [ProductCode] => 1250153272 + [OrderQty] => 256 + [PackedOn] => 2017-11-07 + [Label] => Superlight Black Bicycle + [Price] => 998.45 +) +Array +( + [OrdNo] => 1 + [ItemNo] => 3 + [ProductCode] => 1328781505 + [OrderQty] => 260 + [PackedOn] => 2010-03-03 + [Label] => Silver Chain for Bikes + [Price] => 88.98 +) +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_test_TVP_bind_values.phpt b/test/functional/pdo_sqlsrv/pdo_test_TVP_bind_values.phpt new file mode 100644 index 00000000..76a42a23 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_test_TVP_bind_values.phpt @@ -0,0 +1,128 @@ +--TEST-- +Test Table-valued parameter using bindValue() and random null inputs +--DESCRIPTION-- +Test Table-valued parameter using bindValue() instead of bindParam() with random null values. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + dropProc($conn, 'SelectTVP'); + + $tvpType = 'TestTVP'; + $dropTableType = dropTableTypeSQL($conn, $tvpType); + $conn->exec($dropTableType); + + // Create table type and a stored procedure + $conn->exec($createTestTVP); + $conn->exec($createSelectTVP); + + // Create column arrays + $str = ''; + for ($i = 0; $i < 255; $i++) { + $str .= chr(($i % 95) + 32); + } + $longStr = str_repeat($str, 2000); + + $c01 = ['abcde', '', $str]; + $c02 = ['Hello world!', 'ABCDEFGHIJKLMNOP', $longStr]; + $c03 = [1, 0, 1]; + $c04 = [null, + null, + date_create('1955-12-13 12:20:00')]; + $c05 = [date_create('2384-12-31 12:40:12.34565'), null, date_create('1074-12-31 23:59:59.01234')]; + $c06 = ['4CDBC69F-F0EE-4963-8F17-24DD47090126', + '0F12A09D-D614-4998-AB1F-BD7CDBF6E3FE', + null]; + $c07 = ['1234567', '-9223372036854775808', '9223372036854775807']; + $c08 = [null, -1.79E+308, 1.79E+308]; + $c09 = ['31234567890123.141243449787580175325274', + '0.000000000000000000000001', + '99999999999999.999999999999999999999999']; + + // Create a TVP input array + $nrows = 3; + $ncols = 9; + $params = array(); + for ($i = 0; $i < $nrows; $i++) { + $rowValues = array($c01[$i], $c02[$i], $c03[$i], $c04[$i], $c05[$i], $c06[$i], $c07[$i], $c08[$i], $c09[$i]); + array_push($params, $rowValues); + } + + $tvpInput = array($tvpType => $params); + + // Prepare to call the stored procedure + $stmt = $conn->prepare($callSelectTVP); + + // Bind parameters for the stored procedure + $stmt->bindValue(1, $tvpInput, PDO::PARAM_LOB); + $stmt->execute(); + + // Verify the results + $row = 0; + while ($result = $stmt->fetch(PDO::FETCH_NUM)) { + // For strings, compare their values + for ($col = 0; $col < 2; $col++) { + if ($result[$col] != $params[$row][$col]) { + echo 'Unexpected data at row ' . ($row + 1) . ' and col ' . ($col + 1) . PHP_EOL; + echo 'Expected: ' . $params[$row][$col] . PHP_EOL; + echo 'Fetched: ' . $result[$col] . PHP_EOL; + } + } + // For other types, print them + echo 'Row ' . ($row + 1) . ': from Col ' . ($col + 1) . ' to ' . $ncols . PHP_EOL; + for ($col = 2; $col < $ncols; $col++) { + var_dump($result[$col]); + } + echo PHP_EOL; + $row++; + } + unset($stmt); + + dropProc($conn, 'SelectTVP'); + $conn->exec($dropTableType); + + unset($conn); + echo "Done" . PHP_EOL; + +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +Row 1: from Col 3 to 9 +string(1) "1" +NULL +string(25) "2384-12-31 12:40:12.34565" +string(36) "4CDBC69F-F0EE-4963-8F17-24DD47090126" +string(7) "1234567" +NULL +string(39) "31234567890123.141243449787580175325274" + +Row 2: from Col 3 to 9 +string(1) "0" +NULL +NULL +string(36) "0F12A09D-D614-4998-AB1F-BD7CDBF6E3FE" +string(20) "-9223372036854775808" +string(10) "-1.79E+308" +string(25) ".000000000000000000000001" + +Row 3: from Col 3 to 9 +string(1) "1" +string(19) "1955-12-13 12:20:00" +string(25) "1074-12-31 23:59:59.01234" +NULL +string(19) "9223372036854775807" +string(9) "1.79E+308" +string(39) "99999999999999.999999999999999999999999" + +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_test_TVP_double_tvps.phpt b/test/functional/pdo_sqlsrv/pdo_test_TVP_double_tvps.phpt new file mode 100644 index 00000000..df9773f7 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_test_TVP_double_tvps.phpt @@ -0,0 +1,131 @@ +--TEST-- +Test Table-valued parameter with a stored procedure that takes two TVPs +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +exec($dropProcedure); + + $dropTableType = dropTableTypeSQL($conn, "TestTVP3", $schema); + $conn->exec($dropTableType); + $dropTableType = dropTableTypeSQL($conn, "SupplierType", $schema); + $conn->exec($dropTableType); + + $conn->exec($dropSchema); +} + +try { + $conn = new PDO("sqlsrv:server = $server; database=$databaseName;", $uid, $pwd); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // Use a different schema instead of dbo + $schema = 'Sales DB'; + cleanup($conn, $schema); + + // Create the table type and stored procedure + $conn->exec($createSchema); + $conn->exec($createTestTVP3); + $conn->exec($createSupplierType); + $conn->exec($createAddReview); + + // Create the TVP input arrays + $inputs1 = [ + [12345, 'μεγάλο'], + [67890, 'μεσαία'], + [45678, 'μικρές'], + ]; + + $inputs2 = [ + ['abcde', 12345, '2019-12-31 23:59:59.123456'], + ['fghij', 67890, '2000-07-15 12:30:30.5678'], + ['klmop', 45678, '2007-04-08 06:15:15.333'], + ]; + + $tvpType1 = "$schema.SupplierType"; + $tvpType2 = "$schema.TestTVP3"; + + $tvpInput1 = array($tvpType1 => $inputs1); + $tvpInput2 = array($tvpType2 => $inputs2); + + $image = fopen($tvpIncPath. 'superlight_black_f_large.gif', 'rb'); + + $stmt = $conn->prepare($callAddReview); + $stmt->bindParam(1, $tvpInput1, PDO::PARAM_LOB); + $stmt->bindParam(2, $tvpInput2, PDO::PARAM_LOB); + $stmt->bindParam(3, $image, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + $stmt->execute(); + + // Verify the results + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + print_r($row); + } + $stmt->nextRowset(); + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + print_r($row); + } + $stmt->nextRowset(); + if ($row = $stmt->fetch(PDO::FETCH_NUM)) { + if (!verifyBinaryData($image, $row[0])) { + echo 'The image is corrupted' . PHP_EOL; + } + } else { + echo 'Something went wrong reading the image' . PHP_EOL; + } + + fclose($image); + unset($stmt); + + cleanup($conn, $schema); + + unset($conn); + echo "Done" . PHP_EOL; + +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +Array +( + [SupplierId] => 12345 + [SupplierName] => μεγάλο +) +Array +( + [SupplierId] => 67890 + [SupplierName] => μεσαία +) +Array +( + [SupplierId] => 45678 + [SupplierName] => μικρές +) +Array +( + [SupplierId] => 12345 + [SalesDate] => 2019-12-31 23:59:59.1234560 + [Review] => abcde +) +Array +( + [SupplierId] => 67890 + [SalesDate] => 2000-07-15 12:30:30.5678000 + [Review] => fghij +) +Array +( + [SupplierId] => 45678 + [SalesDate] => 2007-04-08 06:15:15.3330000 + [Review] => klmop +) +Done \ No newline at end of file diff --git a/test/functional/pdo_sqlsrv/pdo_test_TVP_error_cases.phpt b/test/functional/pdo_sqlsrv/pdo_test_TVP_error_cases.phpt new file mode 100644 index 00000000..cc2ac9b7 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_test_TVP_error_cases.phpt @@ -0,0 +1,189 @@ +--TEST-- +Test various error cases with invalid Table-valued parameter inputs +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +prepare($proc); + + // Bind TVP for the stored procedure + if ($inputParam) { + $stmt->bindValue(1, $tvpInput, PDO::PARAM_LOB); + } else { + $stmt->bindParam(1, $tvpInput, PDO::PARAM_LOB, 100); + } + $stmt->execute(); + } catch (PDOException $e) { + echo "Error $caseNo: "; + echo $e->getMessage(); + echo PHP_EOL; + } +} + +function cleanup($conn, $schema, $tvpType, $procName) +{ + global $dropSchema; + + $dropProcedure = dropProcSQL($conn, "[$schema].[$procName]"); + $conn->exec($dropProcedure); + + $dropTableType = dropTableTypeSQL($conn, $tvpType, $schema); + $conn->exec($dropTableType); + + $conn->exec($dropSchema); +} + +try { + $conn = new PDO("sqlsrv:server = $server; database=$databaseName;", $uid, $pwd); + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // Use a different schema instead of dbo + $schema = 'Sales DB'; + $tvpType = 'TestTVP3'; + $procName = 'SelectTVP3'; + + cleanup($conn, $schema, $tvpType, $procName); + + // Create the table type and stored procedure + $conn->exec($createSchema); + $conn->exec($createTestTVP3); + $conn->exec($createSelectTVP3); + + // Create a TVP input array + $inputs = [ + ['ABC', 12345, null], + ['DEF', 6789, null], + ['GHI', null], + ]; + $str = 'dummy'; + + // Case (1) - do not provide TVP type name + $tvpInput = array($inputs); + invokeProc($conn, $callSelectTVP3, $tvpInput, 1); + + // Case (2) - use an empty string as TVP type name + $tvpInput = array("" => array()); + invokeProc($conn, $callSelectTVP3, $tvpInput, 2); + + // The TVP name should include the schema + $tvpTypeName = "$schema.$tvpType"; + + // Case (3) - null inputs + $tvpInput = array($tvpTypeName => null); + invokeProc($conn, $callSelectTVP3, $tvpInput, 3); + + // Case (4) - not using array as inputs + $tvpInput = array($tvpTypeName => 1); + invokeProc($conn, $callSelectTVP3, $tvpInput, 4); + + // Case (5) - invalid TVP type name + $tvpInput = array($str => $inputs); + invokeProc($conn, $callSelectTVP3, $tvpInput, 5); + + // Case (6) - input rows are not the same size + $tvpInput = array($tvpTypeName => $inputs); + invokeProc($conn, $callSelectTVP3, $tvpInput, 6); + + // Case (7) - input row wrong size + unset($inputs); + $inputs = [ + ['ABC', 12345, null, null] + ]; + $tvpInput = array($tvpTypeName => $inputs); + invokeProc($conn, $callSelectTVP3, $tvpInput, 7); + + // Case (8) - use string keys + unset($inputs); + $inputs = [ + ['A' => null, null, null] + ]; + $tvpInput = array($tvpTypeName => $inputs); + invokeProc($conn, $callSelectTVP3, $tvpInput, 8); + + // Case (9) - a row is not an array + unset($inputs); + $inputs = [null]; + $tvpInput = array($tvpTypeName => $inputs); + invokeProc($conn, $callSelectTVP3, $tvpInput, 9); + + // Case (10) - a column value used a string key + unset($inputs); + $inputs = [ + ['ABC', 12345, "key"=>null] + ]; + $tvpInput = array($tvpTypeName => $inputs); + invokeProc($conn, $callSelectTVP3, $tvpInput, 10); + + // Case (11) - invalid input object for a TVP column + class foo + { + function do_foo(){} + } + $bar = new foo; + unset($inputs); + $inputs = [ + ['ABC', 1234, $bar], + ['DEF', 6789, null], + ]; + $tvpInput = array($tvpTypeName => $inputs); + invokeProc($conn, $callSelectTVP3, $tvpInput, 11); + + // Case (12) - invalid input type for a TVP column + unset($inputs); + $inputs = [ + ['ABC', &$str, null], + ['DEF', 6789, null], + ]; + $tvpInput = array($tvpTypeName => $inputs); + invokeProc($conn, $callSelectTVP3, $tvpInput, 12); + + // Case (13) - bind a TVP as an OUTPUT param + invokeProc($conn, $callSelectTVP3, $tvpInput, 13, false); + + // Case (14) - test UTF-8 invalid/corrupt string for a TVP column + unset($inputs); + $utf8 = str_repeat("41", 8188); + $utf8 = $utf8 . "e38395e38395"; + $utf8 = substr_replace($utf8, "fe", 1000, 2); + $utf8 = pack("H*", $utf8); + + $inputs = [ + [$utf8, 1234, null], + ['DEF', 6789, null], + ]; + $tvpInput = array($tvpTypeName => $inputs); + invokeProc($conn, $callSelectTVP3, $tvpInput, 14); + + cleanup($conn, $schema, $tvpType, $procName); + + unset($conn); + echo "Done" . PHP_EOL; + +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECTF-- +Error 1: SQLSTATE[IMSSP]: Expect a non-empty string for a Type Name for Table-Valued Param 1 +Error 2: SQLSTATE[IMSSP]: Expect a non-empty string for a Type Name for Table-Valued Param 1 +Error 3: SQLSTATE[IMSSP]: Invalid inputs for Table-Valued Param 1 +Error 4: SQLSTATE[IMSSP]: Invalid inputs for Table-Valued Param 1 +Error 5: SQLSTATE[IMSSP]: Failed to get metadata for Table-Valued Param 1 +Error 6: SQLSTATE[IMSSP]: For Table-Valued Param 1 the number of values in a row is expected to be 3 +Error 7: SQLSTATE[IMSSP]: For Table-Valued Param 1 the number of values in a row is expected to be 3 +Error 8: SQLSTATE[IMSSP]: Associative arrays not allowed for Table-Valued Param 1 +Error 9: SQLSTATE[IMSSP]: Expect an array for each row for Table-Valued Param 1 +Error 10: SQLSTATE[IMSSP]: Associative arrays not allowed for Table-Valued Param 1 +Error 11: SQLSTATE[IMSSP]: An invalid type for Table-Valued Param 1 Column 3 was specified +Error 12: SQLSTATE[IMSSP]: An invalid type for Table-Valued Param 1 Column 2 was specified +Error 13: SQLSTATE[IMSSP]: You cannot return data in a table-valued parameter. Table-valued parameters are input-only. +Error 14: SQLSTATE[IMSSP]: An error occurred translating a string for Table-Valued Param 1 Column 1 to UTF-16: %a +Done diff --git a/test/functional/pdo_sqlsrv/pdo_test_TVP_nulls_buffered.phpt b/test/functional/pdo_sqlsrv/pdo_test_TVP_nulls_buffered.phpt new file mode 100644 index 00000000..45dae776 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_test_TVP_nulls_buffered.phpt @@ -0,0 +1,149 @@ +--TEST-- +Table-valued parameter with bindParam and named parameters. The initial values of a column are NULLs +--DESCRIPTION-- +Test Table-valued parameter with bindParam. The initial values of a column are NULLs. This test verifies the fetched results using client buffers. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $tvpType = 'TVPParam'; + + dropProc($conn, 'TVPOrderEntry'); + dropTable($conn, 'TVPOrd'); + dropTable($conn, 'TVPItem'); + + $dropTableType = dropTableTypeSQL($conn, $tvpType); + $conn->exec($dropTableType); + + // Create tables and a stored procedure + $conn->exec($createTVPOrd); + $conn->exec($createTVPItem); + $conn->exec($createTVPParam); + $conn->exec($createTVPOrderEntry); + + // Bind parameters for call to TVPOrderEntry + $custCode = 'PDO_789'; + $ordNo = 0; + $ordDate = null; + + // TVP supports column-wise binding + $image3 = fopen($tvpIncPath. $gif3, 'rb'); + $images = [null, null, $image3]; + + // Added images to $items + for ($i = 0; $i < count($items); $i++) { + array_push($items[$i], $images[$i]); + } + + // Create a TVP input array + $tvpInput = array($tvpType => $items); + + // Prepare to call the stored procedure + $stmt = $conn->prepare($callTVPOrderEntryNamed); + + $stmt->bindParam(':id', $custCode); + $stmt->bindParam(':tvp', $tvpInput, PDO::PARAM_LOB); + $stmt->bindParam(':ordNo', $ordNo, PDO::PARAM_INT, 10); + $stmt->bindParam(':ordDate', $ordDate, PDO::PARAM_STR, 20); + + $stmt->execute(); + $stmt->closeCursor(); + + // Verify the results + echo "Order Number: $ordNo" . PHP_EOL; + + $today = getTodayDateAsString($conn); + if ($ordDate != $today) { + echo "Order Date unexpected: "; + var_dump($ordDate); + } + + // Fetch CustID + $tsql = 'SELECT CustID FROM TVPOrd'; + $stmt = $conn->query($tsql); + $row = $stmt->fetch(PDO::FETCH_NUM); + $id = $row[0]; + if ($id != $custCode) { + echo "Customer ID unexpected: " . PHP_EOL; + var_dump($id); + } + + // Fetch the only image from the table that is not NULL + $tsql = 'SELECT ItemNo, Photo FROM TVPItem WHERE Photo IS NOT NULL ORDER BY ItemNo'; + $stmt = $conn->query($tsql); + $index = 2; + $stmt->bindColumn('Photo', $photo, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + if ($row = $stmt->fetch(PDO::FETCH_BOUND)) { + if (!verifyBinaryData($images[$index], $photo)) { + echo 'Image data corrupted for row '. ($index + 1) . PHP_EOL; + } + } else { + echo 'Failed in calling bindColumn' . PHP_EOL; + } + unset($photo); + fclose($image3); + + // Fetch other basic types + $stmt = $conn->prepare($selectTVPItemQuery, array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED)); + $stmt->execute(); + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + print_r($row); + } + unset($stmt); + + dropProc($conn, 'TVPOrderEntry'); + dropTable($conn, 'TVPOrd'); + dropTable($conn, 'TVPItem'); + $conn->exec($dropTableType); + + unset($conn); + echo "Done" . PHP_EOL; + +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +Order Number: 1 +Array +( + [OrdNo] => 1 + [ItemNo] => 1 + [ProductCode] => 0062836700 + [OrderQty] => 367 + [PackedOn] => 2009-03-12 + [Label] => AWC Tee Male Shirt + [Price] => 20.75 +) +Array +( + [OrdNo] => 1 + [ItemNo] => 2 + [ProductCode] => 1250153272 + [OrderQty] => 256 + [PackedOn] => 2017-11-07 + [Label] => Superlight Black Bicycle + [Price] => 998.45 +) +Array +( + [OrdNo] => 1 + [ItemNo] => 3 + [ProductCode] => 1328781505 + [OrderQty] => 260 + [PackedOn] => 2010-03-03 + [Label] => Silver Chain for Bikes + [Price] => 88.98 +) +Done diff --git a/test/functional/pdo_sqlsrv/pdo_test_TVP_with_nulls.phpt b/test/functional/pdo_sqlsrv/pdo_test_TVP_with_nulls.phpt new file mode 100644 index 00000000..311bbaa2 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_test_TVP_with_nulls.phpt @@ -0,0 +1,162 @@ +--TEST-- +Test Table-valued parameter using bindParam and some NULL inputs +--DESCRIPTION-- +Test Table-valued parameter using bindParam with some NULL input values. This test verifies the fetched results of all columns. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $tvpType = 'TVPParam'; + + dropProc($conn, 'TVPOrderEntry'); + dropTable($conn, 'TVPOrd'); + dropTable($conn, 'TVPItem'); + + $dropTableType = dropTableTypeSQL($conn, $tvpType); + $conn->exec($dropTableType); + + // Create tables and a stored procedure + $conn->exec($createTVPOrd); + $conn->exec($createTVPItem); + $conn->exec($createTVPParam); + $conn->exec($createTVPOrderEntry); + + // Bind parameters for call to TVPOrderEntry + $custCode = 'PDO_456'; + $ordNo = 0; + $ordDate = null; + + // Add null image to $items + for ($i = 0; $i < count($items); $i++) { + array_push($items[$i], null); + } + + // Randomly set some values to null + $items[1][0] = null; + $items[0][2] = null; + + // Create a TVP input array + $tvpInput = array($tvpType => $items); + + // Prepare to call the stored procedure + $stmt = $conn->prepare($callTVPOrderEntryNamed); + + $stmt->bindParam(':id', $custCode); + $stmt->bindParam(':tvp', $tvpInput, PDO::PARAM_LOB); + $stmt->bindParam(':ordNo', $ordNo, PDO::PARAM_INT, 10); + $stmt->bindParam(':ordDate', $ordDate, PDO::PARAM_STR, 20); + $stmt->execute(); + $stmt->closeCursor(); + + // Verify the results + echo "Order Number: $ordNo" . PHP_EOL; + + $today = getTodayDateAsString($conn); + if ($ordDate != $today) { + echo "Order Date unexpected: "; + var_dump($ordDate); + } + + // Fetch CustID + $tsql = 'SELECT CustID FROM TVPOrd'; + $stmt = $conn->query($tsql); + $row = $stmt->fetch(PDO::FETCH_NUM); + $id = $row[0]; + if ($id != $custCode) { + echo "Customer ID unexpected: " . PHP_EOL; + var_dump($id); + } + + // Fetch all columns + $tsql = 'SELECT * FROM TVPItem ORDER BY ItemNo'; + $stmt = $conn->query($tsql); + if ($row = $stmt->fetchall(PDO::FETCH_NUM)) { + var_dump($row); + } + unset($stmt); + + dropProc($conn, 'TVPOrderEntry'); + dropTable($conn, 'TVPOrd'); + dropTable($conn, 'TVPItem'); + $conn->exec($dropTableType); + + unset($conn); + echo "Done" . PHP_EOL; + +} catch (PDOException $e) { + var_dump($e->getMessage()); +} +?> +--EXPECT-- +Order Number: 1 +array(3) { + [0]=> + array(8) { + [0]=> + string(1) "1" + [1]=> + string(1) "1" + [2]=> + string(10) "0062836700" + [3]=> + string(3) "367" + [4]=> + NULL + [5]=> + string(18) "AWC Tee Male Shirt" + [6]=> + string(5) "20.75" + [7]=> + NULL + } + [1]=> + array(8) { + [0]=> + string(1) "1" + [1]=> + string(1) "2" + [2]=> + NULL + [3]=> + string(3) "256" + [4]=> + string(10) "2017-11-07" + [5]=> + string(24) "Superlight Black Bicycle" + [6]=> + string(6) "998.45" + [7]=> + NULL + } + [2]=> + array(8) { + [0]=> + string(1) "1" + [1]=> + string(1) "3" + [2]=> + string(10) "1328781505" + [3]=> + string(3) "260" + [4]=> + string(10) "2010-03-03" + [5]=> + string(22) "Silver Chain for Bikes" + [6]=> + string(5) "88.98" + [7]=> + NULL + } +} +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/MsCommon.inc b/test/functional/sqlsrv/MsCommon.inc index 195430d5..ddefaac6 100644 --- a/test/functional/sqlsrv/MsCommon.inc +++ b/test/functional/sqlsrv/MsCommon.inc @@ -11,6 +11,10 @@ require_once('MsHelper.inc'); require_once('MsSetup.inc'); +$tvpIncPath = dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'inc'.DIRECTORY_SEPARATOR; + +require_once($tvpIncPath. 'test_tvp_data.php'); + $usingUTF8data = false; function isWindows() @@ -559,4 +563,19 @@ function verifyError($error, $state, $message) } } +function getTodayDateAsString($conn) +{ + $tsql = 'SELECT CONVERT (VARCHAR(20), GETDATE())'; + $stmt = sqlsrv_query($conn, $tsql); + $result = sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC); + $today = ''; + if ($result) { + $today = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); + } else { + echo "Failed to get today's date as string: " . PHP_EOL; + print_r(sqlsrv_errors()); + } + + return $today; +} ?> diff --git a/test/functional/sqlsrv/sqlsrv_test_TVP_double_tvps.phpt b/test/functional/sqlsrv/sqlsrv_test_TVP_double_tvps.phpt new file mode 100644 index 00000000..c3d40b65 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_test_TVP_double_tvps.phpt @@ -0,0 +1,133 @@ +--TEST-- +Test Table-valued parameter with a stored procedure that takes two TVPs +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +'UTF-8', 'ReturnDatesAsStrings' => true)); + +// Use a different schema instead of dbo +$schema = 'Sales DB'; +cleanup($conn, $schema); + +// Create table types and stored procedures +sqlsrv_query($conn, $createSchema); +sqlsrv_query($conn, $createTestTVP3); +sqlsrv_query($conn, $createSupplierType); +sqlsrv_query($conn, $createAddReview); + +// Create the TVP input arrays +$inputs1 = [ + [12345, 'Large大'], + [67890, 'Medium中'], + [45678, 'Small小'], +]; + +$inputs2 = [ + ['ABCDE', 12345, '2019-12-31 23:59:59.123456'], + ['FGHIJ', 67890, '2000-07-15 12:30:30.5678'], + ['KLMOP', 45678, '2007-04-08 06:15:15.333'], +]; + +$tvpType1 = "$schema.SupplierType"; +$tvpType2 = "$schema.TestTVP3"; + +$tvpInput1 = array($tvpType1 => $inputs1); +$tvpInput2 = array($tvpType2 => $inputs2); + +$image = fopen($tvpIncPath. 'awc_tee_male_large.gif', 'rb'); + +$params = array(array($tvpInput1, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_TABLE, SQLSRV_SQLTYPE_TABLE), + array($tvpInput2, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_TABLE, SQLSRV_SQLTYPE_TABLE), + array($image, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY), SQLSRV_SQLTYPE_VARBINARY('MAX'))); + +$stmt = sqlsrv_query($conn, $callAddReview, $params); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +// Verify the results +$row = 0; +while ($result = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { + print_r($result); +} +sqlsrv_next_result($stmt); +while ($result = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { + print_r($result); +} +sqlsrv_next_result($stmt); +if (sqlsrv_fetch($stmt)) { + $photo = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY)); + if (!verifyBinaryStream($image, $photo)) { + echo 'Image data is corrupted' . PHP_EOL; + } +} else { + echo 'Something went wrong reading the image' . PHP_EOL; +} + +fclose($image); +cleanup($conn, $schema); + +sqlsrv_free_stmt($stmt); +sqlsrv_close($conn); + +echo "Done" . PHP_EOL; +?> +--EXPECT-- +Array +( + [SupplierId] => 12345 + [SupplierName] => Large大 +) +Array +( + [SupplierId] => 67890 + [SupplierName] => Medium中 +) +Array +( + [SupplierId] => 45678 + [SupplierName] => Small小 +) +Array +( + [SupplierId] => 12345 + [SalesDate] => 2019-12-31 23:59:59.1234560 + [Review] => ABCDE +) +Array +( + [SupplierId] => 67890 + [SalesDate] => 2000-07-15 12:30:30.5678000 + [Review] => FGHIJ +) +Array +( + [SupplierId] => 45678 + [SalesDate] => 2007-04-08 06:15:15.3330000 + [Review] => KLMOP +) +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/sqlsrv_test_TVP_error_cases.phpt b/test/functional/sqlsrv/sqlsrv_test_TVP_error_cases.phpt new file mode 100644 index 00000000..57ad3990 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_test_TVP_error_cases.phpt @@ -0,0 +1,187 @@ +--TEST-- +Test various error cases with invalid Table-valued parameter inputs +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +'UTF-8')); + +// Use a different schema instead of dbo +$schema = 'Sales DB'; +$tvpType = 'TestTVP3'; +$procName = 'SelectTVP3'; + +cleanup($conn, $schema, $tvpType, $procName); + +// Create table type and a stored procedure +sqlsrv_query($conn, $createSchema); +sqlsrv_query($conn, $createTestTVP3); +sqlsrv_query($conn, $createSelectTVP3); + +// Create a TVP input array +$inputs = [ + ['ABC', 12345, null], + ['DEF', 6789, null], + ['GHI', null], +]; +$str = 'dummy'; + +// Case (1) - do not provide TVP type name +$tvpInput = array($inputs); +invokeProc($conn, $callSelectTVP3, $tvpInput, 1); + +// Case (2) - use an empty string as TVP type name +$tvpInput = array("" => array()); +invokeProc($conn, $callSelectTVP3, $tvpInput, 2); + +// The TVP name should include the schema +$tvpTypeName = "$schema.$tvpType"; + +// Case (3) - null inputs +$tvpInput = array($tvpTypeName => null); +invokeProc($conn, $callSelectTVP3, $tvpInput, 3); + +// Case (4) - not using array as inputs +$tvpInput = array($tvpTypeName => 1); +invokeProc($conn, $callSelectTVP3, $tvpInput, 4); + +// Case (5) - invalid TVP type name +$tvpInput = array($str => $inputs); +invokeProc($conn, $callSelectTVP3, $tvpInput, 5); + +// Case (6) - input rows are not the same size +$tvpInput = array($tvpTypeName => $inputs); +invokeProc($conn, $callSelectTVP3, $tvpInput, 6); + +// Case (7) - input row wrong size +unset($inputs); +$inputs = [ + ['ABC', 12345, null, null] +]; +$tvpInput = array($tvpTypeName => $inputs); +invokeProc($conn, $callSelectTVP3, $tvpInput, 7); + +// Case (8) - use string keys +unset($inputs); +$inputs = [ + ['A' => null, null, null] +]; +$tvpInput = array($tvpTypeName => $inputs); +invokeProc($conn, $callSelectTVP3, $tvpInput, 8); + +// Case (9) - a row is not an array +unset($inputs); +$inputs = [null]; +$tvpInput = array($tvpTypeName => $inputs); +invokeProc($conn, $callSelectTVP3, $tvpInput, 9); + +// Case (10) - a column value used a string key +unset($inputs); +$inputs = [ + ['ABC', 12345, "key"=>null] +]; +$tvpInput = array($tvpTypeName => $inputs); +invokeProc($conn, $callSelectTVP3, $tvpInput, 10); + +// Case (11) - invalid input object for a TVP column +class foo +{ + function do_foo(){} +} +$bar = new foo; +unset($inputs); +$inputs = [ + ['ABC', 1234, $bar], + ['DEF', 6789, null], +]; +$tvpInput = array($tvpTypeName => $inputs); +invokeProc($conn, $callSelectTVP3, $tvpInput, 11); + +// Case (12) - invalid input type for a TVP column +unset($inputs); +$inputs = [ + ['ABC', &$str, null], + ['DEF', 6789, null], +]; +$tvpInput = array($tvpTypeName => $inputs); +invokeProc($conn, $callSelectTVP3, $tvpInput, 12); + +// Case (13) - bind a TVP as an OUTPUT param +invokeProc($conn, $callSelectTVP3, $tvpInput, 13, SQLSRV_PARAM_OUT); + +// Case (14) - test UTF-8 invalid/corrupt string for a TVP column +unset($inputs); +$utf8 = str_repeat("41", 8188); +$utf8 = $utf8 . "e38395e38395"; +$utf8 = substr_replace($utf8, "fe", 1000, 2); +$utf8 = pack("H*", $utf8); + +$inputs = [ + [$utf8, 1234, null], + ['DEF', 6789, null], +]; +$tvpInput = array($tvpTypeName => $inputs); +invokeProc($conn, $callSelectTVP3, $tvpInput, 14); + +cleanup($conn, $schema, $tvpType, $procName); + +sqlsrv_close($conn); + +echo "Done" . PHP_EOL; +?> +--EXPECTF-- +Error 1: Expect a non-empty string for a Type Name for Table-Valued Param 1 +Error 2: Expect a non-empty string for a Type Name for Table-Valued Param 1 +Error 3: Invalid inputs for Table-Valued Param 1 +Error 4: Invalid inputs for Table-Valued Param 1 +Error 5: Failed to get metadata for Table-Valued Param 1 +Error 6: For Table-Valued Param 1 the number of values in a row is expected to be 3 +Error 7: For Table-Valued Param 1 the number of values in a row is expected to be 3 +Error 8: Associative arrays not allowed for Table-Valued Param 1 +Error 9: Expect an array for each row for Table-Valued Param 1 +Error 10: Associative arrays not allowed for Table-Valued Param 1 +Error 11: An invalid type for Table-Valued Param 1 Column 3 was specified +Error 12: An invalid type for Table-Valued Param 1 Column 2 was specified +Error 13: You cannot return data in a table-valued parameter. Table-valued parameters are input-only. +Error 14: An error occurred translating a string for Table-Valued Param 1 Column 1 to UTF-16: %a +Done diff --git a/test/functional/sqlsrv/sqlsrv_test_TVP_prepare.phpt b/test/functional/sqlsrv/sqlsrv_test_TVP_prepare.phpt new file mode 100644 index 00000000..97903cee --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_test_TVP_prepare.phpt @@ -0,0 +1,149 @@ +--TEST-- +Test Table-valued parameter using prepare/execute and sqlsrv_send_stream_data with one NULL column +--DESCRIPTION-- +Test Table-valued parameter using prepare/execute and sqlsrv_send_stream_data with one column of NULL input values. This test verifies the fetched results of the basic data types. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + true)); + +$tvpType = 'TVPParam'; + +dropProc($conn, 'TVPOrderEntry'); +dropTable($conn, 'TVPOrd'); +dropTable($conn, 'TVPItem'); + +$dropTableType = dropTableTypeSQL($conn, $tvpType); +sqlsrv_query($conn, $dropTableType); + +// Create tables +sqlsrv_query($conn, $createTVPOrd); +sqlsrv_query($conn, $createTVPItem); + +// Create TABLE type for use as a TVP +sqlsrv_query($conn, $createTVPParam); + +// Create procedure with TVP parameters +sqlsrv_query($conn, $createTVPOrderEntry); + +// Bind parameters for call to TVPOrderEntry +$custCode = 'SRV_000'; + +// 2 - Items TVP +$images = [null, null, null]; + +for ($i = 0; $i < count($items); $i++) { + array_push($items[$i], $images[$i]); +} + +// Create a TVP input array +$tvpInput = array($tvpType => $items); + +$ordNo = 0; +$ordDate = null; + +$params = array($custCode, + array($tvpInput, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_TABLE, SQLSRV_SQLTYPE_TABLE), + array(&$ordNo, SQLSRV_PARAM_OUT), + array(&$ordDate, SQLSRV_PARAM_OUT, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR))); + +$options = array("SendStreamParamsAtExec" => 0); +$stmt = sqlsrv_prepare($conn, $callTVPOrderEntry, $params, $options); +if (!$stmt) { + print_r(sqlsrv_errors()); +} +$res = sqlsrv_execute($stmt); +if (!$res) { + print_r(sqlsrv_errors()); +} + +// Now call sqlsrv_send_stream_data in a loop +while (sqlsrv_send_stream_data($stmt)) { +} + +sqlsrv_next_result($stmt); + +// Verify the results +echo "Order Number: $ordNo" . PHP_EOL; + +$today = getTodayDateAsString($conn); +if ($ordDate != $today) { + echo "Order Date unexpected: "; + var_dump($ordDate); +} + +// Fetch CustID +$tsql = 'SELECT CustID FROM TVPOrd'; +$stmt = sqlsrv_query($conn, $tsql); + +if ($result = sqlsrv_fetch( $stmt, SQLSRV_FETCH_NUMERIC)) { + $id = sqlsrv_get_field($stmt, 0); + if ($id != $custCode) { + echo "Customer ID unexpected: " . PHP_EOL; + var_dump($id); + } +} else { + echo "Failed in fetching from TVPOrd: " . PHP_EOL; + print_r(sqlsrv_errors()); +} + +$stmt = sqlsrv_query($conn, 'SELECT * FROM TVPItem ORDER BY ItemNo'); +while ($row = sqlsrv_fetch_array( $stmt, SQLSRV_FETCH_ASSOC)) { + print_r($row); +} + +sqlsrv_free_stmt($stmt); + +dropProc($conn, 'TVPOrderEntry'); +dropTable($conn, 'TVPOrd'); +dropTable($conn, 'TVPItem'); +sqlsrv_query($conn, $dropTableType); + +sqlsrv_close($conn); +echo "Done" . PHP_EOL; +?> +--EXPECT-- +Order Number: 1 +Array +( + [OrdNo] => 1 + [ItemNo] => 1 + [ProductCode] => 0062836700 + [OrderQty] => 367 + [PackedOn] => 2009-03-12 + [Label] => AWC Tee Male Shirt + [Price] => 20.75 + [Photo] => +) +Array +( + [OrdNo] => 1 + [ItemNo] => 2 + [ProductCode] => 1250153272 + [OrderQty] => 256 + [PackedOn] => 2017-11-07 + [Label] => Superlight Black Bicycle + [Price] => 998.45 + [Photo] => +) +Array +( + [OrdNo] => 1 + [ItemNo] => 3 + [ProductCode] => 1328781505 + [OrderQty] => 260 + [PackedOn] => 2010-03-03 + [Label] => Silver Chain for Bikes + [Price] => 88.98 + [Photo] => +) +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/sqlsrv_test_TVP_prepare_buffered.phpt b/test/functional/sqlsrv/sqlsrv_test_TVP_prepare_buffered.phpt new file mode 100644 index 00000000..07b57347 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_test_TVP_prepare_buffered.phpt @@ -0,0 +1,175 @@ +--TEST-- +Test Table-valued parameter using prepare/execute and some NULL inputs +--DESCRIPTION-- +Test Table-valued parameter using prepare/execute and some NULL inputs. This test fetches results as objects using client buffers. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + true)); +$tvpType = 'TVPParam'; + +dropProc($conn, 'TVPOrderEntry'); +dropTable($conn, 'TVPOrd'); +dropTable($conn, 'TVPItem'); + +$dropTableType = dropTableTypeSQL($conn, $tvpType); +sqlsrv_query($conn, $dropTableType); + +// Create tables +sqlsrv_query($conn, $createTVPOrd); +sqlsrv_query($conn, $createTVPItem); + +// Create TABLE type for use as a TVP +sqlsrv_query($conn, $createTVPParam); + +// Create procedure with TVP parameters +sqlsrv_query($conn, $createTVPOrderEntry); + +// Bind parameters for call to TVPOrderEntry +$custCode = 'SRV_789'; + +// 2 - Items TVP +$image3 = fopen($tvpIncPath. $gif3, 'rb'); +$images = [null, null, $image3]; + +for ($i = 0; $i < count($items); $i++) { + array_push($items[$i], $images[$i]); +} + +// Randomly set some values to null +$items[0][1] = null; +$items[2][3] = null; +$items[0][2] = null; + +// Create a TVP input array +$tvpInput = array($tvpType => $items); + +$ordNo = 0; +$ordDate = null; + +$params = array($custCode, + array($tvpInput, SQLSRV_PARAM_IN, null, SQLSRV_SQLTYPE_TABLE), + array(&$ordNo, SQLSRV_PARAM_OUT), + array(&$ordDate, SQLSRV_PARAM_OUT, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR))); + +$stmt = sqlsrv_prepare($conn, $callTVPOrderEntry, $params); +if (!$stmt) { + print_r(sqlsrv_errors()); +} +$res = sqlsrv_execute($stmt); +if (!$res) { + print_r(sqlsrv_errors()); +} + +sqlsrv_next_result($stmt); + +// Verify the results +echo "Order Number: $ordNo" . PHP_EOL; + +$today = getTodayDateAsString($conn); +if ($ordDate != $today) { + echo "Order Date unexpected: "; + var_dump($ordDate); +} + +// Fetch the inserted data from the tables +$tsql = 'SELECT CustID FROM TVPOrd'; +$stmt = sqlsrv_query($conn, $tsql); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +if ($result = sqlsrv_fetch( $stmt, SQLSRV_FETCH_NUMERIC)) { + $id = sqlsrv_get_field($stmt, 0); + if ($id != $custCode) { + echo "Customer ID unexpected: " . PHP_EOL; + var_dump($id); + } +} else { + echo "Failed in fetching from TVPOrd: " . PHP_EOL; + print_r(sqlsrv_errors()); +} + +// Fetch the only image from the table that is not NULL +$tsql = 'SELECT ItemNo, Photo FROM TVPItem WHERE Photo IS NOT NULL ORDER BY ItemNo'; +$stmt = sqlsrv_query($conn, $tsql, array(), array("Scrollable"=>"buffered")); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +// Only the last image is not NULL +$index = 2; +while (sqlsrv_fetch($stmt)) { + $itemNo = sqlsrv_get_field($stmt, 0); + echo $itemNo . PHP_EOL; + $photo = sqlsrv_get_field($stmt, 1, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY)); + if (!verifyBinaryStream($images[$index], $photo)) { + echo "Stream data for image $index corrupted!" . PHP_EOL; + } +} +sqlsrv_free_stmt($stmt); +fclose($image3); + +// Fetch the other columns next +$stmt = sqlsrv_query($conn, $selectTVPItemQuery, array(), array("Scrollable"=>"buffered")); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +while ($item = sqlsrv_fetch_object($stmt)) { + print_r($item); +} + +sqlsrv_free_stmt($stmt); + +dropProc($conn, 'TVPOrderEntry'); +dropTable($conn, 'TVPOrd'); +dropTable($conn, 'TVPItem'); +sqlsrv_query($conn, $dropTableType); + +sqlsrv_close($conn); +echo "Done" . PHP_EOL; +?> +--EXPECT-- +Order Number: 1 +3 +stdClass Object +( + [OrdNo] => 1 + [ItemNo] => 1 + [ProductCode] => 0062836700 + [OrderQty] => + [PackedOn] => + [Label] => AWC Tee Male Shirt + [Price] => 20.75 +) +stdClass Object +( + [OrdNo] => 1 + [ItemNo] => 2 + [ProductCode] => 1250153272 + [OrderQty] => 256 + [PackedOn] => 2017-11-07 + [Label] => Superlight Black Bicycle + [Price] => 998.45 +) +stdClass Object +( + [OrdNo] => 1 + [ItemNo] => 3 + [ProductCode] => 1328781505 + [OrderQty] => 260 + [PackedOn] => 2010-03-03 + [Label] => + [Price] => 88.98 +) +Done diff --git a/test/functional/sqlsrv/sqlsrv_test_TVP_query.phpt b/test/functional/sqlsrv/sqlsrv_test_TVP_query.phpt new file mode 100644 index 00000000..3a22fe2d --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_test_TVP_query.phpt @@ -0,0 +1,166 @@ +--TEST-- +Test Table-valued parameter using direct queries and no null values +--DESCRIPTION-- +Test Table-valued parameter using direct queries and no null values. This test verifies the fetched results of all types. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + true)); + +$tvpType = 'TVPParam'; + +dropProc($conn, 'TVPOrderEntry'); +dropTable($conn, 'TVPOrd'); +dropTable($conn, 'TVPItem'); + +$dropTableType = dropTableTypeSQL($conn, $tvpType); +sqlsrv_query($conn, $dropTableType); + +// Create tables +sqlsrv_query($conn, $createTVPOrd); +sqlsrv_query($conn, $createTVPItem); + +// Create TABLE type for use as a TVP +sqlsrv_query($conn, $createTVPParam); + +// Create procedure with TVP parameters +sqlsrv_query($conn, $createTVPOrderEntry); + +// Bind parameters for call to TVPOrderEntry +$custCode = 'SRV_123'; + +// 2 - Items TVP +$image1 = fopen($tvpIncPath. $gif1, 'rb'); +$image2 = fopen($tvpIncPath. $gif2, 'rb'); +$image3 = fopen($tvpIncPath. $gif3, 'rb'); +$images = [$image1, $image2, $image3]; + +for ($i = 0; $i < count($items); $i++) { + array_push($items[$i], $images[$i]); +} + +// Create a TVP input array +$tvpInput = array($tvpType => $items); + +$ordNo = 0; +$ordDate = null; + +$params = array($custCode, + array($tvpInput, null, null, SQLSRV_SQLTYPE_TABLE), + array(&$ordNo, SQLSRV_PARAM_OUT), + array(&$ordDate, SQLSRV_PARAM_OUT, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR))); + +$stmt = sqlsrv_query($conn, $callTVPOrderEntry, $params); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +sqlsrv_next_result($stmt); + +// Verify the results +echo "Order Number: $ordNo" . PHP_EOL; + +$today = getTodayDateAsString($conn); +if ($ordDate != $today) { + echo "Order Date unexpected: "; + var_dump($ordDate); +} + +// Fetch CustID +$tsql = 'SELECT CustID FROM TVPOrd'; +$stmt = sqlsrv_query($conn, $tsql); + +if ($result = sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC)) { + $id = sqlsrv_get_field($stmt, 0); + if ($id != $custCode) { + echo "Customer ID unexpected: " . PHP_EOL; + var_dump($id); + } +} else { + echo "Failed in fetching from TVPOrd: " . PHP_EOL; + print_r(sqlsrv_errors()); +} + +// Fetch other basic types +$stmt = sqlsrv_query($conn, $selectTVPItemQuery); +while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC)) { + print_r($row); +} +sqlsrv_free_stmt($stmt); + +// Fetch the inserted images from the table and verify them +$tsql = 'SELECT Photo FROM TVPItem ORDER BY ItemNo'; +$stmt = sqlsrv_query($conn, $tsql); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +$index = 0; +while (sqlsrv_fetch($stmt)) { + $photo = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY)); + if (!verifyBinaryStream($images[$index], $photo)) { + echo "Image data corrupted for row ". ($index + 1) . PHP_EOL; + } + $index++; +} +if ($index == 0) { + echo 'Failed in fetching binary data' . PHP_EOL; +} + +sqlsrv_free_stmt($stmt); + +fclose($image1); +fclose($image2); +fclose($image3); + +dropProc($conn, 'TVPOrderEntry'); +dropTable($conn, 'TVPOrd'); +dropTable($conn, 'TVPItem'); +sqlsrv_query($conn, $dropTableType); + +sqlsrv_close($conn); + +echo "Done" . PHP_EOL; +?> +--EXPECT-- +Order Number: 1 +Array +( + [0] => 1 + [1] => 1 + [2] => 0062836700 + [3] => 367 + [4] => 2009-03-12 + [5] => AWC Tee Male Shirt + [6] => 20.75 +) +Array +( + [0] => 1 + [1] => 2 + [2] => 1250153272 + [3] => 256 + [4] => 2017-11-07 + [5] => Superlight Black Bicycle + [6] => 998.45 +) +Array +( + [0] => 1 + [1] => 3 + [2] => 1328781505 + [3] => 260 + [4] => 2010-03-03 + [5] => Silver Chain for Bikes + [6] => 88.98 +) +Done diff --git a/test/functional/sqlsrv/sqlsrv_test_TVP_query_binary_fields.phpt b/test/functional/sqlsrv/sqlsrv_test_TVP_query_binary_fields.phpt new file mode 100644 index 00000000..47196d36 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_test_TVP_query_binary_fields.phpt @@ -0,0 +1,123 @@ +--TEST-- +Test Table-valued parameter using direct queries and sqlsrv_send_stream_data with random null inputs +--DESCRIPTION-- +Test Table-valued parameter using direct queries and sqlsrv_send_stream_data with random null inputs. This test verifies the fetched results of all columns. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- +'UTF-8')); + +dropProc($conn, 'SelectTVP2'); +$tvpType = 'TestTVP2'; +$dropTableType = dropTableTypeSQL($conn, $tvpType); +sqlsrv_query($conn, $dropTableType); + +// Create table type and a stored procedure +sqlsrv_query($conn, $createTestTVP2); +sqlsrv_query($conn, $createSelectTVP2); + +// Create column arrays +$str1 = "Šỡოē šâოрĺẻ ÅŚÇÏЇ-ťếхţ"; +$longStr1 = str_repeat($str1, 1500); +$str2 = pack("H*", '49006427500048005000' ); // I'LOVE_SYMBOL'PHP +$longStr2 = str_repeat($str2, 2000); + +$bin1 = pack('H*', '0FD1CEFACE'); +$bin2 = pack('H*', '0001020304'); +$bin3 = hex2bin('616263646566676869'); // abcdefghi +$bin4 = pack('H*', '7A61CC86C7BDCEB2F18FB3BF'); + +$xml = "The quick brown fox jumps over the lazy dog0123456789"; + +$c01 = [null, $str1, $str2]; +$c02 = [null, $longStr1, $longStr2]; +$c03 = [null, null, 999]; +$c04 = [null, 3.1415927, null]; +$c05 = [$bin1, null, $bin2]; +$c06 = [null, $bin3, $bin4]; +$c07 = [null, '1234.56', '9876.54']; +$c08 = [null, null, $xml]; +$c09 = [9876, $str1, (0x0FAB)]; + +// Create a TVP input array +$nrows = 3; +$ncols = 9; +$inputs = array(); +for ($i = 0; $i < $nrows; $i++) { + $rowValues = array($c01[$i], $c02[$i], $c03[$i], $c04[$i], $c05[$i], $c06[$i], $c07[$i], $c08[$i], $c09[$i]); + array_push($inputs, $rowValues); +} + +$tvpInput = array($tvpType => $inputs); +$params = array(array($tvpInput, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_TABLE, SQLSRV_SQLTYPE_TABLE)); + +$stmt = sqlsrv_query($conn, $callSelectTVP2, $params); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +// Verify the results +$row = 0; +while ($result = sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC)) { + // For strings, compare their values + for ($col = 0; $col < 2; $col++) { + $field = sqlsrv_get_field($stmt, $col, SQLSRV_PHPTYPE_STRING('UTF-8')); + if ($field != $inputs[$row][$col]) { + echo 'Unexpected data at row ' . ($row + 1) . ' and col ' . ($col + 1) . PHP_EOL; + echo 'Expected: ' . $inputs[$row][$col] . PHP_EOL; + echo 'Fetched: ' . $field . PHP_EOL; + } + } + // For other types, print them + echo 'Row ' . ($row + 1) . ': from Col ' . ($col + 1) . ' to ' . $ncols . PHP_EOL; + for ($col = 2; $col < $ncols; $col++) { + $field = sqlsrv_get_field($stmt, $col, SQLSRV_PHPTYPE_STRING('UTF-8')); + var_dump($field); + } + echo PHP_EOL; + $row++; +} +sqlsrv_free_stmt($stmt); + +dropProc($conn, 'SelectTVP2'); +sqlsrv_query($conn, $dropTableType); +sqlsrv_close($conn); + +echo "Done" . PHP_EOL; +?> +--EXPECT-- +Row 1: from Col 3 to 9 +NULL +NULL +string(10) "0FD1CEFACE" +NULL +NULL +NULL +string(4) "9876" + +Row 2: from Col 3 to 9 +NULL +string(9) "3.1415927" +NULL +string(18) "616263646566676869" +string(9) "1234.5600" +NULL +string(46) "Šỡოē šâოрĺẻ ÅŚÇÏЇ-ťếхţ" + +Row 3: from Col 3 to 9 +string(3) "999" +NULL +string(10) "0001020304" +string(24) "7A61CC86C7BDCEB2F18FB3BF" +string(9) "9876.5400" +string(120) "The quick brown fox jumps over the lazy dog0123456789" +string(4) "4011" + +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/sqlsrv_test_TVP_query_with_nulls.phpt b/test/functional/sqlsrv/sqlsrv_test_TVP_query_with_nulls.phpt new file mode 100644 index 00000000..d92c4cf5 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_test_TVP_query_with_nulls.phpt @@ -0,0 +1,131 @@ +--TEST-- +Test Table-valued parameter using direct queries and sqlsrv_send_stream_data with random null inputs +--DESCRIPTION-- +Test Table-valued parameter using direct queries and sqlsrv_send_stream_data with random null inputs. This test verifies the fetched results of all columns. +--ENV-- +PHPT_EXEC=true +--SKIPIF-- + +--FILE-- + true)); + +dropProc($conn, 'SelectTVP'); + +$tvpType = 'TestTVP'; +$dropTableType = dropTableTypeSQL($conn, $tvpType); +sqlsrv_query($conn, $dropTableType); + +// Create table type and a stored procedure +sqlsrv_query($conn, $createTestTVP); +sqlsrv_query($conn, $createSelectTVP); + +// Create column arrays +$str = ''; +for ($i = 0; $i < 255; $i++) { + $str .= chr(($i % 95) + 32); +} +$longStr = str_repeat($str, 3000); + +$c01 = [$str, 'ABCDE', '']; +$c02 = ['abcdefghijklmnopqrstuvwxyz', null, $longStr]; +$c03 = [null, 0, 1]; +$c04 = [null, + date_create('1997-02-13 12:43:10'), + null]; +$c05 = ["2010-12-31 12:40:12.56679", null, "1965-02-18 23:59:59.43258"]; +$c06 = ['4CDBC69F-F0EE-4963-8F17-24DD47090126', + '0F12A09D-D614-4998-AB1F-BD7CDBF6E3FE', + null]; +$c07 = [null, '-9223372036854775808', '9223372036854775807']; +$c08 = [null, -1.79E+308, 1.79E+308]; +$c09 = ['31234567890123.141243449787580175325274', + '0.000000000000000000000001', + '99999999999999.999999999999999999999999']; + +// Create a TVP input array +$nrows = 3; +$ncols = 9; +$inputs = array(); +for ($i = 0; $i < $nrows; $i++) { + $rowValues = array($c01[$i], $c02[$i], $c03[$i], $c04[$i], $c05[$i], $c06[$i], $c07[$i], $c08[$i], $c09[$i]); + array_push($inputs, $rowValues); +} + +$tvpInput = array($tvpType => $inputs); +$params = array(array($tvpInput, null, SQLSRV_PHPTYPE_TABLE, SQLSRV_SQLTYPE_TABLE)); + +$options = array("SendStreamParamsAtExec" => 0); +$stmt = sqlsrv_query($conn, $callSelectTVP, $params, $options); +if (!$stmt) { + print_r(sqlsrv_errors()); +} + +// Now call sqlsrv_send_stream_data in a loop +while (sqlsrv_send_stream_data($stmt)) { +} + +// Verify the results +$row = 0; +while ($result = sqlsrv_fetch($stmt, SQLSRV_FETCH_NUMERIC)) { + // For strings, compare their values + for ($col = 0; $col < 2; $col++) { + $field = sqlsrv_get_field($stmt, $col, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR)); + if ($field != $inputs[$row][$col]) { + echo 'Unexpected data at row ' . ($row + 1) . ' and col ' . ($col + 1) . PHP_EOL; + echo 'Expected: ' . $inputs[$row][$col] . PHP_EOL; + echo 'Fetched: ' . $field . PHP_EOL; + } + } + // For other types, print them + echo 'Row ' . ($row + 1) . ': from Col ' . ($col + 1) . ' to ' . $ncols . PHP_EOL; + for ($col = 2; $col < $ncols; $col++) { + $field = sqlsrv_get_field($stmt, $col); + var_dump($field); + } + echo PHP_EOL; + $row++; +} +sqlsrv_free_stmt($stmt); + +dropProc($conn, 'SelectTVP'); +sqlsrv_query($conn, $dropTableType); +sqlsrv_close($conn); + +echo "Done" . PHP_EOL; +?> +--EXPECT-- +Row 1: from Col 3 to 9 +NULL +NULL +string(25) "2010-12-31 12:40:12.56679" +string(36) "4CDBC69F-F0EE-4963-8F17-24DD47090126" +NULL +NULL +string(39) "31234567890123.141243449787580175325274" + +Row 2: from Col 3 to 9 +int(0) +string(19) "1997-02-13 12:43:00" +NULL +string(36) "0F12A09D-D614-4998-AB1F-BD7CDBF6E3FE" +string(20) "-9223372036854775808" +float(-1.79E+308) +string(25) ".000000000000000000000001" + +Row 3: from Col 3 to 9 +int(1) +NULL +string(25) "1965-02-18 23:59:59.43258" +NULL +string(19) "9223372036854775807" +float(1.79E+308) +string(39) "99999999999999.999999999999999999999999" + +Done