Building 7 Database Drivers — The Compatibility Illusion and Its Discontents
There's a comforting lie that the database industry tells: SQL is portable. The reality, as anyone who has tried to support more than one database knows, is that SQL is portable in the same way that English is portable — you can speak it in London, New York, and Mumbai, but the dialects will trip you up in different ways each time.
Neat ORM supports seven databases: MySQL, MariaDB, PostgreSQL, SQLite, SQL Server, Turso, and Oracle. Building this support was not seven independent efforts. It was a single effort to manage a compatibility matrix — a grid of features × databases × edge cases that grows combinatorially as you add each new driver.
Let me walk you through the matrix.
The Placeholder Babel
The most basic operation in a query builder is parameter binding. You write WHERE name = ? and pass ["John"] as the argument. The database replaces the placeholder with the value.
Except each database uses a different placeholder syntax:
| Database | Placeholder | Style |
|---|---|---|
| MySQL, MariaDB, SQLite, Turso | ? |
Positional |
| PostgreSQL | $1, $2 |
Numbered |
| SQL Server | @p1, @p2 |
Named |
| Oracle | :1, :2 |
Numbered colon |
This is not a minor difference. It affects how you build queries, how you track parameter order, and how you generate SQL for logging and debugging. Neat handles this by maintaining an internal clause representation that's driver-agnostic, then translating to the correct placeholder format at execution time.
This means you write the same query regardless of database:
db.Query().Where("name", "John").Where("age", ">", 30).Get(&users)
Under the hood, MySQL sees WHERE name = ? AND age > ? with args ["John", 30]. PostgreSQL sees WHERE name = $1 AND age > $2. SQL Server sees WHERE name = @p1 AND age > @p2. Oracle sees WHERE name = :1 AND age > :2. Same query, four representations, zero developer friction.
The interesting insight is that these four styles represent three different philosophies: positional (MySQL/SQLite), numbered (PostgreSQL/Oracle), and named (SQL Server). Each philosophy has trade-offs — positional is simplest but can't reorder, numbered allows reference reuse, named is most readable but verbose. Neat abstracts over all of them, which means the query builder never has to commit to a philosophy.
The Concurrency Spectrum
Databases live on a spectrum from "single writer" to "unlimited concurrent writers." Where a database sits on this spectrum determines its connection pool requirements.
SQLite sits at the extreme left: one writer, period. If you try to write from two connections simultaneously, you get database is locked. This is not a limitation to work around — it's a fundamental property of file-based databases. Neat enforces MaxOpenConns=1 for SQLite and Turso (which is SQLite at the edge), serializing all queries through a single connection.
MySQL, MariaDB, PostgreSQL, SQL Server, and Oracle sit toward the right. They're server-based, designed for concurrent connections, and benefit from larger pools. Neat defaults to MaxOpenConns=25, MaxIdleConns=5 for these.
But the spectrum isn't binary. Within the "concurrent" group, there are gradations:
- PostgreSQL handles high concurrency best — its MVCC implementation is particularly efficient.
- MySQL/MariaDB are good but can struggle with very high connection counts (thread-per-connection model).
- SQL Server is solid but has licensing constraints on connection counts.
- Oracle is enterprise-grade but complex to tune.
Neat provides workload-specific recommendations (read-heavy, write-heavy, high-concurrency, low-traffic) because the optimal pool size depends on both the database and the workload.
| Database | MaxOpen | MaxIdle | Why |
|---|---|---|---|
| MySQL | 25 | 5 | Handles concurrent connections well |
| MariaDB | 25 | 5 | Same as MySQL |
| PostgreSQL | 25 | 5 | Excellent concurrency |
| SQLite | 1 | 1 | Single writer, prevent contention |
| SQL Server | 25 | 5 | Similar to MySQL |
| Turso | 1 | 1 | SQLite-based |
| Oracle | 25 | 5 | Enterprise-grade |
The Timestamp Wars
Time is simple until you cross database boundaries. Then it becomes a battlefield.
MySQL returns time.Time for temporal columns, but only if you add parseTime=true to the DSN. Without it, you get strings. Neat automatically appends this parameter.
PostgreSQL always returns time.Time. No configuration needed. The driver handles it natively.
SQLite stores timestamps as text. Always. There's no native temporal type. You get strings back, and you need to convert them.
When you write db.Query().Where("created_at", ">", someTime), the someTime value needs different treatment depending on the driver:
- MySQL/PostgreSQL: pass
time.Timedirectly to the driver - SQLite: convert to UTC string
2006-01-02 15:04:05
Neat's solution is driver-aware conversion: time.Time values are converted to strings only for SQLite, passed through for everything else. This seems obvious in retrospect, but the first implementation converted everything to strings, which broke PostgreSQL's native time handling.
The lesson: abstractions leak, and the leaking is different for each database. You can't pick one strategy and apply it universally. You need per-driver behavior.
The DSN Zoo
A DSN (Data Source Name) is a connection string. Each database has its own format:
mysql://user:pass@host:3306/db?parseTime=true
postgres://user:pass@host:5432/db?sslmode=disable
sqlite:///path/to/db.db
sqlserver://user:pass@host:1433?database=db
oracle://user:pass@host:1521/service_name
Neat parses all of these into a common ConnectionConfig struct with fields for Driver, Host, Port, Database, Username, Password, Charset, Schema, SSLMode, Timezone, Prefix, and read/write replicas.
Port defaults are driver-specific: MySQL=3306, PostgreSQL=5432, SQL Server=1433, Oracle=1521. SSL mode is relevant for MySQL, PostgreSQL, and SQL Server, but not for SQLite. Charset matters for MySQL but not for SQLite.
For Turso, Neat includes a dedicated DSN builder that handles Turso's edge-specific connection parameters.
The common struct hides these differences, but the DSN parser has to know which fields are relevant for each driver. This is where the compatibility matrix gets dense — every field × every driver = a cell that might need special handling.
MariaDB: The Fork in the Road
MariaDB is a MySQL fork. Wire-compatible. Same Go driver. But forks diverge over time, and MariaDB has:
- Different default storage engine (InnoDB vs XtraDB)
- Different collation behavior
- Different JSON function syntax
- Different sequence and column-store features
Neat treats MariaDB as MySQL-compatible (same driver, same placeholder format) but allows the config to specify mariadb for future driver-specific optimizations. This is a pragmatic choice: treat them as the same until they're not, then branch.
Security: 7 Ways to Leak
Each database has its own identifier quoting rules:
- MySQL/MariaDB: Backticks —
`column` - PostgreSQL: Double quotes —
"column" - SQL Server: Square brackets —
[column] - SQLite: Double quotes or backticks —
"column" - Oracle: Double quotes —
"column"
Neat's quoteIdentifier function handles each format and escapes embedded quote characters by doubling them. This is the first line of defense against SQL injection.
Beyond quoting, Neat includes:
- DSN redaction: Credentials are masked in logs (
user:***@host) - Error sanitization: Database errors can contain table names and column values — Neat sanitizes them before returning
- Query binding protection: Bindings are only logged when debug mode is enabled
Testing the Matrix
How do you test 7 database drivers? You need 7 databases running.
Neat uses Docker Compose with MySQL 8.0 and PostgreSQL 15:
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: neat_test
postgres:
image: postgres:15
environment:
POSTGRES_PASSWORD: test
POSTGRES_DB: neat_test
SQLite and Turso are file-based — no server needed. SQL Server and Oracle have Docker images. The test suite runs the same cases against each database. Where driver-specific behavior differs (placeholder format, timestamp handling, identifier quoting), the tests include driver-specific expectations.
This is the unglamorous infrastructure that makes multi-database support real. Without it, you're just hoping it works. With it, you know it works.
The Meta-Pattern
Building 7 database drivers taught me a pattern I'll call abstract the interface, specialize the implementation. The query builder maintains a driver-agnostic internal representation. Each driver specializes the translation to SQL. The schema builder produces a driver-agnostic column definition. Each driver specializes the DDL generation.
This is a pattern Neat uses throughout: the contract is "here's a query, execute it." The implementation is "here's how MySQL/PostgreSQL/SQLite/etc. executes that query."
The compatibility matrix is dense, but the architecture keeps it manageable. Each driver is a self-contained implementation of a common interface. Adding an eighth database would mean writing one new driver, not modifying 50 files.
That's the power of the contract pattern. It turns a combinatorial problem into a linear one.




