Skip to content
Draft
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
2 changes: 2 additions & 0 deletions source/pdo_sqlsrv/pdo_dbh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,7 @@ bool pdo_sqlsrv_dbh_set_attr(_Inout_ pdo_dbh_t *dbh, _In_ zend_long attr, _Inout
break;
case SQLSRV_ENCODING_SYSTEM:
case SQLSRV_ENCODING_UTF8:
case SQLSRV_ENCODING_UTF8_VARCHAR:
driver_dbh->set_encoding( static_cast<SQLSRV_ENCODING>( attr_value ));
break;
default:
Expand Down Expand Up @@ -1781,6 +1782,7 @@ zend_string* pdo_sqlsrv_dbh_quote(_Inout_ pdo_dbh_t* dbh, _In_ const zend_string
}

use_national_char_set = (driver_dbh->use_national_characters == 1 || encoding == SQLSRV_ENCODING_UTF8);
// SQLSRV_ENCODING_UTF8_VARCHAR intentionally does NOT set use_national_char_set
#if PHP_VERSION_ID >= 70200
if ((paramtype & PDO_PARAM_STR_NATL) == PDO_PARAM_STR_NATL) {
use_national_char_set = true;
Expand Down
1 change: 1 addition & 0 deletions source/pdo_sqlsrv/pdo_init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ namespace {
{ "SQLSRV_ENCODING_SYSTEM" , SQLSRV_ENCODING_SYSTEM },
{ "SQLSRV_ENCODING_BINARY" , SQLSRV_ENCODING_BINARY },
{ "SQLSRV_ENCODING_UTF8" , SQLSRV_ENCODING_UTF8 },
{ "SQLSRV_ENCODING_UTF8_VARCHAR" , SQLSRV_ENCODING_UTF8_VARCHAR },

// cursor types (can be assigned to SQLSRV_ATTR_CURSOR_SCROLL_TYPE
{ "SQLSRV_CURSOR_STATIC" , SQL_CURSOR_STATIC },
Expand Down
3 changes: 3 additions & 0 deletions source/pdo_sqlsrv/pdo_stmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ void set_stmt_encoding( _Inout_ sqlsrv_stmt* stmt, _In_ zval* value_z )
case SQLSRV_ENCODING_BINARY:
case SQLSRV_ENCODING_SYSTEM:
case SQLSRV_ENCODING_UTF8:
case SQLSRV_ENCODING_UTF8_VARCHAR:
stmt->set_encoding( static_cast<SQLSRV_ENCODING>( attr_value ));
break;

Expand Down Expand Up @@ -854,6 +855,7 @@ int pdo_sqlsrv_stmt_get_col_data(_Inout_ pdo_stmt_t *stmt, _In_ int colno, _Inou
case SQLSRV_ENCODING_SYSTEM:
case SQLSRV_ENCODING_BINARY:
case SQLSRV_ENCODING_UTF8:
case SQLSRV_ENCODING_UTF8_VARCHAR:
break;
default:
THROW_PDO_ERROR(driver_stmt, PDO_SQLSRV_ERROR_INVALID_DRIVER_COLUMN_ENCODING, colno, NULL);
Expand Down Expand Up @@ -1460,6 +1462,7 @@ int pdo_sqlsrv_stmt_param_hook( _Inout_ pdo_stmt_t *stmt,
case SQLSRV_ENCODING_SYSTEM:
case SQLSRV_ENCODING_BINARY:
case SQLSRV_ENCODING_UTF8:
case SQLSRV_ENCODING_UTF8_VARCHAR:
break;
default:
THROW_PDO_ERROR( driver_stmt, PDO_SQLSRV_ERROR_INVALID_DRIVER_PARAM_ENCODING,
Expand Down
1 change: 1 addition & 0 deletions source/shared/core_sqlsrv.h
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ enum SQLSRV_ENCODING {
SQLSRV_ENCODING_CHAR, // use SQL_C_CHAR when using SQLGetData
SQLSRV_ENCODING_SYSTEM = SQLSRV_ENCODING_CHAR,
SQLSRV_ENCODING_UTF8 = CP_UTF8,
SQLSRV_ENCODING_UTF8_VARCHAR = CP_UTF8 + 1, // UTF-8 data bound as SQL_VARCHAR (not NVARCHAR)
};

// the array keys used when returning a row via sqlsrv_fetch_array and sqlsrv_fetch_object.
Expand Down
22 changes: 18 additions & 4 deletions source/shared/core_stmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1886,7 +1886,7 @@ bool is_valid_sqlsrv_phptype( _In_ sqlsrv_phptype type )
case SQLSRV_PHPTYPE_STREAM:
{
if( type.typeinfo.encoding == SQLSRV_ENCODING_BINARY || type.typeinfo.encoding == SQLSRV_ENCODING_CHAR
|| type.typeinfo.encoding == CP_UTF8 || type.typeinfo.encoding == SQLSRV_ENCODING_DEFAULT ) {
|| type.typeinfo.encoding == CP_UTF8 || type.typeinfo.encoding == SQLSRV_ENCODING_UTF8_VARCHAR || type.typeinfo.encoding == SQLSRV_ENCODING_DEFAULT ) {
return true;
}
break;
Expand Down Expand Up @@ -2253,7 +2253,7 @@ void sqlsrv_param::process_double_param(_Inout_ zval* param_z)

bool sqlsrv_param::derive_string_types_sizes(_In_ zval* /*param_z*/)
{
SQLSRV_ASSERT(encoding == SQLSRV_ENCODING_CHAR || encoding == SQLSRV_ENCODING_UTF8 || encoding == SQLSRV_ENCODING_BINARY, "Invalid encoding in sqlsrv_param::derive_string_types_sizes");
SQLSRV_ASSERT(encoding == SQLSRV_ENCODING_CHAR || encoding == SQLSRV_ENCODING_UTF8 || encoding == SQLSRV_ENCODING_UTF8_VARCHAR || encoding == SQLSRV_ENCODING_BINARY, "Invalid encoding in sqlsrv_param::derive_string_types_sizes");

// Derive the param SQL type only if it is unknown
if (sql_data_type == SQL_UNKNOWN_TYPE) {
Expand All @@ -2267,6 +2267,9 @@ bool sqlsrv_param::derive_string_types_sizes(_In_ zval* /*param_z*/)
case SQLSRV_ENCODING_UTF8:
sql_data_type = SQL_WVARCHAR;
break;
case SQLSRV_ENCODING_UTF8_VARCHAR:
sql_data_type = SQL_VARCHAR;
break;
default:
break;
}
Expand All @@ -2285,6 +2288,9 @@ bool sqlsrv_param::derive_string_types_sizes(_In_ zval* /*param_z*/)
case SQLSRV_ENCODING_UTF8:
c_data_type = is_numeric ? SQL_C_CHAR : SQL_C_WCHAR;
break;
case SQLSRV_ENCODING_UTF8_VARCHAR:
c_data_type = SQL_C_CHAR;
break;
default:
break;
}
Expand Down Expand Up @@ -2330,6 +2336,7 @@ void sqlsrv_param::process_string_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval*

if (!is_numeric && encoding == CP_UTF8) {
// Convert the input param value to wide string and save it for later
// Note: SQLSRV_ENCODING_UTF8_VARCHAR skips this — data stays as UTF-8 bytes
if (Z_STRLEN_P(param_z) > INT_MAX) {
LOG(SEV_ERROR, "Convert input parameter to utf16: buffer length exceeded.");
throw core::CoreException();
Expand All @@ -2353,7 +2360,7 @@ void sqlsrv_param::process_string_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval*

void sqlsrv_param::process_resource_param(_Inout_ zval* param_z)
{
SQLSRV_ASSERT(encoding == SQLSRV_ENCODING_CHAR || encoding == SQLSRV_ENCODING_UTF8 || encoding == SQLSRV_ENCODING_BINARY, "Invalid encoding in sqlsrv_param::get_resource_param_info");
SQLSRV_ASSERT(encoding == SQLSRV_ENCODING_CHAR || encoding == SQLSRV_ENCODING_UTF8 || encoding == SQLSRV_ENCODING_UTF8_VARCHAR || encoding == SQLSRV_ENCODING_BINARY, "Invalid encoding in sqlsrv_param::get_resource_param_info");

// Derive the param SQL type only if it is unknown
if (sql_data_type == SQL_UNKNOWN_TYPE) {
Expand All @@ -2367,6 +2374,9 @@ void sqlsrv_param::process_resource_param(_Inout_ zval* param_z)
case SQLSRV_ENCODING_UTF8:
sql_data_type = SQL_WVARCHAR;
break;
case SQLSRV_ENCODING_UTF8_VARCHAR:
sql_data_type = SQL_VARCHAR;
break;
default:
break;
}
Expand All @@ -2387,6 +2397,9 @@ void sqlsrv_param::process_resource_param(_Inout_ zval* param_z)
case SQLSRV_ENCODING_UTF8:
c_data_type = SQL_C_WCHAR;
break;
case SQLSRV_ENCODING_UTF8_VARCHAR:
c_data_type = SQL_C_CHAR;
break;
default:
break;
}
Expand Down Expand Up @@ -2861,6 +2874,7 @@ void sqlsrv_param_inout::finalize_output_string()
null_size = sizeof(SQLWCHAR); // The string isn't yet converted to UTF-8, still UTF-16
break;
case SQLSRV_ENCODING_SYSTEM:
case SQLSRV_ENCODING_UTF8_VARCHAR:
null_size = sizeof(SQLCHAR);
break;
case SQLSRV_ENCODING_BINARY:
Expand Down Expand Up @@ -2893,7 +2907,7 @@ void sqlsrv_param_inout::finalize_output_string()
core::sqlsrv_zval_stringl(value_z, str, str_len);
}
else {
if (encoding != SQLSRV_ENCODING_CHAR) {
if (encoding != SQLSRV_ENCODING_CHAR && encoding != SQLSRV_ENCODING_UTF8_VARCHAR) {
char* outString = NULL;
SQLLEN outLen = 0;

Expand Down
1 change: 1 addition & 0 deletions source/shared/core_stream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ size_t sqlsrv_stream_read(_Inout_ php_stream* stream, _Out_writes_bytes_(count)

switch( ss->encoding ) {
case SQLSRV_ENCODING_CHAR:
case SQLSRV_ENCODING_UTF8_VARCHAR:
c_type = SQL_C_CHAR;
break;

Expand Down
8 changes: 8 additions & 0 deletions source/shared/core_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ bool convert_string_from_utf16( _In_ SQLSRV_ENCODING encoding, _In_reads_bytes_(
SQLSRV_ASSERT( outString != NULL, "Output buffer pointer must be specified" );
SQLSRV_ASSERT( *outString == NULL, "Output buffer pointer must not be set" );

// SQLSRV_ENCODING_UTF8_VARCHAR uses the same UTF-8 codepage for string conversion
if (encoding == SQLSRV_ENCODING_UTF8_VARCHAR) {
encoding = SQLSRV_ENCODING_UTF8;
}

if (cchInLen == 0 && inString[0] == L'\0') {
*outString = reinterpret_cast<char*>( sqlsrv_malloc ( 1 ) );
*outString[0] = '\0';
Expand Down Expand Up @@ -459,6 +464,9 @@ unsigned int convert_string_from_default_encoding( _In_ unsigned int php_encodin
case SQLSRV_ENCODING_BINARY:
DIE( "Invalid encoding." );
break;
case SQLSRV_ENCODING_UTF8_VARCHAR:
win_encoding = CP_UTF8;
break;
default:
win_encoding = php_encoding;
break;
Expand Down
1 change: 1 addition & 0 deletions source/shared/localizationimpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ struct cp_iconv

const cp_iconv cp_iconv::g_cp_iconv[] = {
{ 65001, "UTF-8" },
{ 65002, "UTF-8" },
{ 1200, "UTF-16LE" },
{ 3, "UTF-8" },
{ 2, "" },
Expand Down
7 changes: 7 additions & 0 deletions source/sqlsrv/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,11 @@ PHP_MINIT_FUNCTION(sqlsrv)

std::string bin = "binary";
std::string chr = "char";
std::string utf8varchar = "utf-8-varchar";

REGISTER_STRING_CONSTANT( "SQLSRV_ENC_BINARY", &bin[0], CONST_PERSISTENT | CONST_CS );
REGISTER_STRING_CONSTANT( "SQLSRV_ENC_CHAR", &chr[0], CONST_PERSISTENT | CONST_CS );
REGISTER_STRING_CONSTANT( "SQLSRV_ENC_UTF8_VARCHAR", &utf8varchar[0], CONST_PERSISTENT | CONST_CS );

REGISTER_LONG_CONSTANT( "SQLSRV_NULLABLE_NO", 0, CONST_PERSISTENT | CONST_CS );
REGISTER_LONG_CONSTANT( "SQLSRV_NULLABLE_YES", 1, CONST_PERSISTENT | CONST_CS );
Expand Down Expand Up @@ -536,6 +538,11 @@ PHP_MINIT_FUNCTION(sqlsrv)
if (NULL == zend_hash_next_index_insert_mem( g_ss_encodings_ht, (void*)&sql_enc_utf8, sizeof( sqlsrv_encoding ))) {
throw ss::SSException();
}

sqlsrv_encoding sql_enc_utf8_varchar( "utf-8-varchar", SQLSRV_ENCODING_UTF8_VARCHAR );
if (NULL == zend_hash_next_index_insert_mem( g_ss_encodings_ht, (void*)&sql_enc_utf8_varchar, sizeof( sqlsrv_encoding ))) {
throw ss::SSException();
}
}
catch( ss::SSException& ) {

Expand Down
2 changes: 1 addition & 1 deletion source/sqlsrv/stmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2057,7 +2057,7 @@ bool is_valid_sqlsrv_phptype( _In_ sqlsrv_phptype type )
case SQLSRV_PHPTYPE_STREAM:
{
if( type.typeinfo.encoding == SQLSRV_ENCODING_BINARY || type.typeinfo.encoding == SQLSRV_ENCODING_CHAR
|| type.typeinfo.encoding == CP_UTF8 || type.typeinfo.encoding == SQLSRV_ENCODING_DEFAULT ) {
|| type.typeinfo.encoding == CP_UTF8 || type.typeinfo.encoding == SQLSRV_ENCODING_UTF8_VARCHAR || type.typeinfo.encoding == SQLSRV_ENCODING_DEFAULT ) {
return true;
}
break;
Expand Down
132 changes: 132 additions & 0 deletions test/functional/pdo_sqlsrv/pdo_utf8_varchar_encoding.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
--TEST--
Test SQLSRV_ENCODING_UTF8_VARCHAR binds parameters as VARCHAR with UTF-8 data
--DESCRIPTION--
Verifies that SQLSRV_ENCODING_UTF8_VARCHAR sends string parameters as SQL_VARCHAR/SQL_C_CHAR
(not SQL_WVARCHAR/SQL_C_WCHAR), allowing efficient use of VARCHAR columns with _UTF8 collations
without implicit NVARCHAR conversion.
--SKIPIF--
<?php require('skipif_mid-refactor.inc'); ?>
--FILE--
<?php
require_once("MsCommon_mid-refactor.inc");

try {
$conn = connect();
$tbname = getTableName('utf8_varchar_test');

// Create table with VARCHAR columns using UTF-8 collation
$conn->exec("IF OBJECT_ID('$tbname', 'U') IS NOT NULL DROP TABLE $tbname");
$conn->exec("CREATE TABLE $tbname (
id int IDENTITY(1,1) NOT NULL,
name varchar(255) COLLATE Latin1_General_100_CI_AS_SC_UTF8 NULL,
data varchar(max) COLLATE Latin1_General_100_BIN2_UTF8 NOT NULL,
CONSTRAINT PK_$tbname PRIMARY KEY (id)
)");

$testCases = [
[
'label' => 'basic_ascii',
'name' => 'Hello World',
'data' => '{"text": "Hello World"}',
],
[
'label' => 'european_utf8',
'name' => 'Straße Köln Grüß Gott',
'data' => '{"text": "Straße Köln Grüß Gott"}',
],
[
'label' => 'extended_latin',
'name' => 'Ñoño café résumé naïve',
'data' => '{"text": "Ñoño café résumé naïve"}',
],
];

// ===== Test 1: Connection-level encoding =====
echo "=== Test 1: Connection-level SQLSRV_ENCODING_UTF8_VARCHAR ===\n";
$conn->setAttribute(PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_UTF8_VARCHAR);

foreach ($testCases as $case) {
$stmt = $conn->prepare("INSERT INTO $tbname (name, data) VALUES (?, ?)");
$stmt->execute([$case['name'], $case['data']]);
$id = $conn->lastInsertId();

$stmt = $conn->prepare("SELECT name, data FROM $tbname WHERE id = ?");
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);

$nameMatch = ($case['name'] === $row['name']) ? 'YES' : 'NO';
$dataMatch = ($case['data'] === $row['data']) ? 'YES' : 'NO';

echo "CASE {$case['label']}: name={$nameMatch}, data={$dataMatch}\n";
}

// ===== Test 2: Statement-level encoding =====
echo "=== Test 2: Statement-level SQLSRV_ENCODING_UTF8_VARCHAR ===\n";
// Reset connection encoding to default UTF-8
$conn->setAttribute(PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_UTF8);
$conn->exec("TRUNCATE TABLE $tbname");

foreach ($testCases as $case) {
$stmt = $conn->prepare("INSERT INTO $tbname (name, data) VALUES (?, ?)",
[PDO::SQLSRV_ATTR_ENCODING => PDO::SQLSRV_ENCODING_UTF8_VARCHAR]);
$stmt->execute([$case['name'], $case['data']]);
$id = $conn->lastInsertId();

$stmt = $conn->prepare("SELECT name, data FROM $tbname WHERE id = ?",
[PDO::SQLSRV_ATTR_ENCODING => PDO::SQLSRV_ENCODING_UTF8_VARCHAR]);
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);

$nameMatch = ($case['name'] === $row['name']) ? 'YES' : 'NO';
$dataMatch = ($case['data'] === $row['data']) ? 'YES' : 'NO';

echo "CASE {$case['label']}: name={$nameMatch}, data={$dataMatch}\n";
}

// ===== Test 3: Per-parameter encoding via bindParam =====
echo "=== Test 3: Per-parameter SQLSRV_ENCODING_UTF8_VARCHAR ===\n";
// Set connection encoding to UTF8_VARCHAR so that reads also use the correct encoding
$conn->setAttribute(PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_UTF8_VARCHAR);
$conn->exec("TRUNCATE TABLE $tbname");

foreach ($testCases as $case) {
$name = $case['name'];
$data = $case['data'];
$stmt = $conn->prepare("INSERT INTO $tbname (name, data) VALUES (:name, :data)");
$stmt->bindParam(':name', $name, PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_UTF8_VARCHAR);
$stmt->bindParam(':data', $data, PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_UTF8_VARCHAR);
$stmt->execute();
$id = $conn->lastInsertId();

$stmt = $conn->prepare("SELECT name, data FROM $tbname WHERE id = ?");
$stmt->execute([$id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);

$nameMatch = ($case['name'] === $row['name']) ? 'YES' : 'NO';
$dataMatch = ($case['data'] === $row['data']) ? 'YES' : 'NO';

echo "CASE {$case['label']}: name={$nameMatch}, data={$dataMatch}\n";
}

// Cleanup
$conn->exec("DROP TABLE $tbname");
echo "Done\n";

} catch (PDOException $e) {
echo "Error: " . $e->getMessage() . "\n";
}
?>
--EXPECT--
=== Test 1: Connection-level SQLSRV_ENCODING_UTF8_VARCHAR ===
CASE basic_ascii: name=YES, data=YES
CASE european_utf8: name=YES, data=YES
CASE extended_latin: name=YES, data=YES
=== Test 2: Statement-level SQLSRV_ENCODING_UTF8_VARCHAR ===
CASE basic_ascii: name=YES, data=YES
CASE european_utf8: name=YES, data=YES
CASE extended_latin: name=YES, data=YES
=== Test 3: Per-parameter SQLSRV_ENCODING_UTF8_VARCHAR ===
CASE basic_ascii: name=YES, data=YES
CASE european_utf8: name=YES, data=YES
CASE extended_latin: name=YES, data=YES
Done
Loading
Loading