Skip to main content
Version: 1.17.0

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.

DialectConstructorNotes
PostgreSQLquark.PostgreSQL()$N placeholders, RETURNING, ON CONFLICT.
MySQLquark.MySQL()AUTO_INCREMENT, LAST_INSERT_ID, ON DUPLICATE KEY UPDATE.
MariaDBquark.MariaDB()MySQL-compatible upsert behavior (ON DUPLICATE KEY UPDATE). Treated as a distinct dialect for DDL edge cases.
SQLitequark.SQLite()In-memory friendly, RETURNING where supported.
MSSQLquark.MSSQL()IDENTITY, OFFSET/FETCH, MERGE upserts.
Oraclequark.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"},
)
DialectGenerated shape
PostgreSQLON CONFLICT (email) DO UPDATE SET ...
MySQLON DUPLICATE KEY UPDATE ...
MariaDBON DUPLICATE KEY UPDATE ...
SQLiteON CONFLICT (email) DO UPDATE SET col = excluded.col
MSSQLMERGE INTO ... USING ... WHEN MATCHED ...
OracleMERGE 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 travelsthe same prepared statement as without it; prepared once per connection, then one execute per callone text query: the driver writes the arguments into the SQL
Applies tothe query builder's statements that carry argumentsevery statement of the pool, raw SQL included
Server-side stateup to n statements per connection, counted against max_prepared_stmt_count (16382 per server by default)none
Inside a transactioneach statement prepared once per transaction and reused in itas outside
Behind a proxy that multiplexes sessionskeep it off: a prepared statement belongs to one sessionworks

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):

ArmCI runnerlaptop
Quark, default268.1 µs102.6 µs
Quark, interpolateParams=true157.9 µs63.4 µs
Quark, WithStatementCache(64)144.9 µs59.9 µs
database/sql, statement prepared once and reused100.3 µs44.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 '' as NULL, so an empty string written to a VARCHAR2/CLOB column comes back as NULL. When scanning into a non-pointer string field, Quark coerces that NULL to "", so empty strings round-trip consistently with the other engines. Use *string or sql.Null[string] if you need to tell NULL apart from "".
  • JSON path is inlined. WhereJSON binds the JSON path as a parameter on every engine except Oracle, whose JSON_VALUE rejects 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.
  • Upsert and UpsertBatch need Oracle 23ai. The MERGE reads its source row from a SELECT without FROM and, for a single integer key, reads the key back with MERGE … 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. See Upsert.

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.