The low-level API could simply not allow SQL statements as strings, and instead provide separate functions to build the queries and statements.
It would provide entry points which could be used to ensure proper escaping and such, and would still allow for easily generating queries dynamically in the cases where that is needed.
Of course, it doesn't completely guard against Bobby Tables[1], one could imagine someone including a run-time code generator and feed it unprotected SQL as input.
But it should make it a lot more difficult, as it would be much more "unnatural", requiring going against the grain, to inject unprotected user data. Also, the "query_execute" function could raise an error if there's more than one statement, requiring one to use a different function for batch execution.
Pseudo-codish example off the top of my head, for the sake of illustration:
is_active = str_to_bool(args['active']); // from user
qry = new_query(ctx);
users_alias = new_table_alias(qry, 't');
query_select_column(users_alias, 'id');
query_select_column(users_alias, 'username');
query_from_table(users_alias, 'users');
filter_active = query_column_eq_clause(users_alias, 'active', is_active);
where = query_where(qry);
query_where_append(where, filter_active);
cursor = query_execute(qry);
[1]: https://xkcd.com/327/