From the Mohio team

The ssidi Problem

Ron Smith 6 min read

Every PHP developer has typed it. Nobody can explain why it has to exist. It doesn't.

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.

Ron Smith

Founder of Mohio and Particular LLC. Building the first AI-native programming language from Mooresville, NC.

Mohio founder

Questions about this topic

What is MoQL?
MoQL (Mohio Query Language) is Mohio's built-in query language. It handles everyday database operations without SQL ceremony — no type strings, no ? placeholders, no manual binding. Raw SQL is still available for anything MoQL doesn't cover.
Does MoQL replace SQL entirely?
No. MoQL handles the large majority of everyday queries. For complex joins or advanced aggregations, you use a raw sql block inside any retrieve statement. Both approaches bind values safely with no injection surface.
What exactly is the PHP bind_param ssidi problem?
In PHP's MySQLi, bind_param() requires a type string like 'ssidi' that maps each ? placeholder to a data type by hand. Add a column, change the order, or mistype one letter and you get a silent bug or a crash with no indication of which field is wrong.
How does Mohio prevent SQL injection?
Values in MoQL are bound by name at the compiler level, never concatenated into query strings. There is no injection surface because there are no string-built queries anywhere in the pipeline.
Can I use Mohio with my existing database?
Yes. Mohio connects to PostgreSQL, MySQL, and other standard databases via the connect db declaration. Your existing schema, tables, and data work as-is. You don't need to migrate anything to start using MoQL.
← All articles

Try it yourself

The compiler is open source. Clone the repo, run your first .mho file, and see what changes.