Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Sep 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -140,6 +140,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 @@ -1403,6 +1404,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,
david-puglielli marked this conversation as resolved.
Show resolved Hide resolved
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);
david-puglielli marked this conversation as resolved.
Show resolved Hide resolved
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