100% safe? Not even close. Bound parameters (prepared statement-wise or otherwise) effectively can prevent, 100%, one class of SQL injection vulnerability (assuming no db bugs and a sane implementation). In no way do they prevent other classes. Note that PostgreSQL (my db of choice) has an option to bind parameters to ad hoc statements which saves a round trip regarding prepared statements if you don't need certain features of these.
You have to figure that many large, complex databases are programs in themselves. The complexity of these programs varies quite a bit, and SQL injection is something that has to be looked out for inside programming routines. Such routines include triggers, user-defined functions, stored procedures, and the like. It is not always obvious how these things interact from an application level as many good dba's provide some degree of abstraction between the application access level and the storage level.
With bound parameters, the query tree is parsed, then, in PostgreSQL at least, the data is looked at in order to plan. The plan is executed. With prepared statements, the plan is saved so you can re-execute the same plan with different data over and over (this may or may not be what you want). But the point is that with bound parameters, a parameter cannot inject anything into the parse tree. So this class of SQL injection issue is properly taken care of.
But now we need to log who writes what to a table, so we add triggers and user-defined functions to encapsulate the logic of these triggers. These pose new issues. If you have any dynamic SQL in these, then you have to worry about SQL injection there. The tables they write to may have triggers of their own, and so forth. Similarly a function call might invoke another query which might invoke another function call and so forth. Each of these is independently planned of the main tree.
What this means is that if I run a query with a bound parameter like
foo'; drop user postgres; -- then it cannot directly implicate the top-level query tree and cause it to add another command to drop the postgres user. However, if this query calls another function directly or not, it is possible that somewhere down the line, a function will be vulnerable and the postgres user will be dropped. The bound parameters offered no protection to secondary queries. Those secondary queries need to make sure they use bound parameters too to the extent possible, and where not, will need to use appropriate quoting routines.
The rabbit hole goes deep.
BTW for a question on Stack Overflow where this problem is apparent, see http://stackoverflow.com/questions/37878426/conditional-where-expression-in-dynamic-query/37878574#37878574
Also a more problematic version (due to limitation on utility statements) at http://stackoverflow.com/questions/38016764/perform-create-index-in-plpgsql-doesnt-run/38021245#38021245