True, you can but it results in a more limited DSL. This is mainly due to variable names needing to exist, be a valid object and chained used method invocations, which results in more awkward expressions (IMHO). Importantly macros enable compile time checking of a DSL rather than only runtime checking. That alone is very powerful.
That's not bad, but compare it to a similar query in Elixir's Ecto:
from account in App.Account,
left_join: t0 in App.Transaction,
on: t0.account_id == account.id
and t0.user_id == ^current_user.id
and t0.deleted == false
and t0.type == "inflow",
where: account.id == ^id and t1.user_id == t0.user_id,
group_by: account.id,
select: {account, {sum(t0.amount), sum(t1.amount)}})
The Elixir DSL allows a bit more flexibility when re-using language built-in's such as `and`/`or`, or undefined ones likes `sum` without needing the hacked names like `and_` or `db.func.sum` as in Python (or Java or others OOs). Generally the formatting is easier with macro based DSL's.
P.S. The examples might not be exactly correct as I cobbled a couple of different ones together.
Some statically typed languages allow you to build up composable query snippets along the lines of (from a Scala DSL):
val accountQ = for {
(a, t) <- Account leftJoin Transaction (_.id is _.accountId)
if a.id is ... && t.userId ...
groupBy a.id
} yield (a, t.id, t.amount.sum, ...)
val invoiceQ = for {
(a, transId, total) <- accountQ
i <- Invoice join (_.transId is transId)
} yield (a, i, total)
Basically you can build up arbitrarily complex queries, all compile time checked -- it's really quite wonderful. Haskell has something similar with Esqueleto and C#/F# has LINQ to SQL.
I love query DSLs, the more we move away from string-y SQL the better :)