Implemented table-valued parameter support (#1257)

This commit is contained in:
Jenny Tam 2021-05-25 15:36:01 -07:00 committed by GitHub
parent 5f2a14c8e9
commit 0da75f5b92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 3113 additions and 40 deletions

View file

@ -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 = &param->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<SQLUSMALLINT>( 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

View file

@ -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, {} }
};

View file

@ -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);

View file

@ -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<sqlsrv_param_tvp*> 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,

View file

@ -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<sqlsrv_param> 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<const char*>(str), static_cast<int>(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<char*>(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<char*>(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<char*>(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<char*>(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<char *>(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<SQLCHAR *>(schema), SQL_NTS, reinterpret_cast<SQLCHAR *>(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<sqlsrv_param_tvp> 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<SQLCHAR*>(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<SQLPOINTER>(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<SQLPOINTER>(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<SQLPOINTER>(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<SQLUSMALLINT>(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<SQLPOINTER>(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<SQLPOINTER>(1), 1);
current_row++;
} else {
// This indicates there is no more TVP row
core::SQLPutData(stmt, reinterpret_cast<SQLPOINTER>(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;
}

View file

@ -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,

View file

@ -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

View file

@ -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;

View file

@ -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<SQLUSMALLINT>( 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;

View file

@ -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, {} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,209 @@
<?php
$createTVPOrd = <<<ORD
CREATE TABLE TVPOrd(
OrdNo INTEGER IDENTITY(1,1),
OrdDate DATETIME,
CustID VARCHAR(10))
ORD;
$createTVPItem = <<<ITEM
CREATE TABLE TVPItem(
OrdNo INTEGER,
ItemNo INTEGER IDENTITY(1,1),
ProductCode CHAR(10),
OrderQty INTEGER,
PackedOn DATE,
Label NVARCHAR(30),
Price DECIMAL(5,2),
Photo VARBINARY(MAX))
ITEM;
$createTVPParam = <<<TYPE
CREATE TYPE TVPParam AS TABLE(
ProductCode CHAR(10),
OrderQty INTEGER,
PackedOn DATE,
Label NVARCHAR(30),
Price DECIMAL(5,2),
Photo VARBINARY(MAX))
TYPE;
$createTVPOrderEntry = <<<PROC
CREATE PROCEDURE TVPOrderEntry(
@CustID VARCHAR(10),
@Items TVPParam READONLY,
@OrdNo INTEGER OUTPUT,
@OrdDate DATETIME OUTPUT)
AS
BEGIN
SET @OrdDate = GETDATE(); SET NOCOUNT ON;
INSERT INTO TVPOrd (OrdDate, CustID) VALUES (@OrdDate, @CustID);
SELECT @OrdNo = SCOPE_IDENTITY();
INSERT INTO TVPItem (OrdNo, ProductCode, OrderQty, PackedOn, Label, Price, Photo)
SELECT @OrdNo, ProductCode, OrderQty, PackedOn, Label, Price, Photo
FROM @Items
END;
PROC;
$callTVPOrderEntry = "{call TVPOrderEntry(?, ?, ?, ?)}";
$callTVPOrderEntryNamed = "{call TVPOrderEntry(:id, :tvp, :ordNo, :ordDate)}";
// The following gif files are some random product pictures
// retrieved from the AdventureWorks sample database (their
// sizes ranging from 12 KB to 26 KB)
$gif1 = 'awc_tee_male_large.gif';
$gif2 = 'superlight_black_f_large.gif';
$gif3 = 'silver_chain_large.gif';
$items = [
['0062836700', 367, "2009-03-12", 'AWC Tee Male Shirt', '20.75'],
['1250153272', 256, "2017-11-07", 'Superlight Black Bicycle', '998.45'],
['1328781505', 260, "2010-03-03", 'Silver Chain for Bikes', '88.98'],
];
$selectTVPItemQuery = 'SELECT OrdNo, ItemNo, ProductCode, OrderQty, PackedOn, Label, Price FROM TVPItem ORDER BY ItemNo';
///////////////////////////////////////////////////////
$createTestTVP = <<<TYPE1
CREATE TYPE TestTVP AS TABLE(
C01 VARCHAR(255),
C02 VARCHAR(MAX),
C03 BIT,
C04 SMALLDATETIME,
C05 DATETIME2(5),
C06 UNIQUEIDENTIFIER,
C07 BIGINT,
C08 FLOAT,
C09 NUMERIC(38, 24))
TYPE1;
$createSelectTVP = <<<PROC1
CREATE PROCEDURE SelectTVP (
@TVP TestTVP READONLY)
AS
SELECT * FROM @TVP
PROC1;
$callSelectTVP = "{call SelectTVP(?)}";
///////////////////////////////////////////////////////
$createTestTVP2 = <<<TYPE2
CREATE TYPE TestTVP2 AS TABLE(
C01 NVARCHAR(50),
C02 NVARCHAR(MAX),
C03 INT,
C04 REAL,
C05 VARBINARY(10),
C06 VARBINARY(MAX),
C07 MONEY,
C08 XML,
C09 SQL_VARIANT)
TYPE2;
$createSelectTVP2 = <<<PROC2
CREATE PROCEDURE SelectTVP2 (
@TVP TestTVP2 READONLY)
AS
SELECT * FROM @TVP
PROC2;
$callSelectTVP2 = "{call SelectTVP2(?)}";
///////////////////////////////////////////////////////
// Use schema other than DBO
///////////////////////////////////////////////////////
$createSchema = 'CREATE SCHEMA [Sales DB]';
$dropSchema = 'DROP SCHEMA IF EXISTS [Sales DB]';
$createTestTVP3 = <<<TYPE3
CREATE TYPE [Sales DB].[TestTVP3] AS TABLE(
Review VARCHAR(100) NOT NULL,
SupplierId INT,
SalesDate DATETIME2 NULL
)
TYPE3;
$createSelectTVP3 = <<<PROC3
CREATE PROCEDURE [Sales DB].[SelectTVP3] (
@TVP TestTVP3 READONLY )
AS
SELECT * FROM @TVP
PROC3;
$callSelectTVP3 = "{call [Sales DB].[SelectTVP3](?)}";
$createSupplierType = <<<SUPP_TYPE
CREATE TYPE [Sales DB].[SupplierType] AS TABLE(
SupplierId INT,
SupplierName NVARCHAR(50)
)
SUPP_TYPE;
$createAddReview = <<<SUPP_PROC
CREATE PROCEDURE [Sales DB].[AddReview] (
@suppType SupplierType READONLY,
@reviewType TestTVP3 READONLY,
@image VARBINARY(MAX))
AS
SELECT * FROM @suppType;
SELECT SupplierId, SalesDate, Review FROM @reviewType;
SELECT @image
SUPP_PROC;
$callAddReview = "{call [Sales DB].[AddReview](?, ?, ?)}";
///////////////////////////////////////////////////////
// Common functions
///////////////////////////////////////////////////////
function dropProcSQL($conn, $procName)
{
return "IF OBJECT_ID('$procName', 'P') IS NOT NULL DROP PROCEDURE $procName";
}
function dropTableTypeSQL($conn, $typeName, $schema = 'dbo')
{
return "IF EXISTS (SELECT * FROM sys.types WHERE is_table_type = 1 AND name = '$typeName') DROP TYPE [$schema].[$typeName]";
}
function verifyBinaryData($fp, $data)
{
$size = 8192;
$pos = 0;
$matched = true;
while (!feof($fp)) {
$original = fread($fp, $size);
$str = substr($data, $pos, $size);
if ($original !== $str) {
$matched = false;
break;
}
$pos += $size;
}
return $matched;
}
function verifyBinaryStream($fp, $stream)
{
$size = 8192;
$matched = true;
while (!feof($fp) && !feof($stream)) {
$original = fread($fp, $size);
$data = fread($stream, $size);
if ($original !== $data) {
$matched = false;
break;
}
}
return $matched;
}
?>

View file

@ -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];
}

View file

@ -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--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once('MsSetup.inc');
require_once('MsCommon_mid-refactor.inc');
try {
$conn = new PDO("sqlsrv:server = $server; database=$databaseName;", $uid, $pwd);
$conn->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 = "<XmlTestData><Letters1>The quick brown fox jumps over the lazy dog</Letters1><Digits1>0123456789</Digits1></XmlTestData>";
$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

View file

@ -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--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once('MsSetup.inc');
require_once('MsCommon_mid-refactor.inc');
try {
date_default_timezone_set('America/Los_Angeles');
$conn = new PDO("sqlsrv:server = $server; database=$databaseName;", $uid, $pwd);
$conn->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

View file

@ -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--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once('MsSetup.inc');
require_once('MsCommon_mid-refactor.inc');
try {
$conn = new PDO("sqlsrv:server = $server; database=$databaseName;", $uid, $pwd);
$conn->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

View file

@ -0,0 +1,131 @@
--TEST--
Test Table-valued parameter with a stored procedure that takes two TVPs
--ENV--
PHPT_EXEC=true
--SKIPIF--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once('MsSetup.inc');
require_once('MsCommon_mid-refactor.inc');
function cleanup($conn, $schema)
{
global $dropSchema;
$dropProcedure = dropProcSQL($conn, "[$schema].[AddReview]");
$conn->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

View file

@ -0,0 +1,189 @@
--TEST--
Test various error cases with invalid Table-valued parameter inputs
--ENV--
PHPT_EXEC=true
--SKIPIF--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once('MsSetup.inc');
require_once('MsCommon_mid-refactor.inc');
function invokeProc($conn, $proc, $tvpInput, $caseNo, $inputParam = true)
{
try {
$stmt = $conn->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

View file

@ -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--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once('MsSetup.inc');
require_once('MsCommon_mid-refactor.inc');
try {
date_default_timezone_set('America/Los_Angeles');
$conn = new PDO("sqlsrv:server = $server; database=$databaseName;", $uid, $pwd);
$conn->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

View file

@ -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--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once('MsSetup.inc');
require_once('MsCommon_mid-refactor.inc');
try {
date_default_timezone_set('America/Los_Angeles');
$conn = new PDO("sqlsrv:server = $server; database=$databaseName;", $uid, $pwd);
$conn->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

View file

@ -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;
}
?>

View file

@ -0,0 +1,133 @@
--TEST--
Test Table-valued parameter with a stored procedure that takes two TVPs
--ENV--
PHPT_EXEC=true
--SKIPIF--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once('MsCommon.inc');
date_default_timezone_set('America/Los_Angeles');
function cleanup($conn, $schema)
{
global $dropSchema;
$dropProcedure = dropProcSQL($conn, "[$schema].[AddReview]");
sqlsrv_query($conn, $dropProcedure);
$dropTableType = dropTableTypeSQL($conn, "TestTVP3", $schema);
sqlsrv_query($conn, $dropTableType);
$dropTableType = dropTableTypeSQL($conn, "SupplierType", $schema);
sqlsrv_query($conn, $dropTableType);
sqlsrv_query($conn, $dropSchema);
}
sqlsrv_configure('LogSeverity', SQLSRV_LOG_SEVERITY_ALL);
$conn = connect(array('CharacterSet'=>'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

View file

@ -0,0 +1,187 @@
--TEST--
Test various error cases with invalid Table-valued parameter inputs
--ENV--
PHPT_EXEC=true
--SKIPIF--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once('MsCommon.inc');
function invokeProc($conn, $proc, $tvpInput, $caseNo, $dir = SQLSRV_PARAM_IN)
{
if ($dir == SQLSRV_PARAM_IN) {
$params = array(array($tvpInput, $dir, SQLSRV_PHPTYPE_TABLE, SQLSRV_SQLTYPE_TABLE));
} else {
$params = array(array(&$tvpInput, $dir, SQLSRV_PHPTYPE_TABLE, SQLSRV_SQLTYPE_TABLE));
}
$stmt = sqlsrv_query($conn, $proc, $params);
if (!$stmt) {
$errors = sqlsrv_errors(SQLSRV_ERR_ALL);
if (!empty($errors)) {
$count = count($errors);
}
for ($i = 0; $i < $count; $i++) {
echo "Error $caseNo: ";
echo $errors[$i]['message'] . PHP_EOL;
}
}
}
function cleanup($conn, $schema, $tvpType, $procName)
{
global $dropSchema;
$dropProcedure = dropProcSQL($conn, "[$schema].[$procName]");
sqlsrv_query($conn, $dropProcedure);
$dropTableType = dropTableTypeSQL($conn, $tvpType, $schema);
sqlsrv_query($conn, $dropTableType);
sqlsrv_query($conn, $dropSchema);
}
sqlsrv_configure('LogSeverity', SQLSRV_LOG_SEVERITY_ALL);
$conn = connect(array('CharacterSet'=>'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

View file

@ -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--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once('MsCommon.inc');
date_default_timezone_set('America/Los_Angeles');
sqlsrv_configure('LogSeverity', SQLSRV_LOG_SEVERITY_ALL);
$conn = connect(array('ReturnDatesAsStrings' => 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

View file

@ -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--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once('MsCommon.inc');
date_default_timezone_set('America/Los_Angeles');
sqlsrv_configure('LogSeverity', SQLSRV_LOG_SEVERITY_ALL);
$conn = connect(array('ReturnDatesAsStrings' => 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

View file

@ -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--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once('MsCommon.inc');
date_default_timezone_set('America/Los_Angeles');
sqlsrv_configure('LogSeverity', SQLSRV_LOG_SEVERITY_ALL);
$conn = connect(array('ReturnDatesAsStrings' => 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

View file

@ -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--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once('MsCommon.inc');
date_default_timezone_set('America/Los_Angeles');
sqlsrv_configure('LogSeverity', SQLSRV_LOG_SEVERITY_ALL);
$conn = connect(array('CharacterSet'=>'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 = "<XmlTestData><Letters1>The quick brown fox jumps over the lazy dog</Letters1><Digits1>0123456789</Digits1></XmlTestData>";
$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) "<XmlTestData><Letters1>The quick brown fox jumps over the lazy dog</Letters1><Digits1>0123456789</Digits1></XmlTestData>"
string(4) "4011"
Done

View file

@ -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--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once('MsCommon.inc');
date_default_timezone_set('America/Los_Angeles');
sqlsrv_configure('LogSeverity', SQLSRV_LOG_SEVERITY_ALL);
$conn = connect(array('ReturnDatesAsStrings' => 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