Feature request - add ReturnDatesAsStrings option to statement level for sqlsrv (#844)

* Added ReturnDatesAsStrings option to the statement level

* Added new tests for ReturnDatesAsStrings at statement level

* Added more datetime types as per review
This commit is contained in:
Jenny Tam 2018-09-17 16:25:02 -07:00 committed by GitHub
parent 7521f095ee
commit 902a03263e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 453 additions and 5 deletions

View file

@ -1098,6 +1098,7 @@ enum SQLSRV_STMT_OPTIONS {
SQLSRV_STMT_OPTION_SEND_STREAMS_AT_EXEC,
SQLSRV_STMT_OPTION_SCROLLABLE,
SQLSRV_STMT_OPTION_CLIENT_BUFFER_MAX_SIZE,
SQLSRV_STMT_OPTION_DATE_AS_STRING,
// Driver specific connection options
SQLSRV_STMT_OPTION_DRIVER_SPECIFIC = 1000,
@ -1282,6 +1283,11 @@ struct stmt_option_buffered_query_limit : public stmt_option_functor {
virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* opt, _In_ zval* value_z TSRMLS_DC );
};
struct stmt_option_date_as_string : public stmt_option_functor {
virtual void operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* opt, _In_ zval* value_z TSRMLS_DC );
};
// used to hold the table for statment options
struct stmt_option {
@ -1393,6 +1399,7 @@ struct sqlsrv_stmt : public sqlsrv_context {
// last results
unsigned long query_timeout; // maximum allowed statement execution time
zend_long buffered_query_limit; // maximum allowed memory for a buffered query (measured in KB)
bool date_as_string; // false by default but the user can set this to true to retrieve datetime values as strings
// holds output pointers for SQLBindParameter
// We use a deque because it 1) provides the at/[] access in constant time, and 2) grows dynamically without moving

View file

@ -141,6 +141,7 @@ sqlsrv_stmt::sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_ error
last_field_index( -1 ),
past_next_result_end( false ),
query_timeout( QUERY_TIMEOUT_INVALID ),
date_as_string(false),
buffered_query_limit( sqlsrv_buffered_result_set::BUFFERED_QUERY_LIMIT_INVALID ),
param_ind_ptrs( 10 ), // initially hold 10 elements, which should cover 90% of the cases and only take < 100 byte
send_streams_at_exec( true ),
@ -1404,6 +1405,15 @@ void stmt_option_buffered_query_limit:: operator()( _Inout_ sqlsrv_stmt* stmt, s
core_sqlsrv_set_buffered_query_limit( stmt, value_z TSRMLS_CC );
}
void stmt_option_date_as_string:: operator()( _Inout_ sqlsrv_stmt* stmt, stmt_option const* /**/, _In_ zval* value_z TSRMLS_DC )
{
if (zend_is_true(value_z)) {
stmt->date_as_string = true;
}
else {
stmt->date_as_string = false;
}
}
// internal function to release the active stream. Called by each main API function
// that will alter the statement and cancel any retrieval of data from a stream.

View file

@ -173,6 +173,7 @@ namespace SSStmtOptionNames {
const char SEND_STREAMS_AT_EXEC[] = "SendStreamParamsAtExec";
const char SCROLLABLE[] = "Scrollable";
const char CLIENT_BUFFER_MAX_SIZE[] = INI_BUFFERED_QUERY_LIMIT;
const char DATE_AS_STRING[] = "ReturnDatesAsStrings";
}
namespace SSConnOptionNames {
@ -243,6 +244,12 @@ const stmt_option SS_STMT_OPTS[] = {
SQLSRV_STMT_OPTION_CLIENT_BUFFER_MAX_SIZE,
std::unique_ptr<stmt_option_buffered_query_limit>( new stmt_option_buffered_query_limit )
},
{
SSStmtOptionNames::DATE_AS_STRING,
sizeof( SSStmtOptionNames::DATE_AS_STRING ),
SQLSRV_STMT_OPTION_DATE_AS_STRING,
std::unique_ptr<stmt_option_date_as_string>( new stmt_option_date_as_string )
},
{ NULL, 0, SQLSRV_STMT_OPTION_INVALID, std::unique_ptr<stmt_option_functor>{} },
};
@ -988,7 +995,7 @@ PHP_FUNCTION( sqlsrv_prepare )
// Initialize the options array to be passed to the core layer
ALLOC_HASHTABLE( ss_stmt_options_ht );
core::sqlsrv_zend_hash_init( *conn , ss_stmt_options_ht, 3 /* # of buckets */,
core::sqlsrv_zend_hash_init( *conn , ss_stmt_options_ht, 5 /* # of buckets */,
ZVAL_PTR_DTOR, 0 /*persistent*/ TSRMLS_CC );
validate_stmt_options( *conn, options_z, ss_stmt_options_ht TSRMLS_CC );
@ -1111,7 +1118,7 @@ PHP_FUNCTION( sqlsrv_query )
// Initialize the options array to be passed to the core layer
ALLOC_HASHTABLE( ss_stmt_options_ht );
core::sqlsrv_zend_hash_init( *conn , ss_stmt_options_ht, 3 /* # of buckets */, ZVAL_PTR_DTOR,
core::sqlsrv_zend_hash_init( *conn , ss_stmt_options_ht, 5 /* # of buckets */, ZVAL_PTR_DTOR,
0 /*persistent*/ TSRMLS_CC );
validate_stmt_options( *conn, options_z, ss_stmt_options_ht TSRMLS_CC );

View file

@ -129,6 +129,10 @@ ss_sqlsrv_stmt::ss_sqlsrv_stmt( _In_ sqlsrv_conn* c, _In_ SQLHANDLE handle, _In_
fetch_fields_count ( 0 )
{
core_sqlsrv_set_buffered_query_limit( this, SQLSRV_G( buffered_query_limit ) TSRMLS_CC );
// initialize date_as_string based on the corresponding connection option
ss_sqlsrv_conn* ss_conn = static_cast<ss_sqlsrv_conn*>(conn);
date_as_string = ss_conn->date_as_string;
}
ss_sqlsrv_stmt::~ss_sqlsrv_stmt( void )
@ -230,7 +234,7 @@ sqlsrv_phptype ss_sqlsrv_stmt::sql_type_to_php_type( _In_ SQLINTEGER sql_type, _
case SQL_SS_TIMESTAMPOFFSET:
case SQL_SS_TIME2:
case SQL_TYPE_TIMESTAMP:
if( reinterpret_cast<ss_sqlsrv_conn*>( this->conn )->date_as_string ) {
if (this->date_as_string) {
ss_phptype.typeinfo.type = SQLSRV_PHPTYPE_STRING;
ss_phptype.typeinfo.encoding = this->conn->encoding();
}
@ -1678,8 +1682,7 @@ sqlsrv_phptype determine_sqlsrv_php_type( _In_ ss_sqlsrv_stmt const* stmt, _In_
case SQL_SS_TIME2:
case SQL_TYPE_TIMESTAMP:
{
ss_sqlsrv_conn* c = static_cast<ss_sqlsrv_conn*>( stmt->conn );
if( c->date_as_string ) {
if (stmt->date_as_string) {
sqlsrv_phptype.typeinfo.type = SQLSRV_PHPTYPE_STRING;
sqlsrv_phptype.typeinfo.encoding = stmt->encoding();
}

View file

@ -0,0 +1,120 @@
--TEST--
Test retrieving null datetime values with statement option ReturnDatesAsStrings as true
--DESCRIPTION--
Test retrieving null datetime values with statement option ReturnDatesAsStrings as true,
which is false by default. Whether retrieved as strings or date time objects should return
NULLs.
--SKIPIF--
<?php require('skipif_versions_old.inc'); ?>
--FILE--
<?php
require_once('MsCommon.inc');
function testFetch($conn, $query, $columns, $withBuffer = false)
{
// The statement option ReturnDatesAsStrings set to true
// Test different fetching
if ($withBuffer){
$options = array('Scrollable' => 'buffered', 'ReturnDatesAsStrings' => true);
} else {
$options = array('ReturnDatesAsStrings' => true);
}
$size = count($columns);
$stmt = sqlsrv_prepare($conn, $query, array(), $options);
// Fetch by getting one field at a time
sqlsrv_execute($stmt);
if( sqlsrv_fetch( $stmt ) === false) {
fatalError("Failed in retrieving data\n");
}
for ($i = 0; $i < $size; $i++) {
$field = sqlsrv_get_field($stmt, $i); // expect string
if (!is_null($field)) {
echo "Expected null for column $columns[$i] but got: ";
var_dump($field);
}
}
// Fetch row as an object
sqlsrv_execute($stmt);
$object = sqlsrv_fetch_object($stmt);
$objArray = (array)$object; // turn the object into an associated array
for ($i = 0; $i < $size; $i++) {
$col = $columns[$i];
$val = $objArray[$col];
if (!is_null($val)) {
echo "Expected null for column $columns[$i] but got: ";
var_dump($val);
}
}
}
function createTestTable($conn, $tableName, $columns)
{
// Create the test table of date and time columns
$dataTypes = array('date', 'smalldatetime', 'datetime', 'datetime2', 'datetimeoffset', 'time');
$colMeta = array(new AE\ColumnMeta($dataTypes[0], $columns[0]),
new AE\ColumnMeta($dataTypes[1], $columns[1]),
new AE\ColumnMeta($dataTypes[2], $columns[2]),
new AE\ColumnMeta($dataTypes[3], $columns[3]),
new AE\ColumnMeta($dataTypes[4], $columns[4]),
new AE\ColumnMeta($dataTypes[5], $columns[5]));
AE\createTable($conn, $tableName, $colMeta);
// Insert null values
$inputData = array($colMeta[0]->colName => null,
$colMeta[1]->colName => null,
$colMeta[2]->colName => null,
$colMeta[3]->colName => null,
$colMeta[4]->colName => null,
$colMeta[5]->colName => null);
$stmt = AE\insertRow($conn, $tableName, $inputData);
if (!$stmt) {
fatalError("Failed to insert data.\n");
}
sqlsrv_free_stmt($stmt);
}
function runTest($tableName, $columns, $dateAsString)
{
// Connect
$conn = connect(array('ReturnDatesAsStrings' => $dateAsString));
if (!$conn) {
fatalError("Could not connect.\n");
}
$query = "SELECT * FROM $tableName";
testFetch($conn, $query, $columns);
testFetch($conn, $query, $columns, true);
sqlsrv_close($conn);
}
set_time_limit(0);
sqlsrv_configure('WarningsReturnAsErrors', 1);
$tableName = "TestNullDateTime";
$columns = array('c1', 'c2', 'c3', 'c4', 'c5', 'c6');
// Connect
$conn = connect();
if (!$conn) {
fatalError("Could not connect.\n");
}
createTestTable($conn, $tableName, $columns);
runTest($tableName, $columns, true);
runTest($tableName, $columns, false);
dropTable($conn, $tableName);
sqlsrv_close($conn);
echo "Done\n";
?>
--EXPECT--
Done

View file

@ -0,0 +1,197 @@
--TEST--
Test retrieving datetime values with statement option ReturnDatesAsStrings set to true
--DESCRIPTION--
Test retrieving datetime values with statement option ReturnDatesAsStrings set to true,
which is false by default. The statement option should override the corresponding
connection option ReturnDatesAsStrings.
--SKIPIF--
<?php require('skipif_versions_old.inc'); ?>
--FILE--
<?php
require_once('MsCommon.inc');
function compareDateTime($expectedStr, $actualObj)
{
$dtime = date_create($expectedStr);
$dtExpected = $dtime->format('Y-m-d H:i:s.u');
// actual datetime value from date time object to string
$dtActual = date_format($actualObj, 'Y-m-d H:i:s.u');
return ($dtActual === $dtExpected);
}
function testNoOption($conn, $tableName, $inputs, $exec)
{
// Without the statement option, should return datetime values as strings
// because the connection option ReturnDatesAsStrings is set to true
$query = "SELECT * FROM $tableName";
if ($exec) {
$stmt = sqlsrv_query($conn, $query);
} else {
$stmt = sqlsrv_prepare($conn, $query);
sqlsrv_execute($stmt);
}
$results = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC);
// Compare values only
$diffs = array_diff($inputs, $results);
if (!empty($diffs)) {
echo 'The results are different from the input values: ';
print_r($diffs);
}
}
function testStmtOption($conn, $tableName, $inputs, $stmtDateAsStr)
{
// The statement option should always override the connection option
$query = "SELECT * FROM $tableName";
$options = array('ReturnDatesAsStrings' => $stmtDateAsStr);
$stmt = sqlsrv_query($conn, $query, array(), $options);
if ($stmtDateAsStr) {
$results = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
// Compare values only
$diffs = array_diff($inputs, $results);
if (!empty($diffs)) {
echo 'The results are different from the input values: ';
print_r($diffs);
}
} else {
$results = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC);
// Expect DateTime Objects in $results
for ($i = 0; $i < count($inputs); $i++) {
if (is_object($results[$i])) {
$matched = compareDateTime($inputs[$i], $results[$i]);
if (!$matched) {
echo "Expected a DateTime object of $inputs[$i] but got: \n";
var_dump($results[$i]);
}
} else {
echo "Expect a DateTime object but got $results[$i]\n";
}
}
}
}
function testFetching($conn, $tableName, $inputs, $columns, $withBuffer)
{
// The statement option ReturnDatesAsStrings set to true
// Test different fetching
$query = "SELECT * FROM $tableName";
if ($withBuffer){
$options = array('Scrollable' => 'buffered', 'ReturnDatesAsStrings' => true);
} else {
$options = array('ReturnDatesAsStrings' => true);
}
$size = count($inputs);
$stmt = sqlsrv_prepare($conn, $query, array(), $options);
// Fetch by getting one field at a time
sqlsrv_execute($stmt);
if( sqlsrv_fetch( $stmt ) === false) {
fatalError("Failed in retrieving data\n");
}
for ($i = 0; $i < $size; $i++) {
$field = sqlsrv_get_field($stmt, $i); // expect string
if ($field != $inputs[$i]) {
echo "Expected $inputs[$i] for column $columns[$i] but got: ";
var_dump($field);
}
}
// Fetch row as an object
sqlsrv_execute($stmt);
$object = sqlsrv_fetch_object($stmt);
$objArray = (array)$object; // turn the object into an associated array
for ($i = 0; $i < $size; $i++) {
$col = $columns[$i];
$val = $objArray[$col];
if ($val != $inputs[$i]) {
echo "Expected $inputs[$i] for column $columns[$i] but got: ";
var_dump($val);
}
}
}
set_time_limit(0);
sqlsrv_configure('WarningsReturnAsErrors', 1);
date_default_timezone_set('America/Los_Angeles');
// Connect with ReturnDatesAsStrings option set to true
$conn = connect(array('ReturnDatesAsStrings' => true));
if (!$conn) {
fatalError("Could not connect.\n");
}
// Generate input values for the test table
$query = 'SELECT CONVERT(date, SYSDATETIME()), SYSDATETIME(),
CONVERT(smalldatetime, SYSDATETIME()),
CONVERT(datetime, SYSDATETIME()),
SYSDATETIMEOFFSET(),
CONVERT(time, SYSDATETIME())';
$stmt = sqlsrv_query($conn, $query);
$values = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC);
// Create the test table of date and time columns
$tableName = 'StmtDateAsString';
$columns = array('c1', 'c2', 'c3', 'c4', 'c5', 'c6');
$dataTypes = array('date', 'datetime2', 'smalldatetime', 'datetime', 'datetimeoffset', 'time');
$colMeta = array(new AE\ColumnMeta($dataTypes[0], $columns[0]),
new AE\ColumnMeta($dataTypes[1], $columns[1]),
new AE\ColumnMeta($dataTypes[2], $columns[2]),
new AE\ColumnMeta($dataTypes[3], $columns[3]),
new AE\ColumnMeta($dataTypes[4], $columns[4]),
new AE\ColumnMeta($dataTypes[5], $columns[5]));
AE\createTable($conn, $tableName, $colMeta);
// Insert data values
$inputData = array($colMeta[0]->colName => $values[0],
$colMeta[1]->colName => $values[1],
$colMeta[2]->colName => $values[2],
$colMeta[3]->colName => $values[3],
$colMeta[4]->colName => $values[4],
$colMeta[5]->colName => $values[5]);
$stmt = AE\insertRow($conn, $tableName, $inputData);
if (!$stmt) {
fatalError("Failed to insert data.\n");
}
sqlsrv_free_stmt($stmt);
// Do not set ReturnDatesAsStrings at statement level
testNoOption($conn, $tableName, $values, true);
testNoOption($conn, $tableName, $values, false);
// Set ReturnDatesAsStrings to false at statement level
testStmtOption($conn, $tableName, $values, false);
sqlsrv_close($conn);
// Now connect but with ReturnDatesAsStrings option set to false
$conn = connect(array('ReturnDatesAsStrings' => false));
if (!$conn) {
fatalError("Could not connect.\n");
}
// Set ReturnDatesAsStrings to true at statement level
testStmtOption($conn, $tableName, $values, true);
// Test fetching by setting ReturnDatesAsStrings to true at statement level
testFetching($conn, $tableName, $values, $columns, true);
testFetching($conn, $tableName, $values, $columns, false);
dropTable($conn, $tableName);
sqlsrv_close($conn);
echo "Done\n";
?>
--EXPECT--
Done

View file

@ -0,0 +1,104 @@
--TEST--
Test retrieving datetime values as output params with statement option ReturnDatesAsStrings
--DESCRIPTION--
Test retrieving datetime values as output params with statement option ReturnDatesAsStrings
with sqlsrv_prepare. When ReturnDatesAsStrings option is false, expect an error to return.
--SKIPIF--
<?php require('skipif_versions_old.inc'); ?>
--FILE--
<?php
require_once('MsCommon.inc');
function runTest($conn, $storedProcName, $inputValue, $sqlType, $dateAsString)
{
$outDateStr = '';
$outSql = AE\getCallProcSqlPlaceholders($storedProcName, 1);
$stmt = sqlsrv_prepare($conn, $outSql,
array(array(&$outDateStr, SQLSRV_PARAM_OUT, null, $sqlType)), array('ReturnDatesAsStrings' => $dateAsString));
if (!$stmt) {
fatalError("Failed when preparing to call $storedProcName");
}
$result = sqlsrv_execute($stmt);
if ($dateAsString) {
// Expect to succeed when returning a DateTime value as a string
// The output param value should be the same as the input value
if (!$result) {
fatalError("Failed when invoking $storedProcName");
}
if ($outDateStr != $inputValue) {
echo "Expected $inputValue but got $outDateStr\n";
}
} else {
// Expect to fail with an error message because setting a DateTime object as the
// output parameter is not allowed
if ($result) {
fatalError("Returning DateTime as output param is expected to fail!");
}
// Check if the error message is the expected one
$error = sqlsrv_errors()[0]['message'];
$message = 'An invalid PHP type was specified as an output parameter. DateTime objects, NULL values, and streams cannot be specified as output parameters';
if (strpos($error, $message) === false) {
print_r(sqlsrv_errors());
}
}
}
set_time_limit(0);
sqlsrv_configure('WarningsReturnAsErrors', 1);
date_default_timezone_set('America/Los_Angeles');
// Connect with ReturnDatesAsStrings option set to true
$conn = connect(array('ReturnDatesAsStrings' => true));
if (!$conn) {
fatalError("Could not connect.\n");
}
// Generate input values for the test table
$query = 'SELECT CONVERT(date, SYSDATETIME()), SYSDATETIME(), SYSDATETIMEOFFSET(), CONVERT(time, CURRENT_TIMESTAMP)';
$stmt = sqlsrv_query($conn, $query);
$values = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC);
// Create the test table of date and time columns
$tableName = 'OuputParamDateAsString';
$columns = array('c1', 'c2', 'c3', 'c4');
$dataTypes = array('date', 'datetime2', 'datetimeoffset', 'time');
$sqlTypes = array(SQLSRV_SQLTYPE_DATE,
SQLSRV_SQLTYPE_DATETIME2,
SQLSRV_SQLTYPE_DATETIMEOFFSET,
SQLSRV_SQLTYPE_TIME);
$colMeta = array(new AE\ColumnMeta($dataTypes[0], $columns[0]),
new AE\ColumnMeta($dataTypes[1], $columns[1]),
new AE\ColumnMeta($dataTypes[2], $columns[2]),
new AE\ColumnMeta($dataTypes[3], $columns[3]));
AE\createTable($conn, $tableName, $colMeta);
// Insert data values
$inputData = array($colMeta[0]->colName => $values[0],
$colMeta[1]->colName => $values[1],
$colMeta[2]->colName => $values[2],
$colMeta[3]->colName => $values[3]);
$stmt = AE\insertRow($conn, $tableName, $inputData);
if (!$stmt) {
fatalError("Failed to insert data.\n");
}
sqlsrv_free_stmt($stmt);
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
runTest($conn, $storedProcName, $values[$i], $sqlTypes[$i], true);
runTest($conn, $storedProcName, $values[$i], $sqlTypes[$i], false);
}
dropTable($conn, $tableName);
sqlsrv_close($conn);
echo "Done\n";
?>
--EXPECT--
Done