In any sufficiently complex application, something like a query builder that allows different parts of the application to work together to lazily compose queries (vs combining/filtering/sorting the data in memory) will be created at some point.

ORM is a tool that can make this easier, it's also a tool that can make it easier to shoot yourself in the foot (ie by making it easy to create N+1 queries without knowing). Like all tools, there are tradeoffs that need to be accounted for together with the actual use case to make a decision. Operating on SQL statement strings is not something I'd recommend in any case.

There are libraries that flip the concept of an ORM on its head. Instead of a library that allows lazy query composition, you write your queries and the library generates the code for that query at compile time. It’s a much better model in my opinion.

E.g. you could write a query like this:

    getUser:
    SELECT * FROM users WHERE id = ?
And the library would generate a class like:

    class GetUserQuery {
        static getUser(id: String): GetUserQueryResult
    }
Some examples for anyone else reading:

Go: https://github.com/kyleconroy/sqlc

Rust: https://github.com/cornucopia-rs/cornucopia

This is my preferred method of interacting with databases now, if available.

Very flexible.