From 7521f095ee0f72c4502237d53886f32d12db1af8 Mon Sep 17 00:00:00 2001 From: Jenny Tam Date: Mon, 17 Sep 2018 16:24:52 -0700 Subject: [PATCH] Feature request - new PDO_STMT_OPTION_FETCHES_DATETIME_TYPE flag for pdo_sqlsrv to return datetime as objects (#842) * Feature request - issue 648 * Fixed constructor for field_cache and added another test * Added tests for FETCH_BOUND * Added a new test for output param * Modified output param test to set attributes differently * Removed a useless helped function in a test * Combined two new tests into one as per review * Uncommented dropTable --- source/pdo_sqlsrv/pdo_dbh.cpp | 19 +- source/pdo_sqlsrv/pdo_init.cpp | 1 + source/pdo_sqlsrv/pdo_stmt.cpp | 136 ++++++---- source/pdo_sqlsrv/php_pdo_sqlsrv.h | 11 +- source/shared/core_stmt.cpp | 3 +- .../pdo_fetch_datetime_as_output_param.phpt | 87 +++++++ .../pdo_fetch_datetime_time_as_objects.phpt | 238 ++++++++++++++++++ .../pdo_fetch_datetime_time_nulls.phpt | 163 ++++++++++++ 8 files changed, 602 insertions(+), 56 deletions(-) create mode 100644 test/functional/pdo_sqlsrv/pdo_fetch_datetime_as_output_param.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt create mode 100644 test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_nulls.phpt diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index 55801de0..6e761b04 100644 --- a/source/pdo_sqlsrv/pdo_dbh.cpp +++ b/source/pdo_sqlsrv/pdo_dbh.cpp @@ -80,6 +80,7 @@ enum PDO_STMT_OPTIONS { PDO_STMT_OPTION_CLIENT_BUFFER_MAX_KB_SIZE, PDO_STMT_OPTION_EMULATE_PREPARES, PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE, + PDO_STMT_OPTION_FETCHES_DATETIME_TYPE }; // List of all the statement options supported by this driver. @@ -93,6 +94,7 @@ const stmt_option PDO_STMT_OPTS[] = { { NULL, 0, PDO_STMT_OPTION_CLIENT_BUFFER_MAX_KB_SIZE, std::unique_ptr( new stmt_option_buffered_query_limit ) }, { NULL, 0, PDO_STMT_OPTION_EMULATE_PREPARES, std::unique_ptr( new stmt_option_emulate_prepares ) }, { NULL, 0, PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE, std::unique_ptr( new stmt_option_fetch_numeric ) }, + { NULL, 0, PDO_STMT_OPTION_FETCHES_DATETIME_TYPE, std::unique_ptr( new stmt_option_fetch_datetime ) }, { NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr{} }, }; @@ -495,7 +497,8 @@ pdo_sqlsrv_dbh::pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ vo direct_query( false ), query_timeout( QUERY_TIMEOUT_INVALID ), client_buffer_max_size( PDO_SQLSRV_G( client_buffer_max_size )), - fetch_numeric( false ) + fetch_numeric( false ), + fetch_datetime( false ) { if( client_buffer_max_size < 0 ) { client_buffer_max_size = sqlsrv_buffered_result_set::BUFFERED_QUERY_LIMIT_DEFAULT; @@ -1061,6 +1064,10 @@ int pdo_sqlsrv_dbh_set_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout driver_dbh->fetch_numeric = (zend_is_true(val)) ? true : false; break; + case SQLSRV_ATTR_FETCHES_DATETIME_TYPE: + driver_dbh->fetch_datetime = (zend_is_true(val)) ? true : false; + break; + // Not supported case PDO_ATTR_FETCH_TABLE_NAMES: case PDO_ATTR_FETCH_CATALOG_NAMES: @@ -1212,6 +1219,12 @@ int pdo_sqlsrv_dbh_get_attr( _Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout break; } + case SQLSRV_ATTR_FETCHES_DATETIME_TYPE: + { + ZVAL_BOOL( return_value, driver_dbh->fetch_datetime ); + break; + } + default: { THROW_PDO_ERROR( driver_dbh, PDO_SQLSRV_ERROR_INVALID_DBH_ATTR ); @@ -1569,6 +1582,10 @@ void add_stmt_option_key( _Inout_ sqlsrv_context& ctx, _In_ size_t key, _Inout_ option_key = PDO_STMT_OPTION_FETCHES_NUMERIC_TYPE; break; + case SQLSRV_ATTR_FETCHES_DATETIME_TYPE: + option_key = PDO_STMT_OPTION_FETCHES_DATETIME_TYPE; + break; + default: CHECK_CUSTOM_ERROR( true, ctx, PDO_SQLSRV_ERROR_INVALID_STMT_OPTION ) { throw core::CoreException(); diff --git a/source/pdo_sqlsrv/pdo_init.cpp b/source/pdo_sqlsrv/pdo_init.cpp index f98799b8..cd6fe487 100644 --- a/source/pdo_sqlsrv/pdo_init.cpp +++ b/source/pdo_sqlsrv/pdo_init.cpp @@ -285,6 +285,7 @@ namespace { { "SQLSRV_ATTR_CURSOR_SCROLL_TYPE" , SQLSRV_ATTR_CURSOR_SCROLL_TYPE }, { "SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE", SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE }, { "SQLSRV_ATTR_FETCHES_NUMERIC_TYPE", SQLSRV_ATTR_FETCHES_NUMERIC_TYPE }, + { "SQLSRV_ATTR_FETCHES_DATETIME_TYPE", SQLSRV_ATTR_FETCHES_DATETIME_TYPE }, // used for the size for output parameters: PDO::PARAM_INT and PDO::PARAM_BOOL use the default size of int, // PDO::PARAM_STR uses the size of the string in the variable diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index de5c3555..22315bf8 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -51,6 +51,9 @@ inline SQLSMALLINT pdo_fetch_ori_to_odbc_fetch_ori ( _In_ enum pdo_fetch_orienta // for list of supported pdo types. SQLSRV_PHPTYPE pdo_type_to_sqlsrv_php_type( _Inout_ sqlsrv_stmt* driver_stmt, _In_ enum pdo_param_type pdo_type TSRMLS_DC ) { + pdo_sqlsrv_stmt *pdo_stmt = static_cast(driver_stmt); + SQLSRV_ASSERT(pdo_stmt != NULL, "pdo_type_to_sqlsrv_php_type: pdo_stmt object was null"); + switch( pdo_type ) { case PDO_PARAM_BOOL: @@ -64,9 +67,12 @@ SQLSRV_PHPTYPE pdo_type_to_sqlsrv_php_type( _Inout_ sqlsrv_stmt* driver_stmt, _I return SQLSRV_PHPTYPE_NULL; case PDO_PARAM_LOB: - // TODO: This will eventually be changed to SQLSRV_PHPTYPE_STREAM when output streaming is implemented. - return SQLSRV_PHPTYPE_STRING; - + if (pdo_stmt->fetch_datetime) { + return SQLSRV_PHPTYPE_DATETIME; + } else { + // TODO: This will eventually be changed to SQLSRV_PHPTYPE_STREAM when output streaming is implemented. + return SQLSRV_PHPTYPE_STRING; + } case PDO_PARAM_STMT: THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_PDO_STMT_UNSUPPORTED ); break; @@ -213,61 +219,63 @@ void meta_data_free( _Inout_ field_meta_data* meta ) zval convert_to_zval( _In_ SQLSRV_PHPTYPE sqlsrv_php_type, _Inout_ void** in_val, _In_opt_ SQLLEN field_len ) { zval out_zval; - ZVAL_UNDEF( &out_zval ); + ZVAL_UNDEF(&out_zval); - switch( sqlsrv_php_type ) { - - case SQLSRV_PHPTYPE_INT: - case SQLSRV_PHPTYPE_FLOAT: - { - if( *in_val == NULL ) { - ZVAL_NULL( &out_zval ); + switch (sqlsrv_php_type) { + + case SQLSRV_PHPTYPE_INT: + case SQLSRV_PHPTYPE_FLOAT: + { + if (*in_val == NULL) { + ZVAL_NULL(&out_zval); + } + else { + + if (sqlsrv_php_type == SQLSRV_PHPTYPE_INT) { + ZVAL_LONG(&out_zval, **(reinterpret_cast(in_val))); } else { - - if( sqlsrv_php_type == SQLSRV_PHPTYPE_INT ) { - ZVAL_LONG( &out_zval, **( reinterpret_cast( in_val ))); - } - else { - ZVAL_DOUBLE( &out_zval, **( reinterpret_cast( in_val ))); - } + ZVAL_DOUBLE(&out_zval, **(reinterpret_cast(in_val))); } - - if( *in_val ) { - sqlsrv_free( *in_val ); - } - - break; } - case SQLSRV_PHPTYPE_STRING: - case SQLSRV_PHPTYPE_STREAM: // TODO: this will be moved when output streaming is implemented - { - - if( *in_val == NULL ) { - - ZVAL_NULL( &out_zval ); - } - else { - - ZVAL_STRINGL( &out_zval, reinterpret_cast( *in_val ), field_len ); - sqlsrv_free( *in_val ); - } - break; + if (*in_val) { + sqlsrv_free(*in_val); } - - case SQLSRV_PHPTYPE_DATETIME: - DIE( "Unsupported php type" ); - out_zval = *( reinterpret_cast( *in_val )); - break; - case SQLSRV_PHPTYPE_NULL: - ZVAL_NULL( &out_zval ); - break; + break; + } + case SQLSRV_PHPTYPE_STRING: + case SQLSRV_PHPTYPE_STREAM: // TODO: this will be moved when output streaming is implemented + { + if (*in_val == NULL) { - default: - DIE( "Unknown php type" ); - break; + ZVAL_NULL(&out_zval); + } + else { + + ZVAL_STRINGL(&out_zval, reinterpret_cast(*in_val), field_len); + sqlsrv_free(*in_val); + } + break; + } + case SQLSRV_PHPTYPE_DATETIME: + if (*in_val == NULL) { + + ZVAL_NULL(&out_zval); + } + else { + + out_zval = *(reinterpret_cast(*in_val)); + sqlsrv_free(*in_val); + } + break; + case SQLSRV_PHPTYPE_NULL: + ZVAL_NULL(&out_zval); + break; + default: + DIE("Unknown php type"); + break; } return out_zval; @@ -339,6 +347,11 @@ void stmt_option_fetch_numeric:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_opt pdo_stmt->fetch_numeric = ( zend_is_true( value_z )) ? true : false; } +void stmt_option_fetch_datetime:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ) +{ + pdo_sqlsrv_stmt *pdo_stmt = static_cast( stmt ); + pdo_stmt->fetch_datetime = ( zend_is_true( value_z )) ? true : false; +} // log a function entry point #ifndef _WIN32 @@ -865,6 +878,10 @@ int pdo_sqlsrv_stmt_set_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _In driver_stmt->fetch_numeric = ( zend_is_true( val )) ? true : false; break; + case SQLSRV_ATTR_FETCHES_DATETIME_TYPE: + driver_stmt->fetch_datetime = ( zend_is_true( val )) ? true : false; + break; + default: THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_STMT_ATTR ); break; @@ -946,6 +963,12 @@ int pdo_sqlsrv_stmt_get_attr( _Inout_ pdo_stmt_t *stmt, _In_ zend_long attr, _In break; } + case SQLSRV_ATTR_FETCHES_DATETIME_TYPE: + { + ZVAL_BOOL( return_value, driver_stmt->fetch_datetime ); + break; + } + default: THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_STMT_ATTR ); break; @@ -1365,6 +1388,17 @@ sqlsrv_phptype pdo_sqlsrv_stmt::sql_type_to_php_type( _In_ SQLINTEGER sql_type, sqlsrv_phptype.typeinfo.type = SQLSRV_PHPTYPE_STRING; } break; + case SQL_TYPE_DATE: + case SQL_SS_TIMESTAMPOFFSET: + case SQL_SS_TIME2: + case SQL_TYPE_TIMESTAMP: + if ( this->fetch_datetime ) { + sqlsrv_phptype.typeinfo.type = SQLSRV_PHPTYPE_DATETIME; + } + else { + sqlsrv_phptype.typeinfo.type = SQLSRV_PHPTYPE_STRING; + } + break; case SQL_BIGINT: case SQL_CHAR: case SQL_DECIMAL: @@ -1373,10 +1407,6 @@ sqlsrv_phptype pdo_sqlsrv_stmt::sql_type_to_php_type( _In_ SQLINTEGER sql_type, case SQL_WCHAR: case SQL_VARCHAR: case SQL_WVARCHAR: - case SQL_TYPE_DATE: - case SQL_SS_TIMESTAMPOFFSET: - case SQL_SS_TIME2: - case SQL_TYPE_TIMESTAMP: case SQL_LONGVARCHAR: case SQL_WLONGVARCHAR: case SQL_SS_XML: diff --git a/source/pdo_sqlsrv/php_pdo_sqlsrv.h b/source/pdo_sqlsrv/php_pdo_sqlsrv.h index 38e4cec4..160156b0 100644 --- a/source/pdo_sqlsrv/php_pdo_sqlsrv.h +++ b/source/pdo_sqlsrv/php_pdo_sqlsrv.h @@ -48,6 +48,7 @@ enum PDO_SQLSRV_ATTR { SQLSRV_ATTR_CURSOR_SCROLL_TYPE, SQLSRV_ATTR_CLIENT_BUFFER_MAX_KB_SIZE, SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, + SQLSRV_ATTR_FETCHES_DATETIME_TYPE }; // valid set of values for TransactionIsolation connection option @@ -203,6 +204,7 @@ struct pdo_sqlsrv_dbh : public sqlsrv_conn { long query_timeout; zend_long client_buffer_max_size; bool fetch_numeric; + bool fetch_datetime; pdo_sqlsrv_dbh( _In_ SQLHANDLE h, _In_ error_callback e, _In_ void* driver TSRMLS_DC ); }; @@ -241,6 +243,10 @@ struct stmt_option_fetch_numeric : public stmt_option_functor { virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); }; +struct stmt_option_fetch_datetime : public stmt_option_functor { + virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /*opt*/, _In_ zval* value_z TSRMLS_DC ); +}; + extern struct pdo_stmt_methods pdo_sqlsrv_stmt_methods; // a core layer pdo stmt object. This object inherits and overrides the callbacks necessary @@ -253,11 +259,13 @@ struct pdo_sqlsrv_stmt : public sqlsrv_stmt { direct_query_subst_string_len( 0 ), placeholders(NULL), bound_column_param_types( NULL ), - fetch_numeric( false ) + fetch_numeric( false ), + fetch_datetime( false ) { pdo_sqlsrv_dbh* db = static_cast( c ); direct_query = db->direct_query; fetch_numeric = db->fetch_numeric; + fetch_datetime = db->fetch_datetime; } virtual ~pdo_sqlsrv_stmt( void ); @@ -275,6 +283,7 @@ struct pdo_sqlsrv_stmt : public sqlsrv_stmt { std::vector > current_meta_data; pdo_param_type* bound_column_param_types; bool fetch_numeric; + bool fetch_datetime; }; diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index 31f2da45..4675589d 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -36,7 +36,8 @@ struct field_cache { : type( t ) { // if the value is NULL, then just record a NULL pointer - if( field_value != NULL ) { + // field_len may be equal to SQL_NULL_DATA even when field_value is not null + if( field_value != NULL && field_len != SQL_NULL_DATA) { value = sqlsrv_malloc( field_len ); memcpy_s( value, field_len, field_value, field_len ); len = field_len; diff --git a/test/functional/pdo_sqlsrv/pdo_fetch_datetime_as_output_param.phpt b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_as_output_param.phpt new file mode 100644 index 00000000..9cc1bc3b --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_as_output_param.phpt @@ -0,0 +1,87 @@ +--TEST-- +Test attribute PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE and datetimes as output params +--DESCRIPTION-- +Do not support returning DateTime objects as output parameters. Setting attribute PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE to true should have no effect. +--SKIPIF-- + +--FILE-- + false); + $conn = connect("", $attr); + + // Generate input values for the test table + $query = 'SELECT SYSDATETIME(), SYSDATETIMEOFFSET(), CONVERT(time, CURRENT_TIMESTAMP)'; + $stmt = $conn->query($query); + $values = $stmt->fetch(PDO::FETCH_NUM); + + // create a test table with the above input date time values + $tableName = "TestDateTimeOutParam"; + $columns = array('c1', 'c2', 'c3'); + $dataTypes = array("datetime2", "datetimeoffset", "time"); + + $colMeta = array(new ColumnMeta($dataTypes[0], $columns[0]), + new ColumnMeta($dataTypes[1], $columns[1]), + new ColumnMeta($dataTypes[2], $columns[2])); + createTable($conn, $tableName, $colMeta); + + $query = "INSERT INTO $tableName VALUES(?, ?, ?)"; + $stmt = $conn->prepare($query); + for ($i = 0; $i < count($columns); $i++) { + $stmt->bindParam($i+1, $values[$i], PDO::PARAM_LOB); + } + $stmt->execute(); + + $lobException = 'An invalid PHP type was specified as an output parameter. DateTime objects, NULL values, and streams cannot be specified as output parameters.'; + + for ($i = 0; $i < count($columns); $i++) { + // create the stored procedure first + $storedProcName = "spDateTimeOutParam" . $i; + $procArgs = "@col $dataTypes[$i] OUTPUT"; + $procCode = "SELECT @col = $columns[$i] FROM $tableName"; + createProc($conn, $storedProcName, $procArgs, $procCode); + + // call stored procedure to retrieve output param type PDO::PARAM_STR + $dateStr = ''; + $outSql = getCallProcSqlPlaceholders($storedProcName, 1); + $options = array(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE => true); + $stmt = $conn->prepare($outSql, $options); + $stmt->bindParam(1, $dateStr, PDO::PARAM_STR, 1024); + $stmt->execute(); + + if ($dateStr != $values[$i]) { + echo "Expected $values[$i] for column ' . ($i+1) .' but got: "; + var_dump($dateStr); + } + + // for output param type PDO::PARAM_LOB it should fail with the correct exception + try { + $stmt->bindParam(1, $dateStr, PDO::PARAM_LOB, 1024); + $stmt->execute(); + echo "Expected this to fail\n"; + } catch (PDOException $e) { + $message = $e->getMessage(); + $matched = strpos($message, $lobException); + if (!$matched) { + var_dump($e->errorInfo); + } + } + + dropProc($conn, $storedProcName); + } + + dropTable($conn, $tableName); + echo "Done\n"; + + unset($stmt); + unset($conn); +} catch (PDOException $e) { + var_dump($e->errorInfo); +} +?> +--EXPECT-- +Done diff --git a/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt new file mode 100644 index 00000000..ab8ba5ec --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_as_objects.phpt @@ -0,0 +1,238 @@ +--TEST-- +Test attribute PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE for date, time and datetime columns +--DESCRIPTION-- +Test attribute PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE for datetime, datetime2, +smalldatetime, datetimeoffset and time columns. The input values are based on current +timestamp and they are retrieved either as strings or date time objects. Note that the +existing attributes ATTR_STRINGIFY_FETCHES and SQLSRV_ATTR_FETCHES_NUMERIC_TYPE +should have no effect on data retrieval. +--SKIPIF-- + +--FILE-- +format('Y-m-d H:i:s.u'); + + // actual datetime value from date time object to string + $dtActual = date_format($dtObj, 'Y-m-d H:i:s.u'); + if ($dtActual != $dtExpected) { + echo "Expected $dtExpected for column $column but the actual value was $dtActual\n"; + } +} + +function runTest($conn, $query, $columns, $values, $useBuffer = false) +{ + // fetch the date time values as strings or date time objects + // prepare with or without buffered cursor + $options = array(); + if ($useBuffer) { + $options = array(PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL, + PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED); + } + + // fetch_numeric off, fetch_datetime off + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, false); + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, false); + $stmt = $conn->prepare($query, $options); + $stmt->execute(); + $obj = $stmt->fetch(PDO::FETCH_OBJ); + checkStringValues($obj, $columns, $values); + + // fetch_numeric off, fetch_datetime on + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, false); + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + $stmt = $conn->prepare($query, $options); + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + checkDTObjectValues($row, $columns, $values, PDO::FETCH_ASSOC); + + // fetch_numeric on, fetch_datetime on + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, true); + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + $stmt = $conn->prepare($query, $options); + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_BOTH); + checkDTObjectValues($row, $columns, $values, PDO::FETCH_BOTH); + + // ATTR_STRINGIFY_FETCHES should have no effect when fetching date time objects + // Setting it to true only converts numeric values to strings when fetching + // See http://www.php.net/manual/en/pdo.setattribute.php for details + // stringify on, fetch_numeric off, fetch_datetime on + $conn->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true); + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, false); + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + $stmt = $conn->prepare($query, $options); + $i = 0; + do { + $stmt->execute(); + $dtObj = $stmt->fetchColumn($i); + checkColumnDTValue($i, $columns[$i], $values, $dtObj); + } while (++$i < count($columns)); + + // reset stringify to off + // fetch_numeric off, fetch_datetime off + $conn->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false); + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, false); + $stmt = $conn->prepare($query, $options); + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_OBJ); + checkStringValues($obj, $columns, $values); + + // conn attribute fetch_datetime on, but statement attribute fetch_datetime off -- + // expected strings to be returned because statement attribute overrides the + // connection attribute + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + $stmt = $conn->prepare($query, $options); + $stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, false); + $stmt->execute(); + $obj = $stmt->fetch(PDO::FETCH_OBJ); + checkStringValues($obj, $columns, $values); + + // conn attribute fetch_datetime unchanged, but statement attribute fetch_datetime on -- + // expected datetime objects to be returned (this time no need to prepare the statement) + $stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + checkDTObjectValues($row, $columns, $values, PDO::FETCH_ASSOC); + + // likewise, conn attribute fetch_datetime off, but statement attribute + // fetch_datetime on -- expected datetime objects to be returned + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, false); + $stmt = $conn->prepare($query, $options); + $stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_BOTH); + checkDTObjectValues($row, $columns, $values, PDO::FETCH_BOTH); + + // conn attribute fetch_datetime unchanged, but statement attribute fetch_datetime off -- + // expected strings to be returned (again no need to prepare the statement) + $stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, false); + $stmt->execute(); + $obj = $stmt->fetch(PDO::FETCH_LAZY); + checkStringValues($obj, $columns, $values); + + // last test: set statement attribute fetch_datetime on with no change to + // prepared statement -- expected datetime objects to be returned + $stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + $stmt->execute(); + $i = 0; + do { + $stmt->execute(); + $dtObj = $stmt->fetchColumn($i); + checkColumnDTValue($i, $columns[$i], $values, $dtObj); + } while (++$i < count($columns)); + + // keep the same settings but test with FETCH_BOUND + for ($i = 0; $i < count($columns); $i++) { + $dateObj = null; + $stmt->execute(); + $stmt->bindColumn($i + 1, $dateObj, PDO::PARAM_LOB); + $row = $stmt->fetch(PDO::FETCH_BOUND); + checkColumnDTValue($i, $columns[$i], $values, $dateObj); + } + + // redo the test but with fetch_datetime off + // expected strings to be returned + $stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, false); + for ($i = 0; $i < count($columns); $i++) { + $dateStr = null; + $stmt->execute(); + $stmt->bindColumn($i + 1, $dateStr); + $row = $stmt->fetch(PDO::FETCH_BOUND); + if ($dateStr != $values[$i]) { + $col = $columns[$i]; + echo "Expected $values[$i] for column $col but the bound value was: "; + var_dump($dateStr); + } + } +} + +try { + date_default_timezone_set('America/Los_Angeles'); + + $conn = connect(); + + // Generate input values for the test table + $query = 'SELECT CONVERT(date, SYSDATETIME()), SYSDATETIME(), + CONVERT(smalldatetime, SYSDATETIME()), + CONVERT(datetime, SYSDATETIME()), + SYSDATETIMEOFFSET(), + CONVERT(time, SYSDATETIME())'; + + $stmt = $conn->query($query); + $values = $stmt->fetch(PDO::FETCH_NUM); + + // create a test table with the above input date time values + $tableName = "TestDateTimeOffset"; + $columns = array('c1', 'c2', 'c3', 'c4', 'c5', 'c6'); + $dataTypes = array('date', 'datetime2', 'smalldatetime', 'datetime', 'datetimeoffset', 'time'); + + $colMeta = array(new ColumnMeta($dataTypes[0], $columns[0]), + new ColumnMeta($dataTypes[1], $columns[1]), + new ColumnMeta($dataTypes[2], $columns[2]), + new ColumnMeta($dataTypes[3], $columns[3]), + new ColumnMeta($dataTypes[4], $columns[4]), + new ColumnMeta($dataTypes[5], $columns[5])); + createTable($conn, $tableName, $colMeta); + + $query = "INSERT INTO $tableName VALUES(?, ?, ?, ?, ?, ?)"; + $stmt = $conn->prepare($query); + for ($i = 0; $i < count($columns); $i++) { + $stmt->bindParam($i+1, $values[$i], PDO::PARAM_LOB); + } + $stmt->execute(); + + $query = "SELECT * FROM $tableName"; + + runTest($conn, $query, $columns, $values); + runTest($conn, $query, $columns, $values, true); + + dropTable($conn, $tableName); + + echo "Done\n"; + + unset($stmt); + unset($conn); +} catch (PDOException $e) { + var_dump($e->errorInfo); +} +?> +--EXPECT-- +Done diff --git a/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_nulls.phpt b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_nulls.phpt new file mode 100644 index 00000000..9bffdf75 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_fetch_datetime_time_nulls.phpt @@ -0,0 +1,163 @@ +--TEST-- +Test attribute PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE for datetime types with null values +--DESCRIPTION-- +Test attribute PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE for different datetime types with +null values. Whether retrieved as strings or date time objects should return NULLs. +--SKIPIF-- + +--FILE-- + PDO::CURSOR_SCROLL, + PDO::SQLSRV_ATTR_CURSOR_SCROLL_TYPE => PDO::SQLSRV_CURSOR_BUFFERED); + } + + // fetch_numeric off, fetch_datetime off + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, false); + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, false); + $stmt = $conn->prepare($query, $options); + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_NUM); + checkNullStrings($row, $columns); + + // fetch_numeric off, fetch_datetime on + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, false); + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + $stmt = $conn->prepare($query, $options); + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + checkNullDTObjects($row, $columns, PDO::FETCH_ASSOC); + + // fetch_numeric on, fetch_datetime on + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE, true); + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + $stmt = $conn->prepare($query, $options); + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_BOTH); + checkNullDTObjects($row, $columns, PDO::FETCH_BOTH); + + // conn attribute fetch_datetime on, but statement attribute fetch_datetime off -- + // expected strings to be returned because statement attribute overrides the + // connection attribute + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + $stmt = $conn->prepare($query, $options); + $stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, false); + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_NUM); + checkNullStrings($row, $columns); + + // conn attribute fetch_datetime unchanged, but statement attribute fetch_datetime on -- + // expected datetime objects to be returned (this time no need to prepare the statement) + $stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + checkNullDTObjects($row, $columns, PDO::FETCH_ASSOC); + + // likewise, conn attribute fetch_datetime off, but statement attribute + // fetch_datetime on -- expected datetime objects to be returned + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, false); + $stmt = $conn->prepare($query, $options); + $stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_BOTH); + checkNullDTObjects($row, $columns, PDO::FETCH_BOTH); + + // conn attribute fetch_datetime unchanged, but statement attribute fetch_datetime off -- + // expected strings to be returned (again no need to prepare the statement) + $stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, false); + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_NUM); + checkNullStrings($row, $columns); + + // last test: set statement attribute fetch_datetime on with no change to + // prepared statement -- expected datetime objects to be returned + $stmt->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + $stmt->execute(); + $i = 0; + do { + $stmt->execute(); + $dtObj = $stmt->fetchColumn($i); + if (!is_null($dtObj)) { + echo "Expected NULL for column " . ($i + 1) . " but got: "; + var_dump($dtObj); + } + } while (++$i < count($columns)); +} + +try { + $conn = connect(); + + // create a test table + $tableName = "TestNullDateTime"; + $columns = array('c1', 'c2', 'c3', 'c4', 'c5', 'c6'); + $colMeta = array(new ColumnMeta('date', $columns[0]), + new ColumnMeta('datetime', $columns[1]), + new ColumnMeta('smalldatetime', $columns[2]), + new ColumnMeta('datetime2', $columns[3]), + new ColumnMeta('datetimeoffset', $columns[4]), + new ColumnMeta('time', $columns[5])); + createTable($conn, $tableName, $colMeta); + + $value = null; + $query = "INSERT INTO $tableName VALUES(?, ?, ?, ?, ?, ?)"; + $stmt = $conn->prepare($query); + for ($i = 0; $i < count($columns); $i++) { + $stmt->bindParam($i+1, $value, PDO::PARAM_NULL); + } + $stmt->execute(); + + $query = "SELECT * FROM $tableName"; + + runTest($conn, $query, $columns); + runTest($conn, $query, $columns, true); + + dropTable($conn, $tableName); + + echo "Done\n"; + + unset($stmt); + unset($conn); +} catch (PDOException $e) { + var_dump($e->errorInfo); +} +?> +--EXPECT-- +Done