|
| 1 | +--TEST-- |
| 2 | +GitHub issue 1443 - stream remains valid after statement goes out of scope |
| 3 | +--DESCRIPTION-- |
| 4 | +When a stream is returned from a function where the statement variable goes out |
| 5 | +of scope, the stream should remain valid as long as it has references. Previously |
| 6 | +the statement destructor would close the stream, making it invalid. |
| 7 | +Also validates that both the stream and statement are properly cleaned up after use, |
| 8 | +with no resource leaks detected via memory_get_usage over repeated iterations. |
| 9 | +--SKIPIF-- |
| 10 | +<?php require('skipif.inc'); ?> |
| 11 | +--FILE-- |
| 12 | +<?php |
| 13 | +require_once("MsCommon.inc"); |
| 14 | + |
| 15 | +// Test 1: Stream returned from a function where $stmt goes out of scope |
| 16 | +function getStream($conn) |
| 17 | +{ |
| 18 | + $stmt = sqlsrv_query($conn, "SELECT CONVERT(VARBINARY(32), 0x48656C6C6F)"); |
| 19 | + if ($stmt === false) { |
| 20 | + fatalError("Query failed in getStream."); |
| 21 | + } |
| 22 | + sqlsrv_fetch($stmt); |
| 23 | + // $stmt will go out of scope when this function returns |
| 24 | + return sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY)); |
| 25 | +} |
| 26 | + |
| 27 | +$conn = connect(); |
| 28 | +if ($conn === false) { |
| 29 | + fatalError("Could not connect."); |
| 30 | +} |
| 31 | + |
| 32 | +$stream = getStream($conn); |
| 33 | +if ($stream === false) { |
| 34 | + fatalError("Failed to get stream."); |
| 35 | +} |
| 36 | + |
| 37 | +// The stream should still be valid even though $stmt went out of scope |
| 38 | +$data = fread($stream, 100); |
| 39 | +if ($data === false) { |
| 40 | + fatalError("fread failed on stream."); |
| 41 | +} |
| 42 | + |
| 43 | +// CONVERT(VARBINARY(32), 0x48656C6C6F) should return the bytes "Hello" |
| 44 | +if ($data === "Hello") { |
| 45 | + echo "Test 1 passed: stream data read correctly after stmt out of scope.\n"; |
| 46 | +} else { |
| 47 | + echo "Test 1 FAILED: expected 'Hello', got '" . bin2hex($data) . "'.\n"; |
| 48 | +} |
| 49 | + |
| 50 | +fclose($stream); |
| 51 | + |
| 52 | +// Test 2: Stream from sqlsrv_prepare + sqlsrv_execute |
| 53 | +function getStreamPrepared($conn) |
| 54 | +{ |
| 55 | + $stmt = sqlsrv_prepare($conn, "SELECT CONVERT(VARBINARY(32), 0x576F726C64)"); |
| 56 | + if ($stmt === false) { |
| 57 | + fatalError("Prepare failed in getStreamPrepared."); |
| 58 | + } |
| 59 | + sqlsrv_execute($stmt); |
| 60 | + sqlsrv_fetch($stmt); |
| 61 | + return sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY)); |
| 62 | +} |
| 63 | + |
| 64 | +$stream2 = getStreamPrepared($conn); |
| 65 | +if ($stream2 === false) { |
| 66 | + fatalError("Failed to get stream from prepared stmt."); |
| 67 | +} |
| 68 | + |
| 69 | +$data2 = fread($stream2, 100); |
| 70 | +if ($data2 === "World") { |
| 71 | + echo "Test 2 passed: stream from prepared stmt works after stmt out of scope.\n"; |
| 72 | +} else { |
| 73 | + echo "Test 2 FAILED: expected 'World', got '" . bin2hex($data2) . "'.\n"; |
| 74 | +} |
| 75 | + |
| 76 | +fclose($stream2); |
| 77 | + |
| 78 | +// Test 3: Multiple reads from the stream |
| 79 | +function getStreamLargeData($conn) |
| 80 | +{ |
| 81 | + $stmt = sqlsrv_query($conn, "SELECT CONVERT(VARBINARY(MAX), REPLICATE(CONVERT(VARBINARY(MAX), 0x41), 1000))"); |
| 82 | + if ($stmt === false) { |
| 83 | + fatalError("Query failed in getStreamLargeData."); |
| 84 | + } |
| 85 | + sqlsrv_fetch($stmt); |
| 86 | + return sqlsrv_get_field($stmt, 0, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY)); |
| 87 | +} |
| 88 | + |
| 89 | +$stream3 = getStreamLargeData($conn); |
| 90 | +if ($stream3 === false) { |
| 91 | + fatalError("Failed to get stream for large data."); |
| 92 | +} |
| 93 | + |
| 94 | +$allData = ''; |
| 95 | +while (!feof($stream3)) { |
| 96 | + $chunk = fread($stream3, 100); |
| 97 | + if ($chunk === false) { |
| 98 | + break; |
| 99 | + } |
| 100 | + $allData .= $chunk; |
| 101 | +} |
| 102 | + |
| 103 | +if (strlen($allData) === 1000 && $allData === str_repeat('A', 1000)) { |
| 104 | + echo "Test 3 passed: large stream data read correctly.\n"; |
| 105 | +} else { |
| 106 | + echo "Test 3 FAILED: expected 1000 bytes of 'A', got " . strlen($allData) . " bytes.\n"; |
| 107 | +} |
| 108 | + |
| 109 | +fclose($stream3); |
| 110 | + |
| 111 | +// Test 4: Verify cleanup - no resource leak over repeated iterations. |
| 112 | +// Run the stream-outlives-stmt pattern in a loop and check that memory |
| 113 | +// usage stabilizes, confirming the statement is freed when the stream closes. |
| 114 | +$leakDetected = false; |
| 115 | +for ($i = 0; $i < 20; $i++) { |
| 116 | + $s = getStream($conn); |
| 117 | + $d = fread($s, 100); |
| 118 | + fclose($s); |
| 119 | + unset($s); |
| 120 | + unset($d); |
| 121 | + |
| 122 | + if ($i === 0) { |
| 123 | + $memBaseline = memory_get_usage(); |
| 124 | + } |
| 125 | +} |
| 126 | +$memAfter = memory_get_usage(); |
| 127 | +// Allow a small tolerance (32 KB) for PHP internal allocations. |
| 128 | +// A real leak would grow ~proportionally to iteration count. |
| 129 | +if (($memAfter - $memBaseline) < 32768) { |
| 130 | + echo "Test 4 passed: no resource leak detected over repeated iterations.\n"; |
| 131 | +} else { |
| 132 | + echo "Test 4 FAILED: possible leak, memory grew by " . ($memAfter - $memBaseline) . " bytes.\n"; |
| 133 | +} |
| 134 | + |
| 135 | +// Test 5: After all streams are closed, the connection should still be |
| 136 | +// fully functional — proving statements were cleaned up properly. |
| 137 | +$stmt = sqlsrv_query($conn, "SELECT 1 AS alive"); |
| 138 | +if ($stmt === false) { |
| 139 | + echo "Test 5 FAILED: connection unusable after cleanup.\n"; |
| 140 | +} else { |
| 141 | + sqlsrv_fetch($stmt); |
| 142 | + $val = sqlsrv_get_field($stmt, 0); |
| 143 | + if ($val == 1) { |
| 144 | + echo "Test 5 passed: connection works after all streams and statements cleaned up.\n"; |
| 145 | + } else { |
| 146 | + echo "Test 5 FAILED: unexpected value $val.\n"; |
| 147 | + } |
| 148 | + sqlsrv_free_stmt($stmt); |
| 149 | +} |
| 150 | + |
| 151 | +sqlsrv_close($conn); |
| 152 | +echo "Done.\n"; |
| 153 | +?> |
| 154 | +--EXPECT-- |
| 155 | +Test 1 passed: stream data read correctly after stmt out of scope. |
| 156 | +Test 2 passed: stream from prepared stmt works after stmt out of scope. |
| 157 | +Test 3 passed: large stream data read correctly. |
| 158 | +Test 4 passed: no resource leak detected over repeated iterations. |
| 159 | +Test 5 passed: connection works after all streams and statements cleaned up. |
| 160 | +Done. |
0 commit comments