26

NOTE: I have narrowed this problem down to specifically PDO because I am able to successfully prepare and execute statements using the odbc_* functions.

Why can't I bind this parameter to the PDO prepared statement?

This works:

$mssqldriver = 'ODBC Driver 13 for SQL Server';
$pdoDB = new PDO("odbc:Driver=$mssqldriver;Server=$hostname;Database=$dbname", $username, $password);
$pdoDB->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );

$sql = "SELECT 'value' AS col where 'this' = 'this'";
$stmt = $pdoDB->prepare($sql);
$params = [];
$stmt->execute($params);
print_r($stmt->fetch());
Array ( [col] => value [0] => value )

Does not work:

$sql = "SELECT 'value' AS col where 'this' = ?";
$stmt = $pdoDB->prepare($sql);
$params = ['this'];
$stmt->execute($params);
print_r($stmt->fetch());

Web Server is running PHP 5.5.9 on Linux Ubuntu 14.04 with ODBC Driver 13 for SQL Server and connecting to Microsoft SQL Server 2012 on Windows Server 2012

Here's the full error:

Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[22001]:
String data, right truncated: 0
[Microsoft][ODBC Driver 13 for SQL Server]
String data, right truncation
(SQLExecute[0] at /build/buildd/php5-5.5.9+dfsg/ext/pdo_odbc/odbc_stmt.c:254)' in /var/www/scratch.php:46
Stack trace:
#0 /var/www/scratch.php(46): PDOStatement->execute(Array)
#1 {main} thrown in /var/www/scratch.php on line 46

I have also tried setting:

$pdoDB->setAttribute( PDO::ATTR_EMULATE_PREPARES, true );

And using named parameters:

$sql = "SELECT 'value' AS col where 'this' = :myVal";
$stmt = $pdoDB->prepare($sql);
$params = ['myVal' => 'this'];
$stmt->execute($params);
print_r($stmt->fetch());

Even with an explicit colon:

$params = [':myVal' => 'this'];

I also tried just using bindParam as demonstrated in this answer:

$sql = "SELECT 'value' AS col where 'this' = ?";
$stmt = $pdoDB->prepare($sql);
$param = 'this';
$stmt->bindParam(1, $param);
$stmt->execute();
print_r($stmt->fetch());

As well as with named parameters:

$sql = "SELECT 'value' AS col where 'this' = :myVal";
$stmt = $pdoDB->prepare($sql);
$param = 'this';
$stmt->bindParam(':myVal', $param, PDO::PARAM_STR);
$stmt->execute();
print_r($stmt->fetch());

If I try to explicitly set the length:

$stmt->bindParam(':myVal', $param, PDO::PARAM_STR, 4);

I get a bonus error:

Fatal error: Uncaught exception 'PDOException' with message
'SQLSTATE[42000]: Syntax error or access violation: 102
[Microsoft][ODBC Driver 13 for SQL Server][SQL Server]
Incorrect syntax near 'OUTPUT'.

And yes, all this is a trivialized example without tables so that you can easily reproduce it, but just to be sure, I have actually tried this with a real table.

CREATE TABLE myTable (
    id INT IDENTITY PRIMARY KEY,
    val NVARCHAR(255)
);
INSERT INTO myTable (val) VALUES ('hello world');

Works:

$sql = "SELECT * FROM myTable WHERE val = 'hello world'";
$stmt = $pdoDB->prepare($sql);
$params = [];
$stmt->execute($params);
print_r($stmt->fetch());
Array ( [id] => 1 [0] => 1 [val] => hello world [1] => hello world )

Does not work:

$sql = "SELECT * FROM myTable WHERE val = ?";
$stmt = $pdoDB->prepare($sql);
$params = ['hello world'];
$stmt->execute($params);
print_r($stmt->fetch());

All paths lead to the same error:

String data, right truncated

Community
  • 1
  • 1
Jeff Puckett
  • 37,464
  • 17
  • 118
  • 167
  • Do you have ATTR_EMULATE_PREPARES enabled or not? Does it make a difference if you change it? Does it make a difference if you use a named parameter like `SELECT :param as col` and bind `[':param' => 'test']` ? I would not expect a change... – Michael Berkowski Jul 07 '16 at 21:56
  • @MichaelBerkowski I just tried both of those to no avail - same error. – Jeff Puckett Jul 07 '16 at 22:01
  • What type is a 'this' filed? – zajonc Jul 07 '16 at 22:41
  • @zajonc it's a string literal so others can easily reproduce without need for a dataset. My actual use case involves an NVARCHAR(255), but I can't get it to work for any datatype. – Jeff Puckett Jul 07 '16 at 22:44
  • I suppose it's entirely DBMS issue, with little relation to PDO. Out of curiosity, can you reproduce with all-static query as well? I mean `'this' = 'this'`? – Your Common Sense Jul 08 '16 at 15:06
  • @YourCommonSense actually, I have narrowed it down to specifically PDO because I am able to successfully prepare and execute using the [odbc_*](http://php.net/manual/en/book.uodbc.php) functions. I'm not sure what you mean by all static query, I can execute `SELECT 'value' AS col where 'this' = 'this'` using PDO just fine, it's the prepared executes that fail. – Jeff Puckett Jul 08 '16 at 15:18
  • That is very interesting because with emulation turned ON the prepared query should be effectively the same. Can you play a trick I am usually using to test the emulation mode? can you run a query like this `SELECT 'value' AS col where 'this' = :myVal bug` and watch the query stub shown in the error message? Note the intentional error at the end – Your Common Sense Jul 08 '16 at 15:57
  • @YourCommonSense sorry I had to step away for a while, but I just tested this and it throws the same error `String data, right truncated` and if I execute `SELECT 'value' AS col where 'this' = 'this' bug` then it throws `Incorrect syntax near 'bug'.` – Jeff Puckett Jul 08 '16 at 23:01
  • ah yes it's my fault. it should be `SELECT 'value' AS col ' where 'this' = 'this'`. The idea to see the part of the actual query, it is show starting for the erroneous spot, so it should be placed before WHERE statement, not after. – Your Common Sense Jul 09 '16 at 04:39
  • @YourCommonSense back at the office now, that results in error: `An expression of non-boolean type specified in a context where a condition is expected, near 'this'. ` – Jeff Puckett Jul 11 '16 at 15:32
  • Heck, I should be more careful with my intentional errors. Can you try something of your own? The idea is to get the part of the query in the error message ans see whether there is a placeholder (means emulation mode is turned off) or interpolated value (means on). Something like `SELECT 'value' AS col wher 'this' = :myVal` should do. The is to verify the emulation mode. – Your Common Sense Jul 11 '16 at 15:38
  • By the way, there is a simpler way. Which line throws an error? Prepare or execute? – Your Common Sense Jul 11 '16 at 15:39
  • @YourCommonSense `SELECT 'value' AS col wher 'this' = :myVal` prepares fine, on execute it throws `String data, right truncated` – Jeff Puckett Jul 11 '16 at 15:48
  • Looks extremely strange to me. I am out of ideas. – Your Common Sense Jul 11 '16 at 15:50
  • @YourCommonSense no joke, I'm convinced now that it's a bug with the pdo implementation - notice the strange error message when trying to specify the length parameter with `bindParam` - it's as if it's expecting an output variable in that place. – Jeff Puckett Jul 11 '16 at 15:56
  • @JeffPuckettII this one is all right. Length is of no use for regular binding. this parameter is for out/inout parameters only. So the error message is rather ok. – Your Common Sense Jul 11 '16 at 16:00
  • @YourCommonSense ah I see now, required *if* output. thanks – Jeff Puckett Jul 11 '16 at 16:04
  • Hi @JeffPuckettII, could you add table creation query to your question as well, please? I have an idea about your error and that information might be helpful. – max Jul 11 '16 at 17:26
  • Hi @max, actually there's no table involved in my example query, but I have created multiple tables and stored procedures with the same error. – Jeff Puckett Jul 11 '16 at 19:25
  • @JeffPuckettI, I think, [this question](http://stackoverflow.com/questions/183488/what-does-the-sql-server-error-string-data-right-truncation-mean-and-how-do-i) is relevant to your problem as you might have fixed-length char type (like constant 'this') and PDO failed to cast the parameter to that length. – max Jul 11 '16 at 19:30
  • @max that's an interesting question, but as stated in the edits, the problem was that they were passing a value too long for perl's DBD API to bind. as for your connection, I have updated my question providing the DDL for a test table and it still produces the same error. – Jeff Puckett Jul 11 '16 at 20:41
  • I cannot recreate your issue. However, your driver is different. Why not the standard: `$pdoDB = new PDO("odbc:Driver={SQL Server};Server=$server;database=$database",$username,$password);` – Parfait Jul 11 '16 at 22:12
  • @Parfait where would I get that driver for linux? – Jeff Puckett Jul 11 '16 at 22:12
  • That may be the issue crossing OS's. Did you run PHP locally on Windows Server? I wonder if your issue persists. It could be the driver. – Parfait Jul 11 '16 at 22:15
  • @Parfait Note that this official Microsoft driver works just fine when using the [`odbc_*` functions](http://php.net/manual/en/book.uodbc.php). I do not have access to a Windows server with PHP installed. – Jeff Puckett Jul 11 '16 at 22:17
  • Two questions: 1. When setting the length explicitly, shouldn't' the length match the length of the database-field? 2. Does the problem also occur if the database-field is of type 'varchar' (without the 'N') and the input has the same length as the varchar field has? – erg Jul 12 '16 at 08:07
  • Ok, last try. See what I mean. When I test the **emulation mode** with my mysql database, for a query like this `"SELECT * FROM myTable BUG val = ? "` I get the message `for the right syntax to use near 'val = 'test'' at line 1'` when emulation mode is on, and `the right syntax to use near 'val = ?' at line 1'` when it is off. The idea is to get the relevant part of the **actual** query that has been sent to database. And thus get the idea whether the emulation mode were effectively turned on. – Your Common Sense Jul 12 '16 at 08:15
  • Have you tried FreeTDS (dblib PDO driver) + ODBC (to display error messages) to access SQL Server from PHP? It could me a little messy to configure but it works perfectly (I use one on production) – Aleksey Ratnikov Jul 12 '16 at 09:02
  • @YourCommonSense thanks again for your gracious bounty - who knows how long it would have taken me to figure this out otherwise. – Jeff Puckett Jul 13 '16 at 22:44
  • @Jeff, could you possibly help [this guy](http://stackoverflow.com/questions/43207071/pdobindvalue-fails-on-query-with-nested-select)? Looks like you have an immense experience with PDO ODBC. – Your Common Sense Apr 11 '17 at 11:16

4 Answers4

21

Unfortunately,

It's a PDO_ODBC 64-bit incompatibility problem (#61777, #64824) and without any doubts you are on a 64-bit build which doesn't allow you to bind parameters.

Fortunately,

It has a patch that was first included in the 5.6 release:

This bug is also referenced in #61777 and is still present in the latest stable release of the 5.5 branch. I see two tickets exist for this problem already, and I'm just submitting these changes via github as a reminder that this is a serious problem for anyone using PDO_ODBC on the x64 builds.

What is wrong with your PHP's shipped PDO_ODBC?

By looking at one of those recommended patches:

diff --git a/ext/pdo_odbc/odbc_stmt.c b/ext/pdo_odbc/odbc_stmt.c
index 8b0ccf3..1d275cd 100644
--- a/ext/pdo_odbc/odbc_stmt.c
+++ b/ext/pdo_odbc/odbc_stmt.c
@@ -551,7 +551,7 @@ static int odbc_stmt_describe(pdo_stmt_t *stmt, int colno TSRMLS_DC)
    struct pdo_column_data *col = &stmt->columns[colno];
    RETCODE rc;
    SWORD   colnamelen;
-   SDWORD  colsize;
+   SQLULEN colsize;
    SQLLEN displaysize;

We see the only thing that's changed is SDWORD (16-bit signed integer) which is substituted with new ODBC type SQLULEN that is 64 bits in a 64-bit ODBC application and 32 bits in a 32-bit ODBC application.

I believe committer wasn't aware of colsize data type only since in the very next line SQLLEN is defined properly.

What should I do now?

  1. Upgrade to PHP version >= 5.6
  2. Stick with odbc_* functions as a working solution.
  3. Compile a PHP v5.5.9 with provided patches.
  4. Build your own PDO wrapper as recommended by @GordonM
revo
  • 47,783
  • 14
  • 74
  • 117
  • Wow 64824 is spot on *the bug* I've got too. I covet thy Google-Fu because I never came close to finding that. I'm afraid that the patch was built for a slightly different bug referenced in 61777, but I'm still eager to try. It might take me a couple days to get the time to spin up a test vm to build custom PDO with that patch, but I'm excited thanks! – Jeff Puckett Jul 12 '16 at 12:16
  • I compared that patch to PHP 7.0 related source files and can say you are going to do it right. @JeffPuckettII – revo Jul 12 '16 at 12:33
  • Today I installed a fresh ubuntu 16.04 on a vm with php7, and confirmed that this issue doesn't exist. I edited your answer to include this option. Tomorrow I will spin up another vm with php5 and compile with the patch to fully confirm your answer, and if it's successful, then I will happily accept your answer. – Jeff Puckett Jul 12 '16 at 23:04
  • I did not add `Upgrade your PHP` to the list since I guessed you are not thinking about this option. So if you are able to do it, you are totally fine. A PHP version > 5.5.x does not come with this issue. @JeffPuckettII – revo Jul 13 '16 at 05:54
  • CONFIRMED. I cloned the php source, checked out release 5.5.9, compiled and installed to verify the bug. Then I cherry-picked that commit, recompiled and installed and totally verified that fixed this bug. – Jeff Puckett Jul 13 '16 at 19:26
4

This is probably not what you want to hear, but this has all the hallmarks of a bug in PHP's ODBC driver for PDO (which is not heavily used as PHP programmers tend to favour open source databases like MySQL/SQLite/Postgres over commercial offerings), or in the underlying SQL server driver (which is poorly supported in Linux, for similar reasons), though if odbc_* works then it's probably not the underlying driver.

If you try to do the exact same tasks, except using "sqlite::memory:" as the DSN, all your examples work. This makes it highly unlikely that you're doing anything wrong (unless MS Server has some really weird non-conforming SQL syntax of which I'm not aware). Your examples work fine for me with SQLite when ATTR_EMULATE_PREPARES is both enabled and disabled.

All I think you can realistically do is file a bug report and hope somebody picks it up. You may be in for a long wait though.

As for practical solutions to your problem, your options are either a) switch to a DBMS that PHP supports or b) resort to SQL string construction instead of prepared statements and be ready to accept the burden of avoiding SQL injection attacks yourself. This should be considered a last resort though! Using PDO::quote() may help but I'd also make sure your data is very thoroughly validated as well. I know it's not an ideal solution, but if you must use MS SQL and can't wait for the PHP team to bugfix it then I don't really see that you have much of a choice.

Or there's option c) which is use odbc_* functions instead, if they work. Of course if you want to use OOP style then you'll have to implement your own class that wraps the procedural odbc functions in OO methods so that could potentially be a lot of work.

EDIT: I found another question on Stack Overflow where the asker seems to have a similar problem. His solution was to ditch the "official" MS driver in favour of FreeTDS. This might be something of a shot in the dark, but it could be worth a try.

Community
  • 1
  • 1
GordonM
  • 31,179
  • 15
  • 87
  • 129
0

I had the same error Uncaught Uncaught PDOException: SQLSTATE[22001]. It turned out that the length of the database for password was too short(30) for the encrypted password. So, I just increased it and solved the problem.

0

I am facing this error time to time too. It has something to do with real data length against defined length - in my opition :). So for example I have data column nvarchar(50) called Name and

select name from tabname

shows that error. But when I cast output to double-sized nvarchar, it works:

select cast(name as nvarchar(100)) as name from tabname

May be I am far away from real reason of problem, but this solution works for me good.

Pavel
  • 361
  • 1
  • 2
  • 9