> My gut tells me that's not quite right (for some reasonable definition of 'usable'), since we can always build elimination forms into the language (after all, 'null' must be built in, and few take objection to building in elimination forms like if/then/else).

True, you could go the Go array route and have Optional be a special type that is generic, without otherwise supporting generic data structures.

> I really like comprehensions in Python, but hardly ever use them elsewhere.

I think code like possiblyMissing.map(value => code) introduces lots of unpleasant imbrication, and breaking out functions for every possibly-missing value also seems to me to lead to bad readability. I suppose this may be a matter of taste, or it may be related to other language features.

> just make sure to distinguish them from other string-like types, in a way that's statically enforceable, and where the only API to convert a 'String' to an 'SQL' is the escaping function.

I think this is the part that doesn't really work, because, in order to escape a piece of user input, you need to understand what it's meant to be. For example, a common (unsafe) idiom is:

sqlFormatString = "SELECT %s FROM %s WHERE %s;";
filterFormatString = "%s LIKE ?";
filterString = sprintf(filterFormatString, userInputColumnName);
sqlQuery = sprintf(sqlFormatString, columnName, tableName, filterString);
preparedQuery = dbImpl.prepare(sqlQuery, userInputValue);

Which of course does the right thing for userInputValue; may or may not be doing the right thing with columnName and tableName, depending on the source; and does the really wrong thing with userInputColumn.

Now, you could replace this with a type safe dbImpl.prepare(), which only takes a NotUserGeneratedSQL type. But now you need to have some way to build a NotUserGeneratedSQL.

One option is to go all in on an SQL AST library, where you could build the query above like

query = selectQuery();
query.columnNames = [ escapeColName(columnName) ];
query.from = escapeTableName(tableName);
queryFilter = equalityComparison();
queryFilter.left = escapeColName(userInputColumn);
queryFilter.right = preparedArg();
query.filter = queryFilter;

preparedQuery = dbImpl.prepare(query, userInputValue)

But this, as I said, gets tedious for the user and complex for the implementer.

However, I don't think there is any alternative that can actually enforce validation of user input in constructed queries. You can help users think about it by forcing them to use some kind of string -> NotUserGeneratedSQL function, but this function can't actually be written to reject the example I gave initially. A function that creates an empty minimal NotUserGeneratedSQL and offers other functions to build it up to a useful query will either accept strings (making it possible to introduce SQL injection) or have to accept structured SQL, making it fall into the AST problem.

Of course, the problem of NotUserGeneratedSQL is easy if you don't want to support any kind of dynamic query, other than prepared statements. But if you want to dynamically generate queries based on user input (e.g. user chooses which columns they want to see, which columns they want to filter by etc.), then I don't think it's possible to statically ensure that SQL injection is not possible without an ORM or SQL AST
API.

Edit: if you believe otherwise, please show me a type-safe, SQL-injection-safe library in any language that isn't an ORM/EDSL or a straight up SQL AST manipulator. I am quite certain none exists, but I would be happy to be proven wrong.