← Back to Posts

I tried writing a database query in my own programming language

9/20/2026

I tried writing a database query in my own programming language

If you've been reading my posts lately, you've probably noticed I haven't talked about Strata in a while.

Between building Lunaris (my local AI coding agent) and experimenting with autonomous browser workflows, Strata has been quietly sitting on the backburner since around February when I was messing with GTK bindings.

This morning, I decided to open up the Strata codebase and write some good old database code. Nothing fancy, just connecting to MySQL using PHP's native PDO driver, executing a query, and printing some rows as JSON.

Here is what I wanted to write:

import PDO;
import PDOException;

type DBConfig = {
    host: String,
    db_name: String,
    username: String,
    password: String?,
    options: Array<Mixed>
};

fn getConfig(): DBConfig {
    return DBConfig(
        host: "localhost:3306",
        db_name: "people",
        username: "root",
        password: "",
        options: [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ]
    );
}

fn getDB(): Result<PDO, PDOException> {
    let config = getConfig();

    try {
        let pdo = PDO(
            dsn: "mysql:host=${config.host};dbname=${config.db_name};charset=utf8mb4",
            username: config.username,
            password: config.password,
            options: config.options
        );

        return Ok(pdo);
    } catch (e: PDOException) {
        return Err(e);
    }
}

fn main(): Void {
    let db = getDB();

    if db is Ok {
        let stmt = db.unwrap().query(query: "SELECT name, email FROM users WHERE id = 1");

        let json = json_encode(value: stmt.fetch(), flags: JSON_PRETTY_PRINT);
        print(json);
    } else {
        print(db.error.getMessage());
    }
}

Fifty lines of clean, strictly-typed code. I hit save, ran strata index.str, and expected to see a user record.

Instead, the terminal blew up:

✗ Error: Static property or constant 'ATTR_ERRMODE' not found in class 'PDO'
✗ Error: Static property or constant 'ERRMODE_EXCEPTION' not found in class 'PDO'
✗ Error: Static property or constant 'FETCH_ASSOC' not found in class 'PDO'

And that was just the opening act. Over the next hour, I ran into four distinct walls, not in PHP, but inside Strata's own compiler.


1. The "Foreign Class" Blind Spot

In Strata, we have a phpInterop flag in .strata.json. It's designed to introspect large vendor packages and Laravel models when you want LSP support for dynamic magic methods.

Because this script was running as a standalone file without phpInterop: true explicitly configured, PhpTypeResolver was skipping native PHP class reflection entirely.

Even though PHP's PDO extension was active in the background runtime, Strata's semantic analyzer said: “You didn't turn on interop, so I refuse to acknowledge that PDO::ATTR_ERRMODE exists.”

The fix: native, loaded PHP core extensions should always be accessible. We updated PhpTypeResolver so that if class_exists($name, false) or defined("$name::$const") resolves directly in PHP's active process, the compiler trusts it without needing a configuration file.


2. The Array Invariance Trap

Next error:

✗ Error: Type mismatch: expected Array<Mixed>, got Array<Int: Mixed>

In the config function, I had:

options: [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
]

Under the hood, Strata's type checker was treating Array<Key, Value> as strictly invariant. If a shape or struct requested Array<Mixed>, passing an associative map like Array<Int, Mixed> was rejected.

Worse, the element checker had a bug where it only validated the first key-value pair of an array literal and silently discarded subsequent entries during validation.

We added proper covariance checks so that specific dictionary and array shapes can widen to Array<Mixed> cleanly, and patched the parser to accept standard comma separators in generic declarations (Array<Key, Value> alongside Array<Key: Value>).


3. The Curious Case of PHP 8.1+ Tentative Return Types

Once the connection was established, the next line crashed during type checking:

let stmt = db.unwrap().query(query: "SELECT name, email FROM users WHERE id = 1");
let user = stmt.fetch(); // ✗ Error: Call target is not a function or callable object. Got Mixed

Why did Strata think stmt was Mixed?

If you inspect PDO::query in PHP using reflection:

$r = new ReflectionMethod('PDO', 'query');
var_dump($r->getReturnType()); // NULL!

In PHP 8.1+, internal methods don't populate getReturnType() if their types were added tentatively for backward compatibility. Instead, PHP provides getTentativeReturnType(), which returns PDOStatement|false.

Because Strata only queried getReturnType(), it gave up and tagged the return type as Mixed. And because calling methods on raw untyped expressions was guarded, .fetch() failed.

By updating the type resolver to check getTentativeReturnType(), Strata correctly identified stmt as PDOStatement|false. We also taught union resolution to inspect the valid object members within a union type so calling .fetch() resolves directly to the method on PDOStatement.


4. When Codegen Tries to Be Too Clever

The final bug was the most deceptive because it passed compilation and only blew up at runtime.

When compiling getDB():

try {
    let pdo = PDO(...);
    return Ok(pdo);
} catch (e: PDOException) {
    return Err(e);
}

At runtime, calling getDB() returned a raw \PDO instance instead of Result<PDO, PDOException>.

Because $db was an actual \PDO object, the check if db is Ok ($db instanceof Ok) evaluated to false. Execution fell straight into the else branch:

print(db.error.getMessage());

Which promptly died with a PHP fatal error: Cannot access property PDO::$error.

Why did this happen? Inside PhpEmitter.php, there was a helper named maybeUnwrapResult(). It assumed that if an Ok(...) constructor was returned from inside a try block, the developer had already handled the error and wanted the inner value unpacked.

It was a classic case of an emitter trying to be smarter than the programmer. We removed that automatic unwrapping, if you write return Ok(pdo), you get Ok(pdo). Period.


The Takeaway

After patching these compiler quirks and running the file:

$ strata index.str
{
    "name": "Donald Pakkies",
    "email": "donald@example.com"
}

Zero warnings. Zero errors. Pure, clean execution.

Writing a compiler is full of friction. You can write all the synthetic unit tests you want in isolation, but the moment you sit down to write a real program, connecting to a real database, parsing real arrays, using real runtime reflection, theory meets reality very quickly.

Strata might have been quiet for a few months, but every time I return to it, the language gets sharper.