Skip to content

Commit

Permalink
Feature request - add ReturnDatesAsStrings option to statement level …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
yitam authored Sep 17, 2018
1 parent 7521f09 commit 902a032
Show file tree
Hide file tree
Showing 7 changed files with 453 additions and 5 deletions.
7 changes: 7 additions & 0 deletions source/shared/core_sqlsrv.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {

Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions source/shared/core_stmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 ),
Expand Down Expand Up @@ -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.
Expand Down
11 changes: 9 additions & 2 deletions source/sqlsrv/conn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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>{} },
};

Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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 );
Expand Down
9 changes: 6 additions & 3 deletions source/sqlsrv/stmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
}
Expand Down
120 changes: 120 additions & 0 deletions test/functional/sqlsrv/sqlsrv_statement_datetimes_as_nulls.phpt
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 902a032

Please sign in to comment.