Luma Talks to Databases: The Full Stack Lands
Four blog posts ago, we asked a question: how should Luma talk to MySQL? We had no TCP sockets. No binary buffers. No crypto. No database interface. We had a vision and a plan — build the toolbox first, then build a driver with those tools.
Today, every piece of that plan is done. Luma has a complete database stack — from raw TCP sockets all the way up to conn.query() with named parameters, transactions, and debug output. And the MySQL driver that ties it all together is written entirely in Luma. Not in Go, not in C, not hidden inside the compiler. In Luma. About 800 lines of it.
This is a big moment.
What we shipped
Let’s be specific about what landed:
The db module — a built-in database interface that separates application code from driver internals. Your application calls conn.query(), conn.exec(), conn.close(). The db module handles named parameter binding, result wrapping, transaction state, and debug SQL output. It doesn’t know MySQL from PostgreSQL. It doesn’t need to.
The MySQL driver — a .luma file that implements the MySQL wire protocol from scratch. Connection handshake. mysql_native_password authentication. caching_sha2_password authentication (both fast-auth and full-auth paths). AuthSwitchRequest handling. COM_QUERY text protocol. DSN parsing. String escaping. It connects to MySQL and MariaDB, authenticates, sends queries, parses results, and closes cleanly.
Named parameters — :name syntax instead of positional ? placeholders. Pass a map, get safe, escaped SQL. Strings are quoted, numbers are bare, nil becomes NULL, booleans become 1 or 0.
Transactions — conn.beginTransaction(), conn.commit(), conn.rollback(), conn.inTransaction(). State is tracked by the db module, so calling beginTransaction() twice gives you a clear error instead of a puzzling SQL failure.
Debug output — conn.debug() returns the actual SQL that was sent to the database, with all parameters already substituted. When a query doesn’t return what you expect, this shows you exactly what the database received.
Result iteration — rows.walk(), rows.count(), rows.first(), rows.all(). Type-dispatched row.get() that returns str, int, float, or bool based on the declared type of the receiving variable.
What it looks like
mysql = import "mysql"
conn = db.open(mysql, "root:secret@localhost:3306/myapp")
conn.exec("INSERT INTO users (name, age) VALUES (:name, :age)", {
"name": "Alice",
"age": 30
})
rows = conn.query("SELECT name, age FROM users WHERE age > :min", {
"min": 20
})
rows.walk(row) -> {
name: str = row.get("name")
age: int = row.get("age")
print("${name} is ${age}")
}
conn.close()Read that code. Now read it again. There’s no ceremony. No connection factory, no statement builder, no result mapper, no error callback chain. Import a driver. Open a connection. Write queries. Read results. Close.
If you’ve ever set up database access in Java, you know what 30 lines of imports, try-catch blocks, and prepared statement boilerplate looks like. If you’ve used Go’s database/sql, you know the rows.Scan(&name, &age) dance with pointer arguments and deferred closes. If you’ve used Python, you know the cursor abstraction and the fetchall() pattern.
Luma’s approach is simpler than all of them. Not because it does less — it does named parameters, type-safe result access, transactions, and debug output. It’s simpler because the language’s design principles remove the noise. Duck-typed dispatch means one row.get() method handles every type. The variable declaration tells Luma what format you want. No generics, no type parameters, no casting.
Why we took the long road
We could have shipped database support months ago. The fastest path would have been embedding an existing Go MySQL driver — go-sql-driver/mysql is excellent, battle-tested, and would have worked immediately inside Luma’s compiled output.
We didn’t do that. Here’s why.
We wanted drivers to be Luma code. Not Go code hiding in .lcore files. Not a black box that users consume but can never read. When someone asks “how does the MySQL driver work?”, the answer should be “open mysql.luma and read it.” The driver uses tcp.connect() to open a socket, buffer() to parse packets, crypto.sha1() to hash passwords, and bit.and() to check capability flags. It’s all there. No magic.
We wanted anyone to be able to write a driver. If PostgreSQL support matters to you, you shouldn’t need to understand Luma’s compiler internals or write Go code. You need to understand the PostgreSQL wire protocol and know how to use tcp, buffer, and crypto. The same tools the MySQL driver uses. The same tools available to every Luma developer.
We wanted zero external dependencies. Luma compiles your .luma source to Go, then to a native binary. The binary uses only Go’s standard library plus Luma’s embedded runtime. No go.mod, no package fetching, no build system. Embedding a third-party Go driver would have introduced a dependency with a different license (MPL-2.0), a maintenance burden for upstream patches, and roughly 6,000 lines of code that isn’t ours. Instead, the MySQL driver is MIT-licensed Luma code, maintained by us, readable by everyone.
We wanted the tools to exist independently. TCP sockets aren’t just for databases — they’re for any network protocol. Binary buffers aren’t just for MySQL packets — they’re for any binary format. Crypto hashing isn’t just for authentication — it’s for checksums, tokens, signatures. By building the primitives first, we didn’t just get a MySQL driver. We got a networking toolkit that makes Luma useful for an entire category of programs that didn’t exist before.
What we avoided
There were shortcuts we deliberately didn’t take, and we think the decisions are worth explaining.
No ORM. We’re not building an object-relational mapper. ORMs add complexity, obscure what SQL actually runs, and create a second mental model on top of the database. Luma gives you conn.query() and conn.exec() with named parameters. You write SQL. You know what runs. If you want a query builder or model layer someday, that’s a module someone can write — but it won’t be baked into the language.
No connection pooling. Not yet. A single connection with explicit open and close is the right starting point. It’s predictable, debuggable, and sufficient for the programs most people write first. Pooling is a feature for high-concurrency servers, and it can be added later without changing the application API.
No prepared statements. The MySQL text protocol (COM_QUERY) handles everything we need. Named parameters are bound and escaped by the db module before the SQL reaches the driver. This is simpler, more debuggable (you can see the exact SQL with conn.debug()), and avoids the complexity of statement lifecycle management. Binary protocol support can come later if performance demands it.
No global driver registry. You don’t “register” a driver with Luma. You import a module and pass it to db.open(). The connection between your application and your driver is explicit — a function call, not a side effect. This makes programs easier to read and removes an entire category of initialization-order bugs.
The developer experience we wanted
We think about database access from the perspective of someone who just wants to store and retrieve data. Not someone who wants to configure a connection pool. Not someone who wants to learn an ORM’s query DSL. Someone who has a table and wants to read from it.
For that person, the experience should be:
- Download the driver file
- Import it
- Open a connection
- Write queries
- Read results
Five steps. No package manager. No configuration file. No build tool. Just a .luma file in your project directory and an import statement.
That’s what we built. And we think it’s the simplest database experience in any modern language.
The full toolbox
Building the database stack meant building four foundational modules along the way. Here’s everything that shipped:
| Module | What it does | Lines of .lcore runtime |
|---|---|---|
buffer |
Build and parse binary data — duck-typed read/write, cursor tracking, bounds checking | 13 files |
crypto |
SHA1/SHA256/SHA512, HMAC, secure random, XOR, constant-time comparison, hex/base64 encoding | 32 files |
tcp |
Socket connections with TLS, timeouts, STARTTLS upgrade, duck-typed read/write | 13 files |
db |
Named parameters, result wrapping, transactions, debug output, driver dispatch | Built-in |
These modules are useful far beyond databases. tcp lets you write any network client. buffer lets you parse any binary format. crypto lets you hash, sign, and verify anything. They’re general-purpose tools that happened to be built for a specific goal — and that’s exactly how good infrastructure should work.
What’s next
The MySQL driver is the first. It won’t be the last.
The architecture is designed so that every new database is just a new .luma file. PostgreSQL has a different wire protocol, but it uses the same primitives — TCP for transport, buffers for packet parsing, crypto for authentication. Someone who reads mysql.luma can understand how a driver works and write one for PostgreSQL, SQLite, Redis, or anything else that speaks over a socket.
We’re not setting timelines for additional drivers. But the path is clear, the tools are built, and the contract is documented. If you want to write a driver, everything you need is in the Writing a Driver guide.
The story so far
Look at what Luma can do today:
- Serve HTTP with routing, path parameters, and JSON responses
- Read and write files
- Parse and generate JSON
- Connect to MySQL and MariaDB
- Query with named parameters and transactions
- Open TCP sockets to any service
- Build and parse binary protocols
- Hash, sign, and verify with SHA1/SHA256/SHA512/HMAC
- Import modules for clean code organization
- Handle dates, times, and command-line arguments
A few months ago, Luma was a language with interesting syntax and a working compiler. Today, you can build a web application backed by a real database. That’s not an incremental step. That’s a category change.
We started this journey by saying we’d build the tools first. The tools would build everything else. That turned out to be true — and the database stack is the proof.