Skip to content

Commit

Permalink
Added TVP support to non-procedure statements (#1309)
Browse files Browse the repository at this point in the history
  • Loading branch information
yitam committed Sep 29, 2021
1 parent c87af63 commit 36d2704
Show file tree
Hide file tree
Showing 5 changed files with 474 additions and 6 deletions.
48 changes: 42 additions & 6 deletions source/shared/core_stmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3107,8 +3107,6 @@ void sqlsrv_param_tvp::process_param(_Inout_ sqlsrv_stmt* stmt, _Inout_ zval* pa
int num_columns = parse_tv_param_arrays(stmt, param_z);
column_size = num_rows;

buffer = NULL;
buffer_length = 0;
strlen_or_indptr = (num_columns == 0)? SQL_DEFAULT_PARAM : SQL_DATA_AT_EXEC;
} else {
// This is one of the constituent columns of the table-valued parameter
Expand Down Expand Up @@ -3154,11 +3152,16 @@ int sqlsrv_param_tvp::parse_tv_param_arrays(_Inout_ sqlsrv_stmt* stmt, _Inout_ z
throw core::CoreException();
}

// Save the TVP type name for SQLSetDescField later
buffer = ZSTR_VAL(tvp_name);
buffer_length = SQL_NTS;

// Check if schema is provided by the user
if (zend_hash_move_forward_ex(inputs_ht, &pos) == SUCCESS) {
zval *schema_z = zend_hash_get_current_data_ex(inputs_ht, &pos);
if (schema_z != NULL && Z_TYPE_P(schema_z) == IS_STRING) {
schema_name = Z_STR_P(schema_z);
ZVAL_NEW_STR(&placeholder_z, schema_name);
}
}

Expand Down Expand Up @@ -3308,6 +3311,34 @@ void sqlsrv_param_tvp::bind_param(_Inout_ sqlsrv_stmt* stmt)
return;
}

// Set Table-Valued parameter type name (and the schema where it is defined)
SQLHDESC hIpd = NULL;
core::SQLGetStmtAttr(stmt, SQL_ATTR_IMP_PARAM_DESC, &hIpd, 0, 0);

if (buffer != NULL) {
// SQL_CA_SS_TYPE_NAME is optional for stored procedure calls, but it must be
// specified for SQL statements that are not procedure calls to enable the
// server to determine the type of the table-valued parameter.
char *tvp_name = reinterpret_cast<char *>(buffer);
SQLRETURN r = ::SQLSetDescField(hIpd, param_pos + 1, SQL_CA_SS_TYPE_NAME, reinterpret_cast<SQLCHAR*>(tvp_name), SQL_NTS);
CHECK_SQL_ERROR_OR_WARNING(r, stmt) {
throw core::CoreException();
}
}
if (Z_TYPE(placeholder_z) == IS_STRING) {
// If the table type for the table-valued parameter is defined in a different
// schema than the default, SQL_CA_SS_SCHEMA_NAME must be specified. If not,
// the server will not be able to determine the type of the table-valued parameter.
char * schema_name = Z_STRVAL(placeholder_z);
SQLRETURN r = ::SQLSetDescField(hIpd, param_pos + 1, SQL_CA_SS_SCHEMA_NAME, reinterpret_cast<SQLCHAR*>(schema_name), SQL_NTS);
CHECK_SQL_ERROR_OR_WARNING(r, stmt) {
throw core::CoreException();
}
// Free and reset the placeholder_z
zend_string_release(Z_STR(placeholder_z));
ZVAL_UNDEF(&placeholder_z);
}

// Bind the TVP columns one by one
// Register this object first using SQLSetDescField() for sending TVP data post execution
SQLHDESC desc;
Expand Down Expand Up @@ -3606,12 +3637,17 @@ bool sqlsrv_params_container::get_next_parameter(_Inout_ sqlsrv_stmt* stmt)
// Done now, reset current_param
current_param = NULL;
return false;
} else if (r == SQL_NEED_DATA) {
if (param != NULL) {
current_param = reinterpret_cast<sqlsrv_param*>(param);
SQLSRV_ASSERT(current_param != NULL, "sqlsrv_params_container::get_next_parameter - The parameter requested is missing!");
current_param->init_data_from_zval(stmt);
} else {
// Do not reset current_param when param is NULL, because
// it means that data is expected from the existing current_param
}
}

current_param = reinterpret_cast<sqlsrv_param*>(param);
SQLSRV_ASSERT(current_param != NULL, "sqlsrv_params_container::get_next_parameter - The parameter requested is missing!");
current_param->init_data_from_zval(stmt);

return true;
}

Expand Down
97 changes: 97 additions & 0 deletions test/functional/pdo_sqlsrv/pdo_1307_tvp_non_procedure.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
--TEST--
Verify Github Issue 1307 is fixed.
--DESCRIPTION--
To show that table-valued parameters work with non-procedure statements
--SKIPIF--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once("MsSetup.inc");
require_once("MsCommon_mid-refactor.inc");

function cleanup($conn, $tvpname, $testTable)
{
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);

$dropTableType = dropTableTypeSQL($conn, $tvpname);
$conn->exec($dropTableType);
$conn->exec("DROP TABLE IF EXISTS [$testTable]");

$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}

function readData($conn, $testTable)
{
$tsql = "SELECT id FROM $testTable ORDER BY id";
$stmt = $conn->query($tsql);
$stmt->bindColumn('id', $ID);
while ($row = $stmt->fetch( PDO::FETCH_BOUND ) ){
echo $ID . PHP_EOL;
}
}

try {
$conn = new PDO("sqlsrv:Server=$server;Database=$databaseName;", $uid, $pwd);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$tvpname = 'pdo_id_table';
$testTable = 'pdo_test_table';

cleanup($conn, $tvpname, $testTable);

// Create the table type and test table
$tsql = "CREATE TYPE $tvpname AS TABLE(id INT PRIMARY KEY)";
$conn->exec($tsql);

$tsql = "CREATE TABLE $testTable (id INT PRIMARY KEY)";
$conn->exec($tsql);

// Populate the table using the table type
$tsql = "INSERT INTO $testTable SELECT * FROM ?";
$tvpinput = array($tvpname => [[1], [2], [3]]);

$stmt = $conn->prepare($tsql);
$stmt->bindParam(1, $tvpinput, PDO::PARAM_LOB);
$result = $stmt->execute();

// Verify the results
readData($conn, $testTable);

// Use Merge statement next
$tsql = <<<QRY
MERGE INTO $testTable t
USING ? s ON s.id = t.id
WHEN NOT MATCHED THEN
INSERT (id) VALUES(s.id);
QRY;

unset($tvpinput);
$tvpinput = array($tvpname => [[5], [4], [3], [2]]);

$stmt = $conn->prepare($tsql);
$stmt->bindParam(1, $tvpinput, PDO::PARAM_LOB);
$result = $stmt->execute();

// Verify the results
readData($conn, $testTable);

cleanup($conn, $tvpname, $testTable);

echo "Done\n";

unset($stmt);
unset($conn);
} catch (PDOException $e) {
var_dump($e);
}
?>
--EXPECT--
1
2
3
1
2
3
4
5
Done
105 changes: 105 additions & 0 deletions test/functional/pdo_sqlsrv/pdo_1307_tvp_non_procedure_schema.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
--TEST--
Verify Github Issue 1307 is fixed but TVP and table are defined in a different schema
--DESCRIPTION--
To show that table-valued parameters work with non-procedure statements
--SKIPIF--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once("MsSetup.inc");
require_once("MsCommon_mid-refactor.inc");

function cleanup($conn, $tvpname, $testTable, $schema)
{
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);

$dropTableType = dropTableTypeSQL($conn, $tvpname, $schema);
$conn->exec($dropTableType);
$conn->exec("DROP TABLE IF EXISTS [$schema].[$testTable]");
$conn->exec("DROP SCHEMA IF EXISTS [$schema]");

$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}

function readData($conn, $schema, $testTable)
{
$tsql = "SELECT id FROM [$schema].[$testTable] ORDER BY id";
$stmt = $conn->query($tsql);
$stmt->bindColumn('id', $ID);
while ($row = $stmt->fetch( PDO::FETCH_BOUND ) ){
echo $ID . PHP_EOL;
}
}

try {
$conn = new PDO("sqlsrv:Server=$server;Database=$databaseName;", $uid, $pwd);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$tvpname = 'pdo_id_table2';
$testTable = 'pdo_test_table2';
$schema = 'pdo schema';

cleanup($conn, $tvpname, $testTable, $schema);

// Create the schema
$tsql = "CREATE SCHEMA [$schema]";
$conn->exec($tsql);

// Create the table type and test table
$tsql = "CREATE TYPE [$schema].[$tvpname] AS TABLE(id INT PRIMARY KEY)";
$conn->exec($tsql);

$tsql = "CREATE TABLE [$schema].[$testTable] (id INT PRIMARY KEY)";
$conn->exec($tsql);

// Populate the table using the table type
$tsql = "INSERT INTO [$schema].[$testTable] SELECT * FROM ?";
$tvpinput = array($tvpname => [[5], [3], [1]], $schema);

$stmt = $conn->prepare($tsql);
$stmt->bindParam(1, $tvpinput, PDO::PARAM_LOB);
$result = $stmt->execute();

// Verify the results
readData($conn, $schema, $testTable);

// Use Merge statement next
$tsql = <<<QRY
MERGE INTO [$schema].[$testTable] t
USING ? s ON s.id = t.id
WHEN NOT MATCHED THEN
INSERT (id) VALUES(s.id);
QRY;

unset($tvpinput);
$tvpinput = array($tvpname => [[2], [4], [6], [7]], $schema);

$stmt = $conn->prepare($tsql);
$stmt->bindParam(1, $tvpinput, PDO::PARAM_LOB);
$result = $stmt->execute();

// Verify the results
readData($conn, $schema, $testTable);

cleanup($conn, $tvpname, $testTable, $schema);

echo "Done\n";

unset($stmt);
unset($conn);
} catch (PDOException $e) {
var_dump($e);
}
?>
--EXPECT--
1
3
5
1
2
3
4
5
6
7
Done
Loading

0 comments on commit 36d2704

Please sign in to comment.