Skip to content

Commit 10251b2

Browse files
Matt Fickenweltling
Matt Ficken
authored andcommitted
Fixed bug #62379 failing ODBC long column functionality
1 parent d9d21b2 commit 10251b2

File tree

4 files changed

+185
-65
lines changed

4 files changed

+185
-65
lines changed

ext/pdo/tests/pdo_test.inc

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,19 @@ class PDOTest {
6666
}
6767

6868
static function test_factory($file) {
69-
$data = file_get_contents($file);
70-
$data = preg_replace('/^.*--REDIRECTTEST--/s', '', $data);
71-
$config = eval($data);
69+
$config = self::get_config($file);
7270
foreach ($config['ENV'] as $k => $v) {
7371
putenv("$k=$v");
7472
}
7573
return self::factory();
7674
}
75+
76+
static function get_config($file) {
77+
$data = file_get_contents($file);
78+
$data = preg_replace('/^.*--REDIRECTTEST--/s', '', $data);
79+
$config = eval($data);
80+
81+
return $config;
82+
}
7783
}
7884
?>

ext/pdo_odbc/odbc_stmt.c

Lines changed: 36 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -633,58 +633,49 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, unsigned l
633633
}
634634

635635
if (rc == SQL_SUCCESS_WITH_INFO) {
636-
/* promote up to a bigger buffer */
637-
638-
if (C->fetched_len != SQL_NO_TOTAL) {
639-
/* use size suggested by the driver, if it knows it */
640-
buf = emalloc(C->fetched_len + 1);
641-
memcpy(buf, C->data, C->fetched_len);
642-
buf[C->fetched_len] = 0;
643-
used = C->fetched_len;
644-
} else {
645-
buf = estrndup(C->data, 256);
646-
used = 255; /* not 256; the driver NUL terminated the buffer */
647-
}
648-
636+
/* this is a 'long column'
637+
638+
read the column in 255 byte blocks until the end of the column is reached, reassembling those blocks
639+
in order into the output buffer
640+
641+
this loop has to work whether or not SQLGetData() provides the total column length.
642+
calling SQLDescribeCol() or other, specifically to get the column length, then doing a single read
643+
for that size would be slower except maybe for extremely long columns.*/
644+
char *buf2;
645+
646+
buf2 = emalloc(256);
647+
buf = estrndup(C->data, 256);
648+
used = 255; /* not 256; the driver NUL terminated the buffer */
649+
649650
do {
650651
C->fetched_len = 0;
651-
rc = SQLGetData(S->stmt, colno+1, SQL_C_CHAR,
652-
buf + used, alloced - used,
653-
&C->fetched_len);
654-
655-
if (rc == SQL_NO_DATA) {
656-
/* we got the lot */
657-
break;
658-
} else if (rc != SQL_SUCCESS) {
659-
pdo_odbc_stmt_error("SQLGetData");
660-
if (rc != SQL_SUCCESS_WITH_INFO) {
661-
break;
662-
}
663-
}
664-
665-
if (C->fetched_len == SQL_NO_TOTAL) {
666-
used += alloced - used;
652+
/* read block. 256 bytes => 255 bytes are actually read, the last 1 is NULL */
653+
rc = SQLGetData(S->stmt, colno+1, SQL_C_CHAR, buf2, 256, &C->fetched_len);
654+
655+
/* resize output buffer and reassemble block */
656+
if (rc==SQL_SUCCESS_WITH_INFO) {
657+
/* point 5, in section "Retrieving Data with SQLGetData" in http://msdn.microsoft.com/en-us/library/windows/desktop/ms715441(v=vs.85).aspx
658+
states that if SQL_SUCCESS_WITH_INFO, fetched_len will be > 255 (greater than buf2's size)
659+
(if a driver fails to follow that and wrote less than 255 bytes to buf2, this will AV or read garbage into buf) */
660+
buf = erealloc(buf, used + 255+1);
661+
memcpy(buf + used, buf2, 255);
662+
used = used + 255;
663+
} else if (rc==SQL_SUCCESS) {
664+
buf = erealloc(buf, used + C->fetched_len+1);
665+
memcpy(buf + used, buf2, C->fetched_len);
666+
used = used + C->fetched_len;
667667
} else {
668-
used += C->fetched_len;
669-
}
670-
671-
if (rc == SQL_SUCCESS) {
672-
/* this was the final fetch */
668+
/* includes SQL_NO_DATA */
673669
break;
674670
}
675-
676-
/* we need to fetch another chunk; resize the
677-
* buffer */
678-
alloced *= 2;
679-
buf = erealloc(buf, alloced);
671+
680672
} while (1);
681-
682-
/* size down */
683-
if (used < alloced - 1024) {
684-
alloced = used+1;
685-
buf = erealloc(buf, used+1);
686-
}
673+
674+
efree(buf2);
675+
676+
/* NULL terminate the buffer once, when finished, for use with the rest of PHP */
687677
buf[used] = '\0';
678+
688679
*ptr = buf;
689680
*caller_frees = 1;
690681
*len = used;

ext/pdo_odbc/tests/common.phpt

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,54 @@
22
ODBC
33
--SKIPIF--
44
<?php # vim:ft=php
5-
if (!extension_loaded('pdo_odbc')) print 'skip'; ?>
5+
if (!extension_loaded('pdo_odbc')) print 'skip';
6+
if (substr(PHP_OS, 0, 3) == 'WIN' &&
7+
false === getenv('PDOTEST_DSN') &&
8+
false === getenv('PDO_ODBC_TEST_DSN') &&
9+
!extension_loaded('com_dotnet')) {
10+
die('skip - either PDOTEST_DSN or com_dotnet extension is needed to setup the connection');
11+
}
612
--REDIRECTTEST--
713
# magic auto-configuration
814

915
$config = array(
10-
'TESTS' => 'ext/pdo/tests'
16+
'TESTS' => 'ext/pdo/tests',
17+
'ENV' => array()
1118
);
1219

13-
14-
if (false !== getenv('PDO_ODBC_TEST_DSN')) {
15-
# user set them from their shell
20+
// try loading PDO driver using ENV vars and if none given, and on Windows, try using MS Access
21+
// and if not, skip the test
22+
//
23+
// try to use common PDO env vars, instead of PDO_ODBC specific
24+
if (false !== getenv('PDOTEST_DSN')) {
25+
// user should have to set PDOTEST_DSN so that:
26+
// 1. test is skipped if user doesn't want to test it, even if they have MS Access installed
27+
// 2. it detects if ODBC driver is not installed - to avoid test bug
28+
// 3. it detects if ODBC driver is installed - so test will be run
29+
// 4. so a specific ODBC driver can be tested - if system has multiple ODBC drivers
30+
31+
$config['ENV']['PDOTEST_DSN'] = getenv('PDOTEST_DSN');
32+
$config['ENV']['PDOTEST_USER'] = getenv('PDOTEST_USER');
33+
$config['ENV']['PDOTEST_PASS'] = getenv('PDOTEST_PASS');
34+
if (false !== getenv('PDOTEST_ATTR')) {
35+
$config['ENV']['PDOTEST_ATTR'] = getenv('PDOTEST_ATTR');
36+
}
37+
} else if (false !== getenv('PDO_ODBC_TEST_DSN')) {
38+
// user set these from their shell instead
1639
$config['ENV']['PDOTEST_DSN'] = getenv('PDO_ODBC_TEST_DSN');
1740
$config['ENV']['PDOTEST_USER'] = getenv('PDO_ODBC_TEST_USER');
1841
$config['ENV']['PDOTEST_PASS'] = getenv('PDO_ODBC_TEST_PASS');
1942
if (false !== getenv('PDO_ODBC_TEST_ATTR')) {
2043
$config['ENV']['PDOTEST_ATTR'] = getenv('PDO_ODBC_TEST_ATTR');
2144
}
2245
} elseif (preg_match('/^WIN/i', PHP_OS)) {
23-
# on windows, try to create a temporary MS access database
46+
// on Windows and user didn't set PDOTEST_DSN, try this as a fallback:
47+
// check if MS Access DB is installed, and if yes, try using it. create a temporary MS access database.
48+
//
2449
$path = realpath(dirname(__FILE__)) . '\pdo_odbc.mdb';
2550
if (!file_exists($path)) {
2651
try {
52+
// try to create database
2753
$adox = new COM('ADOX.Catalog');
2854
$adox->Create('Provider=Microsoft.Jet.OLEDB.4.0;Data Source=' . $path);
2955
$adox = null;
@@ -32,9 +58,12 @@ if (false !== getenv('PDO_ODBC_TEST_DSN')) {
3258
}
3359
}
3460
if (file_exists($path)) {
61+
// database was created and written to file system
3562
$config['ENV']['PDOTEST_DSN'] = "odbc:Driver={Microsoft Access Driver (*.mdb)};Dbq=$path;Uid=Admin";
36-
}
37-
}
63+
} // else: $config['ENV']['PDOTEST_DSN'] not set
64+
} // else: $config['ENV']['PDOTEST_DSN'] not set
65+
// test will be skipped. see SKIPIF section of long_columns.phpt
66+
3867
# other magic autodetection here, eg: for DB2 by inspecting env
3968
/*
4069
$USER = 'db2inst1';

ext/pdo_odbc/tests/long_columns.phpt

Lines changed: 103 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,44 @@ PDO ODBC "long" columns
33
--SKIPIF--
44
<?php # vim:ft=php
55
if (!extension_loaded('pdo_odbc')) print 'skip not loaded';
6+
// make sure there is an ODBC driver and a DSN, or the test will fail
7+
include 'ext/pdo/tests/pdo_test.inc';
8+
$config = PDOTest::get_config('ext/pdo_odbc/tests/common.phpt');
9+
if (!isset($config['ENV']['PDOTEST_DSN']) || $config['ENV']['PDOTEST_DSN']===false) print 'skip';
610
?>
711
--FILE--
812
<?php
13+
// setup: set PDOTEST_DSN environment variable
14+
// for MyODBC (MySQL) and MS SQL Server, you need to also set PDOTEST_USER and PDOTEST_PASS
15+
//
16+
// can use MS SQL Server on Linux - using unixODBC
17+
// -RHEL6.2
18+
// -download & instructions: http://www.microsoft.com/en-us/download/details.aspx?id=28160
19+
// -Linux6\sqlncli-11.0.1790.0.tar.gz (it calls RHEL6.x 'Linux6' for some reason)
20+
// -follow instructions on web page and install script
21+
// -may have to specify connection info in connection string without using a DSN (DSN-less connection)
22+
// -for example:
23+
// set PDOTEST_DSN='odbc:Driver=SQL Server Native Client 11.0;Server=10.200.51.179;Database=testdb'
24+
// set PDOTEST_USER=sa
25+
// set PDOTEST_PASS=Password01
26+
//
27+
// on Windows, the easy way to do this:
28+
// 1. install MS Access (part of MS Office) and include ODBC (Development tools feature)
29+
// install the x86 build of the Drivers. You might not be able to load the x64 drivers.
30+
// 2. in Control Panel, search for ODBC and open "Setup data sources (ODBC)"
31+
// 3. click on System DSN tab
32+
// 4. click Add and choose "Microsoft Access Driver (*.mdb, *.accdb)" driver
33+
// 5. enter a DSN, ex: accdb12
34+
// 6. click 'Create' and select a file to save the database as
35+
// -otherwise, you'll have to open MS Access, create a database, then load that file in this Window to map it to a DSN
36+
// 7. set the environment variable PDOTEST_DSN="odbc:<system dsn from step 5>" ex: SET PDOTEST_DSN=odbc:accdb12
37+
// -note: on Windows, " is included in environment variable
38+
//
39+
// easy way to compile:
40+
// configure --disable-all --enable-cli --enable-zts --enable-pdo --with-pdo-odbc --enable-debug
41+
// configure --disable-all --eanble-cli --enable-pdo --with-pdo-odbc=unixODBC,/usr,/usr --with-unixODBC=/usr --enable-debug
42+
//
43+
944
require 'ext/pdo/tests/pdo_test.inc';
1045
$db = PDOTest::test_factory('ext/pdo_odbc/tests/common.phpt');
1146
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
@@ -20,27 +55,86 @@ if (false === $db->exec('CREATE TABLE TEST (id INT NOT NULL PRIMARY KEY, data CL
2055

2156
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
2257

23-
$sizes = array(32, 64, 128, 253, 254, 255, 256, 257, 258, 512, 1024, 2048, 3998, 3999, 4000);
58+
// the driver reads columns in blocks of 255 bytes and then reassembles those blocks into a single buffer.
59+
// test sizes around 255 to make sure that the reassembly works (and that the column is split into 255 byte blocks by the database)
60+
// also, test sizes below 255 to make sure that they work - and are not treated as a long column (should be read in a single read)
61+
$sizes = array(32, 53, 64, 79, 128, 253, 254, 255, 256, 257, 258, 1022, 1023, 1024, 1025, 1026, 510, 511, 512, 513, 514, 1278, 1279, 1280, 1281, 1282, 2046, 2047, 2048, 2049, 2050, 1534, 1535, 1536, 1537, 1538, 3070, 3071, 3072, 3073, 3074, 3998, 3999, 4000);
2462

25-
$db->beginTransaction();
26-
$insert = $db->prepare('INSERT INTO TEST VALUES (?, ?)');
63+
function alpha_repeat($len) {
64+
// use the alphabet instead of 'i' characters to make sure the blocks don't overlap when they are reassembled
65+
$out = "";
66+
while (strlen($out) < $len) {
67+
$out .= "abcdefghijklmnopqrstuvwxyz";
68+
}
69+
return substr($out, 0, $len);
70+
}
71+
72+
// don't use Prepared Statements. that fails on MS SQL server (works with Access, MyODBC), which is a separate failure, feature/code-path from what
73+
// this test does - nice to be able to test using MS SQL server
2774
foreach ($sizes as $num) {
28-
$insert->execute(array($num, str_repeat('i', $num)));
75+
$text = alpha_repeat($num);
76+
$db->exec("INSERT INTO TEST VALUES($num, '$text')");
2977
}
30-
$insert = null;
31-
$db->commit();
3278

79+
// verify data
3380
foreach ($db->query('SELECT id, data from TEST') as $row) {
34-
$expect = str_repeat('i', $row[0]);
81+
$expect = alpha_repeat($row[0]);
3582
if (strcmp($expect, $row[1])) {
3683
echo "Failed on size $row[id]:\n";
3784
printf("Expected %d bytes, got %d\n", strlen($expect), strlen($row['data']));
38-
echo bin2hex($expect) . "\n";
39-
echo bin2hex($row['data']) . "\n";
85+
echo ($expect) . "\n";
86+
echo ($row['data']) . "\n";
87+
} else {
88+
echo "Passed on size $row[id]\n";
4089
}
4190
}
4291

4392
echo "Finished\n";
4493

4594
--EXPECT--
95+
Passed on size 32
96+
Passed on size 53
97+
Passed on size 64
98+
Passed on size 79
99+
Passed on size 128
100+
Passed on size 253
101+
Passed on size 254
102+
Passed on size 255
103+
Passed on size 256
104+
Passed on size 257
105+
Passed on size 258
106+
Passed on size 1022
107+
Passed on size 1023
108+
Passed on size 1024
109+
Passed on size 1025
110+
Passed on size 1026
111+
Passed on size 510
112+
Passed on size 511
113+
Passed on size 512
114+
Passed on size 513
115+
Passed on size 514
116+
Passed on size 1278
117+
Passed on size 1279
118+
Passed on size 1280
119+
Passed on size 1281
120+
Passed on size 1282
121+
Passed on size 2046
122+
Passed on size 2047
123+
Passed on size 2048
124+
Passed on size 2049
125+
Passed on size 2050
126+
Passed on size 1534
127+
Passed on size 1535
128+
Passed on size 1536
129+
Passed on size 1537
130+
Passed on size 1538
131+
Passed on size 3070
132+
Passed on size 3071
133+
Passed on size 3072
134+
Passed on size 3073
135+
Passed on size 3074
136+
Passed on size 3998
137+
Passed on size 3999
138+
Passed on size 4000
46139
Finished
140+

0 commit comments

Comments
 (0)