Open any PHP codebase that touches a database and you will find something like this:
$stmt = $conn->prepare(
"INSERT INTO members (name, email, age, tier, balance)
VALUES (?, ?, ?, ?, ?)"
);
$stmt->bind_param("ssidi", $name, $email, $age, $tier, $balance);
$stmt->execute();
Look at "ssidi".
That string is you telling the database the type of every single parameter (string, string, integer, double, integer) in the exact order of the ? marks. Every time. By hand. For every query.
Add a column. Reorder one. Fat-finger a d for an s. You get a silent bug or a runtime crash, and the type string tells you exactly nothing about which field is wrong. Because ssidi has no names in it. It's just letters, in a specific order, that have to match a list of question marks, that correspond to your actual fields, somewhere above.
This is not a SQL problem. SQL has no idea ssidi exists. This is a ceremony problem. The layer of ritual between your intent and the database that has nothing to do with what you actually want to do.
What a better answer looks like
Before you even think about the solution, think about what the operation actually is.
You are saving a member. You have five pieces of data. You want them in the database. That's the whole thing.
A better answer looks like this:
save to db.members
name request.name
email request.email
age request.age
tier request.tier
balance request.balance
save: done
No placeholders. No type string. No order to keep synchronized. Fields are bound by name. Mohio knows the types. There is no ssidi because it doesn't need to exist.
Read it out loud. "Save to the members table. Name is the request name. Email is the request email..." You're done in one pass, top to bottom, and a non-developer walking by can follow it without needing a comment that explains what "ssidi" means.
And what about save-or-update?
Here's where SQL really earns its reputation for ceremony. You need to insert a record if it doesn't exist, or update it if it does. In MySQL:
INSERT INTO members (id, name, email, tier)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
email = VALUES(email),
tier = VALUES(tier)
You listed the fields twice. You wrote VALUES(name) which is its own special syntax that exists only inside ON DUPLICATE KEY UPDATE. You still have the type string. And if you add a column, you update it in three places.
In Mohio:
upsert db.members
match id to request.member_id
name request.name
email request.email
tier request.tier
upsert: done
match tells Mohio what the uniqueness constraint is. The fields below are what gets written whether it inserts or updates. One word. One job. The word tells you exactly what it does.
Compound keys work the same way. No new syntax:
upsert db.sessions
match session_id to player_session
match room to current_room
response narrator
created_at now()
upsert: done
Both match lines form the uniqueness constraint together. The rest are the fields.
The full picture, before we go further
At this point it's worth stepping back. Here is every database operation Mohio has:
save to db.members // insert
update db.members // update
upsert db.members // insert or update
retrieve member from // single record read
find members in // collection read
remove from db.members // delete
Six operations. Six words. Each one tells you what it does on the first read. That's the whole CRUD surface. Walk-By Test passes on all of them.
Now let's look at the read side.
Same story for reads
The ceremony doesn't stop at writing. Here's the simplest possible read in SQL:
SELECT * FROM members WHERE id = '123' LIMIT 1
And in Mohio:
retrieve member from db.members
match id to request.member_id
retrieve: done
match ... to is equality. No quoting, no escaping, no injection surface. The value is bound by name, not concatenated into a string. The retrieve: done closer tells you exactly what just ended. Named closers are not ceremony. They're the opposite. They're the thing that lets you scan 200 lines of logic and always know where you are.
The part where people say "but what about real queries"
Fair. Here's a real one. Members with their transaction counts and averages, active only, from the last 30 days, with at least five transactions, sorted by average amount, top twenty.
SQL:
SELECT m.id, m.name,
COUNT(t.id) AS transaction_count,
AVG(t.amount) AS avg_amount
FROM members m
LEFT JOIN transactions t ON t.member_id = m.id
WHERE m.status = 'active'
AND t.created_at > NOW() - INTERVAL '30 days'
GROUP BY m.id, m.name
HAVING COUNT(t.id) > 5
ORDER BY avg_amount DESC
LIMIT 20
Mohio:
retrieve results from db.members
match status to "active"
and retrieve from db.transactions
match member_id to member.id
where created_at above now() - 30 days
return member.id, member.name,
transaction.count as transaction_count,
transaction.amount.average as avg_amount
where transaction.count above 5
order by avg_amount descending
up to 20
retrieve: done
Read it out loud: "Retrieve results from members. Match the active ones. Retrieve their transactions from the last 30 days. Return their name and counts. Only those with more than five. Order by average amount. Top twenty."
That is the query, in the order you would actually explain it to a person. No GROUP BY that has to mirror your SELECT column for column. No m and t aliases to track across eighty characters of joins. No HAVING versus WHERE distinction that exists for historical reasons nobody in the room can remember.
Mohio is more verbose here. That's intentional. More words means more readable. More readable means harder to get wrong, easier to review, and easier for the next person (or the next version of you, six months from now) to understand on the first pass.
When you need raw SQL, it's right there
Mohio doesn't trap you. For anything MoQL doesn't cover, or when you just prefer SQL for a specific query, drop in a sql block:
retrieve gold_members from db
sql
SELECT m.*, COUNT(o.id) AS order_count
FROM members m
LEFT JOIN orders o ON o.member_id = m.id
WHERE m.tier = 'gold'
GROUP BY m.id
HAVING COUNT(o.id) > 10
sql: done
retrieve: done
Explicit. Honest. Still bound safely inside the retrieve context. SQL stays a tool you reach for when you need it, not a string you babysit on every query.
MoQL handles the overwhelming majority of what most applications actually do. The advanced join and aggregation edge cases that aren't worth rewriting? That's what the sql block is for. The goal was never to replace SQL. The goal was to eliminate the ceremony around it for the 95% of queries that are actually simple questions dressed up in complicated clothes.
The thing underneath all of this
ssidi is not a quirk of PHP. It's a symptom of what happens when the language doesn't understand what you're doing. PHP is moving text around. It has no idea you're talking to a database, so you have to translate manually, type-string by type-string, placeholder by placeholder.
Mohio knows. The database connection is declared once. The field names are real. The types are inferred. The binding is handled. You describe what you want, and it executes the reason.
That's the difference between writing code and writing intent.