Am I losing my mind, or does the Postgres PDO driver just not support prepared statements, but instead emulates them client side?
The following code returns NO ERROR
See http://www.php.net/manual/en/pdo.prepare.php
Note:
Emulated prepared statements does not communicate with the database server so PDO::prepare() does not check the statement.
(in fact real prepared statements are not sent immediately anyway, see answer to Q2 below)
Anyway you may issue:
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES,false); 
to get real prepared statements implemented with the SQL PREPARE command. See http://www.php.net/manual/en/pdo.setattribute.php for more.
On further discussion and tests, two questions arise:
Q1. Why does pdo::getAttribute(PDO::ATTR_EMULATE_PREPARES) yield an error?
Indeed setAttribute doesn't error out but getAttribute(PDO::ATTR_EMULATE_PREPARES) says:
'SQLSTATE[IM001]: Driver does not support this function: driver does not support that attribute'
Looking at the documentation for pdo::getAttribute, it says The constants that apply to database connections are as follows, and a number of constants follow from PDO::ATTR_AUTOCOMMIT to PDO::ATTR_TIMEOUT, and it's remarkable that PDO::ATTR_EMULATE_PREPARES is not in them. So strictly speaking, we should not expect getAttribute(PDO::ATTR_EMULATE_PREPARES) to work, anyway.
Now looking at the source code to be sure, it appears that the pdo_pgsql driver provides a pdo_pgsql_get_attribute function that has a switch statement on:
and that's it. No trace of PDO_ATTR_EMULATE_PREPARES which is why ultimately this error appears.
On the other hand, the function pdo_pgsql_set_attr has a switch statement on:
which confirms that this attribute is actually taken into account when set.
So PDO is just inconsistent with getAttribute that doesn't match setAttribute.
Q2 - When prepare emulation is false, why doesn't a bogus statement immediately raise an error when prepared?
Consider this piece of code in pgsql_statement.c:
        if (!S->is_prepared) {
stmt_retry:
            /* we deferred the prepare until now, because we didn't
             * know anything about the parameter types; now we do */
            S->result = PQprepare(H->server, S->stmt_name, S->query, 
                        stmt->bound_params ? zend_hash_num_elements(stmt->bound_params) : 0,
                        S->param_types);
It shows that PQprepare is used (so that's a "real" prepared statement), but also that the statement is not immediately sent to the server. That's why the dbo::prepare("bogus statement") won't return false: it's actually not sent to the server for lack of parameter types.