diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index 35046d487..7410c1a42 100644 --- a/source/pdo_sqlsrv/pdo_dbh.cpp +++ b/source/pdo_sqlsrv/pdo_dbh.cpp @@ -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( attr_value )); break; default: @@ -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; diff --git a/source/pdo_sqlsrv/pdo_init.cpp b/source/pdo_sqlsrv/pdo_init.cpp index e68ada33a..dbb1096d4 100644 --- a/source/pdo_sqlsrv/pdo_init.cpp +++ b/source/pdo_sqlsrv/pdo_init.cpp @@ -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 }, diff --git a/source/pdo_sqlsrv/pdo_stmt.cpp b/source/pdo_sqlsrv/pdo_stmt.cpp index 7009efe2b..93da404be 100644 --- a/source/pdo_sqlsrv/pdo_stmt.cpp +++ b/source/pdo_sqlsrv/pdo_stmt.cpp @@ -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( attr_value )); break; @@ -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); @@ -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, diff --git a/source/shared/core_sqlsrv.h b/source/shared/core_sqlsrv.h index 7542f251b..14d8c3124 100644 --- a/source/shared/core_sqlsrv.h +++ b/source/shared/core_sqlsrv.h @@ -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. diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index 0af1acfc4..662ea5cff 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -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; @@ -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) { @@ -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; } @@ -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; } @@ -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(); @@ -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) { @@ -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; } @@ -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; } @@ -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: @@ -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; diff --git a/source/shared/core_stream.cpp b/source/shared/core_stream.cpp index 0224d3897..d62e6582b 100644 --- a/source/shared/core_stream.cpp +++ b/source/shared/core_stream.cpp @@ -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; diff --git a/source/shared/core_util.cpp b/source/shared/core_util.cpp index d537ea082..973a6be89 100644 --- a/source/shared/core_util.cpp +++ b/source/shared/core_util.cpp @@ -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( sqlsrv_malloc ( 1 ) ); *outString[0] = '\0'; @@ -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; diff --git a/source/shared/localizationimpl.cpp b/source/shared/localizationimpl.cpp index 3bf40c3bf..1543ea4d8 100644 --- a/source/shared/localizationimpl.cpp +++ b/source/shared/localizationimpl.cpp @@ -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, "" }, diff --git a/source/sqlsrv/init.cpp b/source/sqlsrv/init.cpp index 7eae394e4..d05beae8d 100644 --- a/source/sqlsrv/init.cpp +++ b/source/sqlsrv/init.cpp @@ -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 ); @@ -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& ) { diff --git a/source/sqlsrv/stmt.cpp b/source/sqlsrv/stmt.cpp index 6f4d4a56b..bf213f50d 100644 --- a/source/sqlsrv/stmt.cpp +++ b/source/sqlsrv/stmt.cpp @@ -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; diff --git a/test/functional/pdo_sqlsrv/pdo_utf8_varchar_encoding.phpt b/test/functional/pdo_sqlsrv/pdo_utf8_varchar_encoding.phpt new file mode 100644 index 000000000..e5751f60d --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_utf8_varchar_encoding.phpt @@ -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-- + +--FILE-- +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 diff --git a/test/functional/pdo_sqlsrv/pdo_utf8_varchar_extra_coverage.phpt b/test/functional/pdo_sqlsrv/pdo_utf8_varchar_extra_coverage.phpt new file mode 100644 index 000000000..874edc47c --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_utf8_varchar_extra_coverage.phpt @@ -0,0 +1,93 @@ +--TEST-- +Test SQLSRV_ENCODING_UTF8_VARCHAR with bound columns, emulate prepares, and output params +--DESCRIPTION-- +Covers additional code paths for SQLSRV_ENCODING_UTF8_VARCHAR: +- bindColumn with the new encoding +- Emulate prepares (PDO::ATTR_EMULATE_PREPARES) +- Output parameters from stored procedures +--SKIPIF-- + +--FILE-- +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, + CONSTRAINT PK_$tbname PRIMARY KEY (id) + )"); + + $testVal = 'Straße Köln Grüß Gott'; + + // Insert test data using the new encoding + $conn->setAttribute(PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_UTF8_VARCHAR); + $stmt = $conn->prepare("INSERT INTO $tbname (name) VALUES (?)"); + $stmt->execute([$testVal]); + + // === Test 1: bindColumn with SQLSRV_ENCODING_UTF8_VARCHAR === + echo "=== Test 1: bindColumn ===\n"; + $stmt = $conn->prepare("SELECT name FROM $tbname WHERE id = 1"); + $stmt->execute(); + $stmt->bindColumn('name', $nameOut, PDO::PARAM_STR, 0, PDO::SQLSRV_ENCODING_UTF8_VARCHAR); + $stmt->fetch(PDO::FETCH_BOUND); + echo "bindColumn: " . (($nameOut === $testVal) ? 'PASS' : 'FAIL') . "\n"; + + // === Test 2: emulate prepares (ASCII data — emulate prepares with non-ASCII === + // data requires the database default collation to be UTF-8) + echo "=== Test 2: Emulate prepares ===\n"; + $asciiVal = 'Hello World Test'; + $conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); + $conn->setAttribute(PDO::SQLSRV_ATTR_ENCODING, PDO::SQLSRV_ENCODING_UTF8_VARCHAR); + + $stmt = $conn->prepare("INSERT INTO $tbname (name) VALUES (?)"); + $stmt->execute([$asciiVal]); + $id = $conn->lastInsertId(); + + // Read back (turn off emulate for SELECT) + $conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + $stmt = $conn->prepare("SELECT name FROM $tbname WHERE id = ?"); + $stmt->execute([$id]); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + echo "emulate: " . (($row['name'] === $asciiVal) ? 'PASS' : 'FAIL') . "\n"; + + // === Test 3: output parameter === + echo "=== Test 3: Output param ===\n"; + $conn->exec("IF OBJECT_ID('$procName', 'P') IS NOT NULL DROP PROCEDURE $procName"); + $conn->exec("CREATE PROCEDURE $procName @id INT, @out_name VARCHAR(255) OUTPUT + AS + BEGIN + SELECT @out_name = name FROM $tbname WHERE id = @id + END"); + + $outName = str_repeat(' ', 255); + $stmt = $conn->prepare("EXEC $procName ?, ?"); + $stmt->bindValue(1, 1, PDO::PARAM_INT); + $stmt->bindParam(2, $outName, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 255, PDO::SQLSRV_ENCODING_UTF8_VARCHAR); + $stmt->execute(); + + $outName = rtrim($outName); + echo "output: " . (($outName === $testVal) ? 'PASS' : 'FAIL') . "\n"; + + // Cleanup + $conn->exec("DROP PROCEDURE $procName"); + $conn->exec("DROP TABLE $tbname"); + echo "Done\n"; + +} catch (PDOException $e) { + echo "Error: " . $e->getMessage() . "\n"; +} +?> +--EXPECT-- +=== Test 1: bindColumn === +bindColumn: PASS +=== Test 2: Emulate prepares === +emulate: PASS +=== Test 3: Output param === +output: PASS +Done diff --git a/test/functional/sqlsrv/sqlsrv_utf8_varchar_encoding.phpt b/test/functional/sqlsrv/sqlsrv_utf8_varchar_encoding.phpt new file mode 100644 index 000000000..de1bef7f8 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_utf8_varchar_encoding.phpt @@ -0,0 +1,85 @@ +--TEST-- +Test SQLSRV_ENC_UTF8_VARCHAR binds parameters as VARCHAR with UTF-8 data +--DESCRIPTION-- +Verifies that using CharacterSet utf-8-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-- + +--FILE-- + SQLSRV_ENC_UTF8_VARCHAR)); +if ($conn === false) { + die(print_r(sqlsrv_errors(), true)); +} + +$tbname = '#utf8_varchar_test_' . rand(0, 1000); + +// Create table with VARCHAR columns using UTF-8 collation +$sql = "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 +)"; +$stmt = sqlsrv_query($conn, $sql); +if ($stmt === false) { + die(print_r(sqlsrv_errors(), true)); +} + +$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"}', + ], +]; + +echo "=== Insert and roundtrip with SQLSRV_ENC_UTF8_VARCHAR ===\n"; + +foreach ($testCases as $case) { + $stmt = sqlsrv_query($conn, + "INSERT INTO $tbname (name, data) VALUES (?, ?)", + [$case['name'], $case['data']] + ); + if ($stmt === false) { + die(print_r(sqlsrv_errors(), true)); + } + + $stmt = sqlsrv_query($conn, + "SELECT name, data FROM $tbname WHERE name = ?", + [$case['name']] + ); + if ($stmt === false) { + die(print_r(sqlsrv_errors(), true)); + } + + $row = sqlsrv_fetch_array($stmt, SQLSRV_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"; +} + +sqlsrv_query($conn, "DROP TABLE $tbname"); +sqlsrv_close($conn); +echo "Done\n"; +?> +--EXPECT-- +=== Insert and roundtrip with SQLSRV_ENC_UTF8_VARCHAR === +CASE basic_ascii: name=YES, data=YES +CASE european_utf8: name=YES, data=YES +CASE extended_latin: name=YES, data=YES +Done diff --git a/test/functional/sqlsrv/sqlsrv_utf8_varchar_extra_coverage.phpt b/test/functional/sqlsrv/sqlsrv_utf8_varchar_extra_coverage.phpt new file mode 100644 index 000000000..0d1d93ee0 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_utf8_varchar_extra_coverage.phpt @@ -0,0 +1,122 @@ +--TEST-- +Test SQLSRV_ENC_UTF8_VARCHAR with stream params and output params +--DESCRIPTION-- +Covers additional code paths for SQLSRV_ENCODING_UTF8_VARCHAR: +- Stream/resource parameters (process_resource_param and core_stream.cpp) +- Output parameters from stored procedures (process_output_string) +--SKIPIF-- + +--FILE-- + SQLSRV_ENC_UTF8_VARCHAR)); +if ($conn === false) { + die(print_r(sqlsrv_errors(), true)); +} + +$tbname = '#utf8vc_extra_' . rand(0, 1000); +$procName = '#sp_utf8vc_out_' . rand(0, 1000); + +// Create table +$stmt = sqlsrv_query($conn, "CREATE TABLE $tbname ( + id int IDENTITY(1,1) NOT NULL, + name varchar(255) COLLATE Latin1_General_100_CI_AS_SC_UTF8 NULL, + content varchar(max) COLLATE Latin1_General_100_CI_AS_SC_UTF8 NULL +)"); +if ($stmt === false) die(print_r(sqlsrv_errors(), true)); +sqlsrv_free_stmt($stmt); + +$testVal = 'Straße Köln Grüß Gott'; +$streamContent = 'UTF-8 stream data with special chars: Grüße café résumé'; + +// === Test 1: Stream/resource parameter === +echo "=== Test 1: Stream parameter ===\n"; + +// Create a temp file with UTF-8 content +$tmpFile = tempnam(sys_get_temp_dir(), 'utf8'); +file_put_contents($tmpFile, $streamContent); + +$fp = fopen($tmpFile, 'r'); +$stmt = sqlsrv_query($conn, + "INSERT INTO $tbname (name, content) VALUES (?, ?)", + [$testVal, &$fp], + array('SendStreamParamsAtExec' => 1) +); +if ($stmt === false) { + echo "Stream insert failed:\n"; + print_r(sqlsrv_errors()); +} else { + sqlsrv_free_stmt($stmt); + + // Read back and verify + $stmt = sqlsrv_query($conn, "SELECT name, content FROM $tbname WHERE id = 1"); + if (sqlsrv_fetch($stmt)) { + $name = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_UTF8_VARCHAR)); + $content = sqlsrv_get_field($stmt, 1, SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_UTF8_VARCHAR)); + echo "name: " . (($name === $testVal) ? 'PASS' : 'FAIL') . "\n"; + echo "content: " . (($content === $streamContent) ? 'PASS' : 'FAIL') . "\n"; + } + sqlsrv_free_stmt($stmt); +} +fclose($fp); +unlink($tmpFile); + +// === Test 2: Output parameter === +echo "=== Test 2: Output parameter ===\n"; + +$stmt = sqlsrv_query($conn, "CREATE PROCEDURE $procName @id INT, @out_name VARCHAR(255) OUTPUT +AS +BEGIN + SELECT @out_name = name FROM $tbname WHERE id = @id +END"); +if ($stmt === false) die(print_r(sqlsrv_errors(), true)); +sqlsrv_free_stmt($stmt); + +$outName = str_repeat(' ', 255); +$stmt = sqlsrv_prepare($conn, "EXEC $procName ?, ?", + array( + array(1, SQLSRV_PARAM_IN), + array(&$outName, SQLSRV_PARAM_INOUT, null, SQLSRV_SQLTYPE_VARCHAR(255)) + ) +); +if ($stmt === false) die(print_r(sqlsrv_errors(), true)); +sqlsrv_execute($stmt); +// Consume all result sets to finalize output params +while (sqlsrv_next_result($stmt) !== false); +sqlsrv_free_stmt($stmt); + +$outName = rtrim($outName); +echo "output: " . (($outName === $testVal) ? 'PASS' : 'FAIL') . "\n"; + +// === Test 3: Fetch as stream === +echo "=== Test 3: Fetch as stream ===\n"; +$stmt = sqlsrv_query($conn, "SELECT content FROM $tbname WHERE id = 1"); +if (sqlsrv_fetch($stmt)) { + $stream = sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_UTF8_VARCHAR)); + if ($stream !== false) { + $data = stream_get_contents($stream); + fclose($stream); + echo "stream: " . (($data === $streamContent) ? 'PASS' : 'FAIL') . "\n"; + } else { + echo "stream: FAIL (get_field returned false)\n"; + } +} +sqlsrv_free_stmt($stmt); + +// Cleanup +sqlsrv_query($conn, "DROP PROCEDURE $procName"); +sqlsrv_query($conn, "DROP TABLE $tbname"); +sqlsrv_close($conn); +echo "Done\n"; +?> +--EXPECT-- +=== Test 1: Stream parameter === +name: PASS +content: PASS +=== Test 2: Output parameter === +output: PASS +=== Test 3: Fetch as stream === +stream: PASS +Done