diff --git a/source/shared/core_stmt.cpp b/source/shared/core_stmt.cpp index 649e1c3d..f88f6fc7 100644 --- a/source/shared/core_stmt.cpp +++ b/source/shared/core_stmt.cpp @@ -748,10 +748,14 @@ void core_sqlsrv_bind_param( _Inout_ sqlsrv_stmt* stmt, _In_ SQLUSMALLINT param_ core::SQLBindParameter( stmt, param_num + 1, direction, c_type, sql_type, column_size, decimal_digits, buffer, buffer_len, &ind_ptr ); - if ( stmt->conn->ce_option.enabled && sql_type == SQL_TYPE_TIMESTAMP ) - { - if( decimal_digits == 3 ) - core::SQLSetDescField( stmt, param_num + 1, SQL_CA_SS_SERVER_TYPE, (SQLPOINTER)SQL_SS_TYPE_DATETIME, SQL_IS_INTEGER ); + + // When calling SQLDescribeParam() on a parameter targeting a Datetime column, the return values for ParameterType, ColumnSize and DecimalDigits are SQL_TYPE_TIMESTAMP, 23, and 3 respectively. + // For a parameter targeting a SmallDatetime column, the return values are SQL_TYPE_TIMESTAMP, 16, and 0. Inputting these values into SQLBindParameter() results in Operand type clash error. + // This is because SQL_TYPE_TIMESTAMP corresponds to Datetime2 by default, and conversion of Datetime2 to Datetime and conversion of Datetime2 to SmallDatatime is not allowed with encrypted columns. + // To fix the conversion problem, set the SQL_CA_SS_SERVER_TYPE field of the parameter to SQL_SS_TYPE_DATETIME and SQL_SS_TYPE_SMALLDATETIME respectively for a Datetime and Smalldatetime column. + if (stmt->conn->ce_option.enabled && sql_type == SQL_TYPE_TIMESTAMP) { + if (decimal_digits == 3) + core::SQLSetDescField(stmt, param_num + 1, SQL_CA_SS_SERVER_TYPE, (SQLPOINTER)SQL_SS_TYPE_DATETIME, SQL_IS_INTEGER); else if (decimal_digits == 0) core::SQLSetDescField( stmt, param_num + 1, SQL_CA_SS_SERVER_TYPE, (SQLPOINTER)SQL_SS_TYPE_SMALLDATETIME, SQL_IS_INTEGER ); } diff --git a/test/functional/pdo_sqlsrv/pdo_ae_insert_datetime_encrypted.phpt b/test/functional/pdo_sqlsrv/pdo_ae_insert_datetime_encrypted.phpt new file mode 100644 index 00000000..5d84a1d6 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_ae_insert_datetime_encrypted.phpt @@ -0,0 +1,139 @@ +--TEST-- +Test for inserting and retrieving encrypted data of datetime and smalldatetime types encrypted +--DESCRIPTION-- +Verify that inserting into smalldatetime column might trigger "Datetime field overflow" error +--SKIPIF-- + +--FILE-- + $type) { + $colDef = getColDef($name, $type) . ', '; + $tsql .= $colDef; + } + + $tsql = rtrim($tsql, ', ') . ')'; + return $tsql; + +} + +function createTablePlainQuery($conn, $tableName, $columns) +{ + $tsql = "CREATE TABLE $tableName ("; + foreach ($columns as $name => $type) { + $colDef = '[' . $name . '] ' . $type . ', '; + $tsql .= $colDef; + } + + $tsql = rtrim($tsql, ', ') . ')'; + return $tsql; +} + +try { + // This test requires to connect with the Always Encrypted feature + // First check if the system is qualified to run this test + $dsn = getDSN($server, null); + $conn = new PDO($dsn, $uid, $pwd); + $qualified = isAEQualified($conn) && (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'); + + if ($qualified) { + unset($conn); + + // Now connect with ColumnEncryption enabled + $connectionInfo = "ColumnEncryption = Enabled;"; + $conn = new PDO("sqlsrv:server = $server; database=$databaseName; $connectionInfo", $uid, $pwd); + } + + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $tableName = 'pdo_datetime_encrypted'; + dropTable($conn, $tableName); + + // Define the column definitions + $columns = array('c1' => 'smalldatetime', 'c2' => 'datetime', 'c3' => 'datetime2(4)'); + + if ($qualified) { + $tsql = createTableEncryptedQuery($conn, $tableName, $columns); + } else { + $tsql = createTablePlainQuery($conn, $tableName, $columns); + } + $conn->exec($tsql); + + // Insert values that cause errors + $val1 = '9999-12-31 23:59:59'; + $val2 = null; + $val3 = '9999-12-31 23:59:59.9999'; + + $tsql = "INSERT INTO $tableName (c1, c2, c3) VALUES (?,?,?)"; + $stmt = $conn->prepare($tsql); + + $stmt->bindParam(1, $val1); + $stmt->bindParam(2, $val2); + $stmt->bindParam(3, $val3); + + try { + $stmt->execute(); + } catch (PDOException $e) { + $error = ($qualified)? '*Datetime field overflow' : '*The conversion of a nvarchar data type to a smalldatetime data type resulted in an out-of-range value.'; + if (!fnmatch($error, $e->getMessage())) { + echo "The error message is unexpected:\n"; + var_dump($e->getMessage()); + } + } + + // These values should work + $val1 = '2021-11-03 11:49:00'; + $val2 = '2015-10-23 07:03:00.000'; + + try { + $stmt->execute(); + } catch (PDOException $e) { + echo "Errors unexpected!!\n"; + var_dump($e->getMessage()); + } + + unset($stmt); + + // Now fetch the values + $tsql = "SELECT * FROM $tableName"; + + $stmt = $conn->prepare($tsql); + $stmt->execute(); + + $row = $stmt->fetch(PDO::FETCH_NUM); + var_dump($row); + + dropTable($conn, $tableName); + + unset($stmt); + unset($conn); +} catch (PDOException $e) { + echo $e->getMessage(); +} + +echo "Done\n"; + +?> +--EXPECT-- +array(3) { + [0]=> + string(19) "2021-11-03 11:49:00" + [1]=> + string(23) "2015-10-23 07:03:00.000" + [2]=> + string(24) "9999-12-31 23:59:59.9999" +} +Done \ No newline at end of file diff --git a/test/functional/sqlsrv/sqlsrv_ae_insert_datetime_encrypted.phpt b/test/functional/sqlsrv/sqlsrv_ae_insert_datetime_encrypted.phpt new file mode 100644 index 00000000..8527d861 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_ae_insert_datetime_encrypted.phpt @@ -0,0 +1,149 @@ +--TEST-- +Test for inserting and retrieving encrypted data of datetime and smalldatetime types encrypted +--DESCRIPTION-- +Verify that inserting into smalldatetime column might trigger "Datetime field overflow" error +--SKIPIF-- + +--FILE-- + $type) { + $colDef = getColDef($name, $type) . ', '; + $tsql .= $colDef; + } + + $tsql = rtrim($tsql, ', ') . ')'; + return $tsql; + + } + + function createTablePlainQuery($conn, $tableName, $columns) + { + $tsql = "CREATE TABLE $tableName ("; + foreach ($columns as $name => $type) { + $colDef = '[' . $name . '] ' . $type . ', '; + $tsql .= $colDef; + } + + $tsql = rtrim($tsql, ', ') . ')'; + return $tsql; + } + + require_once("MsCommon.inc"); + + // This test requires to connect with the Always Encrypted feature + // First check if the system is qualified to run this test + $options = array('Database' => $database, 'UID' => $userName, 'PWD' => $userPassword, 'ReturnDatesAsStrings' => true); + $conn = sqlsrv_connect($server, $options); + if ($conn === false) { + fatalError("Failed to connect to $server."); + } + + $qualified = AE\isQualified($conn) && (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'); + if ($qualified) { + sqlsrv_close($conn); + + // Now connect with ColumnEncryption enabled + $connectionOptions = array_merge($options, array('ColumnEncryption' => 'Enabled')); + $conn = sqlsrv_connect($server, $connectionOptions); + if ($conn === false) { + fatalError("Failed to connect to $server."); + } + } + + $tableName = 'srv_datetime_encrypted'; + dropTable($conn, $tableName); + + // Define the column definitions + $columns = array('c1' => 'smalldatetime', 'c2' => 'datetime', 'c3' => 'datetime2(4)'); + + if ($qualified) { + $tsql = createTableEncryptedQuery($conn, $tableName, $columns); + } else { + $tsql = createTablePlainQuery($conn, $tableName, $columns); + } + + $stmt = sqlsrv_query($conn, $tsql); + if (!$stmt) { + fatalError("Failed to create table $tableName\n"); + } + + // Insert values that cause errors + $val1 = '9999-12-31 23:59:59'; + $val2 = null; + $val3 = '9999-12-31 23:59:59.9999'; + + $tsql = "INSERT INTO $tableName (c1, c2, c3) VALUES (?,?,?)"; + $params = array($val1, $val2, $val3); + + $stmt = sqlsrv_prepare($conn, $tsql, $params); + if (!$stmt) { + fatalError("Failed to prepare insert statement"); + } + $result = sqlsrv_execute($stmt); + if ($result) { + echo "Inserting invalid values should have failed!\n"; + } else { + $error = ($qualified)? '*Datetime field overflow' : '*The conversion of a varchar data type to a smalldatetime data type resulted in an out-of-range value.'; + if (!fnmatch($error, sqlsrv_errors()[0]['message'])) { + var_dump(sqlsrv_errors()); + } + } + + sqlsrv_free_stmt($stmt); + + // These values should work + $val1 = '2021-11-03 11:49:00'; + $val2 = '2015-10-23 07:03:00.000'; + + $params = array($val1, $val2, $val3); + $stmt = sqlsrv_prepare($conn, $tsql, $params); + if (!$stmt) { + fatalError("Failed to prepare insert statement"); + } + $result = sqlsrv_execute($stmt); + if (!$result) { + fatalError("Failed to insert valid values\n"); + } + + sqlsrv_free_stmt($stmt); + + // Now fetch the values + $tsql = "SELECT * FROM $tableName"; + + $stmt = sqlsrv_query($conn, $tsql); + if (!$stmt) { + fatalError("Failed to select from $tableName"); + } + + $row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC); + var_dump($row); + + dropTable($conn, $tableName); + + sqlsrv_free_stmt($stmt); + sqlsrv_close($conn); + + echo "Done\n"; + +?> +--EXPECT-- +array(3) { + ["c1"]=> + string(19) "2021-11-03 11:49:00" + ["c2"]=> + string(23) "2015-10-23 07:03:00.000" + ["c3"]=> + string(24) "9999-12-31 23:59:59.9999" +} +Done