Dialects
Every SQL difference between the six supported engines is handled in one place: the dialect. It owns placeholders, identifier quoting, insert-ID behaviour, upsert syntax, and DDL — which is why the same model and query code runs everywhere.
You normally never choose one. Quark detects the dialect from the driver name at
quark.New. This page is the engine-by-engine reference for what that detection
gets you, and where an engine still behaves differently enough to matter.
| Dialect | Constructor | Notes |
|---|---|---|
| PostgreSQL | quark.PostgreSQL() | $N placeholders, RETURNING, ON CONFLICT. |
| MySQL | quark.MySQL() | AUTO_INCREMENT, LAST_INSERT_ID, ON DUPLICATE KEY UPDATE. |
| MariaDB | quark.MariaDB() | MySQL-compatible upsert behavior (ON DUPLICATE KEY UPDATE). Treated as a distinct dialect for DDL edge cases. |
| SQLite | quark.SQLite() | In-memory friendly, RETURNING where supported. |
| MSSQL | quark.MSSQL() | IDENTITY, OFFSET/FETCH, MERGE upserts. |
| Oracle | quark.Oracle() | GENERATED AS IDENTITY, MERGE upserts (Oracle 23ai or later, see below). |
MariaDB is detected automatically since v1.1.0. MariaDB and MySQL share the
"mysql" driver, so the driver name alone can't tell them apart. On a
quark.New("mysql", dsn) connection Quark probes SELECT VERSION() once at
construction and switches to the MariaDB dialect when the server reports MariaDB.
You get the MariaDB DDL edge cases without naming the constructor yourself.
Upsert mapping
user := User{Email: "alice@example.com", Name: "Alice Updated"}
err := quark.For[User](ctx, client).Upsert(
&user,
[]string{"email"},
[]string{"name", "active"},
)
| Dialect | Generated shape |
|---|---|
| PostgreSQL | ON CONFLICT (email) DO UPDATE SET ... |
| MySQL | ON DUPLICATE KEY UPDATE ... |
| MariaDB | ON DUPLICATE KEY UPDATE ... |
| SQLite | ON CONFLICT (email) DO UPDATE SET col = excluded.col |
| MSSQL | MERGE INTO ... USING ... WHEN MATCHED ... |
| Oracle | MERGE INTO ... USING ... WHEN MATCHED ... |
MySQL: statements per query
Given a query with arguments, go-sql-driver/mysql prepares it on the server,
executes it and closes it — three commands and two round trips, on every call,
unless the DSN sets interpolateParams=true. By default Quark keeps no
statement, so every Find pays all three. There are two ways to stop paying
them, and they do different things:
quark.WithStatementCache(n) | interpolateParams=true in the DSN | |
|---|---|---|
| What travels | the same prepared statement as without it; prepared once per connection, then one execute per call | one text query: the driver writes the arguments into the SQL |
| Applies to | the query builder's statements that carry arguments | every statement of the pool, raw SQL included |
| Server-side state | up to n statements per connection, counted against max_prepared_stmt_count (16382 per server by default) | none |
| Inside a transaction | each statement prepared once per transaction and reused in it | as outside |
| Behind a proxy that multiplexes sessions | keep it off: a prepared statement belongs to one session | works |
Measured on the engine bench (Benchmarks, controls MY-01 and
MY-02), FindByPK against mysql:8.4 over loopback, medians of ten rounds of
one run on each machine — the CI runner (AMD EPYC 7763, 4 vCPUs) and a laptop
(Apple M4 Pro, Docker Desktop's Linux VM):
| Arm | CI runner | laptop |
|---|---|---|
| Quark, default | 268.1 µs | 102.6 µs |
Quark, interpolateParams=true | 157.9 µs | 63.4 µs |
Quark, WithStatementCache(64) | 144.9 µs | 59.9 µs |
database/sql, statement prepared once and reused | 100.3 µs | 44.5 µs |
The cache and interpolateParams buy about the same. What is left over the
reused statement is Quark's own work on the query — building the SELECT,
scanning the row — not the protocol.
client, err := quark.New("mysql", dsn, quark.WithStatementCache(256))
Size the cache so that size × MaxOpenConns, summed over every instance of the
application, fits under the server's max_prepared_stmt_count. The option is
off by default: it changes how statements travel, and holds server-side state
an application should choose to hold. It also works on MariaDB, SQL Server and
SQLite. It is ignored on PostgreSQL, whose pgx driver keeps a statement cache
per connection already, and on Oracle: with go-ora v2.9.0, a prepared query
re-executed after other statements on the same connection answered from a
stale result — a count of 0 for a row that existed — and Quark relies on such
counts to tell a stale entity from an excluded one.
MySQL: CreateBatch keys and innodb_autoinc_lock_mode
CreateBatch with a generated key sends one multi-row INSERT per chunk on
MySQL only when the server runs innodb_autoinc_lock_mode 0 or 1, and one
INSERT per row on MySQL 8's default, 2. The reason, the other conditions and
the measurements are in
Batch Operations.
Oracle-specific behavior
Oracle differs from the other five engines in three ways worth knowing:
- Empty strings are NULL. Oracle stores
''asNULL, so an empty string written to aVARCHAR2/CLOBcolumn comes back asNULL. When scanning into a non-pointerstringfield, Quark coerces thatNULLto"", so empty strings round-trip consistently with the other engines. Use*stringorsql.Null[string]if you need to tellNULLapart from"". - JSON path is inlined.
WhereJSONbinds the JSON path as a parameter on every engine except Oracle, whoseJSON_VALUErejects a bound path (ORA-40454: path expression not a literal). On Oracle the path is inlined as a literal instead. It stays injection-safe because the path is validated against a strict[A-Za-z0-9_.]grammar before it reaches the SQL. UpsertandUpsertBatchneed Oracle 23ai. TheMERGEreads its source row from aSELECTwithoutFROMand, for a single integer key, reads the key back withMERGE … RETURNING. Oracle accepts both from 23ai on; 19c and 21c do not. Quark's Oracle lane runs Oracle Database 23ai Free, and no earlier release is tested. SeeUpsert.
Custom dialects
Custom or proprietary engines can be registered:
myDialect := &MyCustomDialect{}
quark.RegisterDialect("customdb", myDialect)
dialect, err := quark.DetectDialectByName("customdb")
if err != nil {
return err
}
client, err := quark.New("customdb", dsn, quark.WithDialect(dialect))
DetectDialectByName looks up a registered dialect by name and returns
(Dialect, error). Register from an init function when you can. The registry
is safe for concurrent use, so a registration made later — in a test, or by a
package loaded after start-up — does not race with clients being built.
Registering a name again replaces the dialect registered under it.
The dialect interface covers SQL generation, placeholder formatting, identifier quoting, and schema DDL.
The contract is declared in github.com/jcsvwinston/quark/quarkdriver: the
Dialect interface and the types it names, the optional interfaces Quark asks
a dialect, the schema model, ErrUnsupportedFeature and the registry
(quarkdriver.RegisterDialect). quark.Dialect, quark.RegisterDialect and
the other names in package quark are aliases of the same types, so the code
above is unchanged; a driver module writes and registers its dialect against
quarkdriver without importing package quark. See
Where the contract lives.
A dialect is checked against its engine by the conformance kit,
drivertest.VerifyDialect, and the engine as a whole by the engine suite,
suite.Run — the same checks the six built-in engines run. See
Checking a dialect, and
Writing a driver for a driver module built from
an empty module to one that passes both.