--TEST-- Test connection and statement attributes for formatting decimal and numeric data (feature request issue 415) --DESCRIPTION-- Test the connection and statement options, FormatDecimals and DecimalPlaces, the latter affects money types only, not decimal or numeric types (feature request issue 415). Money, decimal or numeric types are always fetched as strings to preserve accuracy and precision, unlike other primitive numeric types, where there is an option to retrieve them as numbers. Setting FormatDecimals to false will turn off all formatting, regardless of DecimalPlaces value. Also, any negative DecimalPlaces value will be ignored. Likewise, since money or smallmoney fields have scale 4, if DecimalPlaces value is larger than 4, it will be ignored as well. 1. By default, data will be returned with the original precision and scale 2. Set FormatDecimals to true to add the leading zeroes to money and decimal types, if missing. 3. For output params, leading zeroes will be added for any decimal fields if FormatDecimals is true, but only if either SQLSRV_SQLTYPE_DECIMAL or SQLSRV_SQLTYPE_NUMERIC is set correctly to match the original column type and its precision / scale. FormatDecimals and DecimalPlaces will only format the fetched results and have no effect on other operations like insertion or update. --ENV-- PHPT_EXEC=true --SKIPIF-- --FILE-- 1.5); $stmt = sqlsrv_query($conn, $query, array(), $options); if ($stmt) { fatalError("Case 1: expected query to fail!!"); } else { $error = sqlsrv_errors()[0]['message']; if (strpos($error, $message) === false) { print_r(sqlsrv_errors()); } } $options = array('DecimalPlaces' => true); $stmt = sqlsrv_query($conn, $query, array(), $options); if ($stmt) { fatalError("Case 2: expected query to fail!!"); } else { $error = sqlsrv_errors()[0]['message']; if (strpos($error, $message) === false) { print_r(sqlsrv_errors()); } } } function testNoOption($conn, $tableName, $inputs, $columns, $exec) { // This should return decimal values as they are $query = "SELECT * FROM $tableName"; if ($exec) { $stmt = sqlsrv_query($conn, $query); } else { $stmt = sqlsrv_prepare($conn, $query); sqlsrv_execute($stmt); } // Compare values $results = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_NUMERIC); for ($i = 0; $i < count($inputs); $i++) { compareNumbers($results[$i], $inputs[$i], $columns[$i], $i, false); } } function testStmtOption($conn, $tableName, $inputs, $columns, $decimalPlaces, $withBuffer) { // Decimal values should NOT be affected by the statement // option DecimalPlaces $query = "SELECT * FROM $tableName"; if ($withBuffer){ $options = array('Scrollable' => 'buffered', 'DecimalPlaces' => $decimalPlaces); } else { $options = array('DecimalPlaces' => $decimalPlaces); } $size = count($inputs); $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 a string compareNumbers($field, $inputs[$i], $columns[$i], $i, true); } } function getOutputParam($conn, $storedProcName, $inputValue, $prec, $scale, $numeric, $inout) { $outString = ''; $numDigits = 2; $dir = SQLSRV_PARAM_OUT; // The output param value should be the same as the input, // unaffected by the statement attr DecimalPlaces. If // the correct sql type is specified or ColumnEncryption // is enabled, in which case the driver is able to derive // the correct field type, leading zero will be added // if missing $sqlType = null; if (!AE\isColEncrypted()) { $type = ($numeric) ? 'SQLSRV_SQLTYPE_NUMERIC' : 'SQLSRV_SQLTYPE_DECIMAL'; $sqlType = call_user_func($type, $prec, $scale); } // For inout parameters the input type should match the output one if ($inout) { $dir = SQLSRV_PARAM_INOUT; $outString = '0.0'; } $outSql = AE\getCallProcSqlPlaceholders($storedProcName, 1); $stmt = sqlsrv_prepare($conn, $outSql, array(array(&$outString, $dir, null, $sqlType)), array('DecimalPlaces' => $numDigits)); if (!$stmt) { fatalError("getOutputParam: failed when preparing to call $storedProcName"); } if (!sqlsrv_execute($stmt)) { fatalError("getOutputParam: failed to execute procedure $storedProcName"); } // Verify value of output param $column = 'outputParam'; compareNumbers($outString, $inputValue, $column, $scale, true); sqlsrv_free_stmt($stmt); if (!AE\isColEncrypted()) { // With ColumnEncryption enabled, the driver is able to derive the decimal type, // so skip this part of the test $outString2 = $inout ? '0.0' : ''; $stmt = sqlsrv_prepare($conn, $outSql, array(array(&$outString2, $dir)), array('DecimalPlaces' => $numDigits)); if (!$stmt) { fatalError("getOutputParam2: failed when preparing to call $storedProcName"); } if (!sqlsrv_execute($stmt)) { fatalError("getOutputParam2: failed to execute procedure $storedProcName"); } $column = 'outputParam2'; compareNumbers($outString2, $inputValue, $column, $scale, true); sqlsrv_free_stmt($stmt); } } function testOutputParam($conn, $tableName, $inputs, $columns, $dataTypes, $inout = false) { for ($i = 0, $p = 3; $i < count($columns); $i++, $p++) { // Create the stored procedure first $storedProcName = "spFormatDecimals" . $i; $procArgs = "@col $dataTypes[$i] OUTPUT"; $procCode = "SELECT @col = $columns[$i] FROM $tableName"; createProc($conn, $storedProcName, $procArgs, $procCode); // Call stored procedure to retrieve output param getOutputParam($conn, $storedProcName, $inputs[$i], $p, $i, $i > 2, $inout); dropProc($conn, $storedProcName); } } set_time_limit(0); sqlsrv_configure('WarningsReturnAsErrors', 1); $conn = AE\connect(); if (!$conn) { fatalError("Could not connect.\n"); } // Test error conditions testErrorCases($conn); // Create the test table of decimal / numeric data columns $tableName = 'sqlsrvFormatDecimals'; $columns = array('c1', 'c2', 'c3', 'c4', 'c5', 'c6'); $dataTypes = array('decimal(3,0)', 'decimal(4,1)', 'decimal(5,2)', 'numeric(6,3)', 'numeric(7,4)', 'numeric(8, 5)'); $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); // Generate random input values based on precision and scale $values = array(); $max2 = 1; for ($s = 0, $p = 3; $s < count($columns); $s++, $p++) { // First get a random number $n = rand(1, 6); $neg = ($n % 2 == 0) ? -1 : 1; // $n1 is a tiny number, which may or may not be negative $max1 = 5; $n1 = rand(0, $max1) * $neg; if ($s > 0) { $max2 *= 10; $n2 = rand(0, $max2); $number = sprintf("%d.%d", $n1, $n2); } else { $number = sprintf("%d", $n1); } array_push($values, $number); } // Insert data values as strings $inputData = array($colMeta[0]->colName => $values[0], $colMeta[1]->colName => $values[1], $colMeta[2]->colName => $values[2], $colMeta[3]->colName => $values[3], $colMeta[4]->colName => $values[4], $colMeta[5]->colName => $values[5]); $stmt = AE\insertRow($conn, $tableName, $inputData); if (!$stmt) { var_dump($values); fatalError("Failed to insert data.\n"); } sqlsrv_free_stmt($stmt); testNoOption($conn, $tableName, $values, $columns, true); testNoOption($conn, $tableName, $values, $columns, false); sqlsrv_close($conn); // Reconnect with FormatDecimals option set to true $conn = AE\connect(array('FormatDecimals' => true)); if (!$conn) { fatalError("Could not connect.\n"); } // Now try with setting number decimals to 3 then 2 testStmtOption($conn, $tableName, $values, $columns, 3, false); testStmtOption($conn, $tableName, $values, $columns, 3, true); testStmtOption($conn, $tableName, $values, $columns, 2, false); testStmtOption($conn, $tableName, $values, $columns, 2, true); // Test output parameters testOutputParam($conn, $tableName, $values, $columns, $dataTypes); testOutputParam($conn, $tableName, $values, $columns, $dataTypes, true); dropTable($conn, $tableName); sqlsrv_close($conn); echo "Done\n"; ?> --EXPECT-- Done