iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🖥️

Building a Lightweight DB Viewer with Rust + GPUI (DbMiru)

に公開2件

Hi there. I'm someone who's recently become hooked on Rust.

I was inspired after seeing the following article on Zenn recently:

https://zenn.dev/syuya2036/articles/7f02faaf8c8ec4

I tend to build "things that don't exist yet but that I personally want," but reading that article gave me a new perspective: "evolving existing tools into something that suits my needs."

I’ve always had a complaint:

pgAdmin takes too long to launch.

I just want to take a quick peek at a table... but using psql is also a hassle...

Driven by that laziness and the influence of the aforementioned blog, I decided, "Alright, I'll build a lightning-fast version of a pgAdmin-like app!"

And so, in about two weeks, I finished building the tool mentioned in the title (thanks, Codex).

Of course, it was all vibe coding. I’m still learning Rust, so I can’t keep up with understanding all the code generated by Professor Codex, and I spend my days copying code and pouring over documentation.

I hope my approach of Specification-Driven Development (SDD) and technology choices can serve as some kind of reference for others.

What you'll learn in this article

  • An alternative approach when you feel pgAdmin is heavy...
  • The philosophy behind building a "lightweight DB viewer" using Rust + gpui
  • A simple design for handling DBs without locking in the UI
  • How writing specifications before development makes personal projects easier

Introducing what I built

About DbMiru

The app is named "DbMiru" (which literally means "See DB" in Japanese).

The app screen looks like this:

What it can do

  • Create/edit/delete connection profiles (right side of the screen)
  • Connect to PostgreSQL
  • Execute SQL and view results in a table
  • Browse schemas / tables / columns (bottom right of the screen)
  • Table preview (SELECT * LIMIT)

I love the font used in Zed, so I bundled it to use in the app.
(The license is OFL, so I've included assets/fonts/OFL.txt.)

What kind of app is it? What did I leave out?

DbMiru isn't a "full-featured DB client" type of app, but is leaned toward being a tool that satisfies the need to "take a quick look" as fast as possible.

MVP (What I wanted to satisfy first)

  • Connection profile management (create/edit/delete)
  • Connect to DB and execute SQL
  • Display results in a table
  • Easy-to-understand error handling, no crashes

Things I intentionally don't do

  • Comprehensive management features like pgAdmin (role management, backups, monitoring, etc.)
  • ER diagrams / visual query builder
  • Trying to be an all-purpose tool that can do anything

Writing "don't be greedy" into the specifications significantly reduced the amount of times I got lost during implementation.

About the development environment

I'm not sure if this is useful, but I'll list it just in case.

sw_vers
# ProductName    : macOS
# ProductVersion : 26.1

cargo --version
# cargo 1.89.0

How I built it (Document structure for SDD)

For this Specification-Driven Development, I wanted a mechanism to remember "the background of my implementation so far" so that Professor Codex wouldn't get lost. I keep my docs in the following structure:

AGENTS.md
docs
 ├ milestones.md    // Background of conversations with Codex and implementation progress
 ├ ui.md            // UI definitions (roles and operations of screens)
 ├ architecture.md  // Architectural design (layers and data flow)
 ├ requirements.md  // Requirements specifications (what to do / what not to do)
 ├ status.md        // Current work status (tasks for today, blockers)

Doing this significantly reduces those moments of "I don't know where to continue" when opening the project the next day.

About the technology selection

Tech stack

  • Rust
  • gpui
  • PostgreSQL
  • codex (for implementation)
  • Async: tokio
  • PostgreSQL driver: tokio-postgres
  • Persistence: serde + JSON
  • Logs: tracing
  • Password: Saved in the OS keychain (via keyring)

Why I chose Rust

  • I wanted to build a lightning-fast native app
  • I wanted to build a tool that launches quickly
  • To learn Rust

Honestly, I'm still a beginner with Rust.
However, I value "learning while building even if I don't understand it yet."

DB clients naturally experience "failures" such as network errors and SQL errors.

I believe Rust is well-suited for a direction where we can "handle failures gracefully rather than panicking or freezing."

Why I chose gpui

Reasons for selection

  • It's used in the Zed editor
  • It seems lightweight and native
  • "I like the vibe"
  • The article at the beginning also used it

I believe choosing technology for tools that you yourself enjoy using is extremely important for personal projects!!

In reality, my understanding of Rust and gpui is shallow, and I often get stuck.

But because of that, I have had to think seriously about:

  • How to separate UI and state management
  • How to design asynchronous processes

...which, as a result, has been great learning.

Why I focused on "viewing only"

DbMiru is not a tool for "writing SQL aggressively."

The position I am aiming for

"A tool for quickly understanding the structure of a DB"

The reason is simple:

  • If I want to write SQL, it's faster to do it in an editor
  • But "grasping the structure" is heavy with existing tools
  • I just want to check something, but pgAdmin is slow to launch

I had these frustrations.

Future plans

  • Schemas
  • Tables
  • Columns
  • Relationships for Primary Key / Foreign Key (*planned for future enhancement)

I value being able to check such information quickly and easily.

However, in the future, I want to make it possible to perform update operations like INSERT "safely" as well.
(That's why I've already solidified the design for that now.)

Tips for architectural design

UI should not touch the DB directly

In DbMiru, I established this rule from the beginning:

The UI does not call DB driver APIs directly.

It is quietly effective.

If the UI touches the DB directly:

  • The UI freezes
  • Error handling gets scattered all over the screen
  • You end up having to touch the entire UI when adding more DB types in the future

...as it tends to result in a "poorly cultivated structure."

Layer structure

  • app: UI (gpui)
  • core: State and domain
  • db: DB adapter
  • storage: Persistence
app  →  core
app  →  db      → core
app  →  storage → core

Tips for not locking in the UI

Run the DB on a separate thread

  • UI → DBWorker : DbCommand
  • DBWorker → AppState : DbEvent

By moving towards a unidirectional flow of "Event → state → rendering," asynchronous processing becomes much easier.

Tips for result display

Don't pass DB Row to the UI directly

The Row returned by the DB driver is difficult to use in the UI as-is.

  • Types are diverse (bool / int / text / json / uuid, etc.)
  • NULL appears normally
  • Display rules are needed

So in DbMiru, I convert the DB results into a "UI-friendly format" before passing them on.

  • columns: Vec<String>
  • rows: Vec<Vec<String>> (each cell is a string)

By converting DB Row → table of strings:

  • The UI only needs to draw a table
  • You don't have to touch the UI even if you swap out the DB implementation
  • Display concerns can be encapsulated within the db layer

...there are these advantages.

Also, I don't display an infinite number of rows, but rather set an upper limit (e.g., 1000 rows).

I prioritize "the app doesn't freeze" over "I got 1 million rows."

A trick to avoid accidents in table previews

Quoting identifiers

In schema browser previews, SQL often breaks with:

  • Reserved words
  • Case sensitivity
  • Names containing special characters

Therefore, I wrap identifiers in double quotes and escape internal " before assembling the SQL.

"weird""name"

It's a practical workaround to ensure it doesn't break due to metadata-derived names, rather than a measure against SQL injection.

Security design

Don't save passwords

In DbMiru, I decided as a specification: "Do not save in plain text."

  • profiles.json only contains connection info + remember_password
  • The password itself is sent to the OS keychain (keyring)

This was a point I solidified early on to prevent passwords from leaking from configuration files or logs.

How to proceed with Specification-Driven Development (SDD)

What I was most conscious of in this development was Specification-Driven Development (SDD).

Before starting implementation, I clearly wrote down in Markdown:

  • What to make
  • When it's finished (DoD)
  • What to do next

Effects of milestones.md

  • Write Goal / Checklist / DoD for each milestone
  • When complete, change [ ] to [x]
  • Always have Codex read this file

Just by doing this, I was able to create a state where:

  • Implementation doesn't go off course
  • Decisions on whether something is done or not don't waver
  • I can resume work immediately the next day

How to get along with Codex

Things I'm conscious of to avoid loops

I try to ask Professor Codex:

"Proceed in a way that fulfills the DoD of this milestone."

Also, when I get stuck on the UI, etc.:

  • Don't make him fix it immediately
  • First, have him present a hypothesis for the cause
  • Have him ask questions if there are any points that are unclear

...I am conscious of that flow. I feel it's the same as human team development: if instructions are vague, the deliverables will also be vague.

In closing

DbMiru is still a work in progress.

But I am quite satisfied with the point that "It launches fast and I can check the contents of the DB immediately."

  • People who love speed
  • People who feel pgAdmin is heavy
  • People interested in Rust + UI

I would be happy if this article serves as some kind of reference for you.

Next, I plan to build a "viewing experience that traces the connections of Primary / Foreign Keys (mapping the DB)" to make it an even more "pleasing tool to look at."

https://github.com/KASAHARA-Kyohei/dbmiru

Discussion

shsh

引用ありがとうございます!!

1