* By 'untyped' I mean that the type-system that you were using is no longer enforcing anything. There may be another type system in the compiler target.
However, we don't need to do anything nearly so elaborate to stop SQL injection. We just need a type "SqlQuery", since that will be instantly unusable by all string concatenation functions. We then make a concatenation function for SqlQuery values and a "stringToSqlQuery" function (or ".toSqlQuery" method, if you prefer) which converts strings to SqlQuery values by escaping them.
This way, we've turned SQL escaping into a type coercion, so we can only pass the type checker by escaping every string we put in our queries. Note also that it solves the double-escaping problem: since escaped strings have a different type to unescaped strings, we can't send them back through the escape function; ie. "stringToSqlQuery(stringToSqlQuery(foo))" is a type error.
$params = array();
$sql = "select * from foobars where 1";
if (isset($_GET['name'])) {
$sql .= " and name = :name"
$params[':name'] = $_GET['name'];
}
$stmt = $db->prepare($sql);
$stmt->execute($params);
You can still construct at runtime. Just let your typesystem handle the distinction between SQL code and data.