Dialects API Reference
A dialect is the one place a database's quirks live: placeholder syntax, identifier quoting, upsert form, pagination, and DDL. Quark picks one for you from the driver name, so most applications never touch this API.
You need it in two cases — overriding a mis-detected dialect, and registering a dialect of your own. See Dialects for the engine-by-engine support table, and the extension contract for every interface of the dialect contract with its stability and what Quark promises about its shape.
Built-In Constructors
postgres := quark.PostgreSQL()
mysql := quark.MySQL()
mariadb := quark.MariaDB()
sqlite := quark.SQLite()
mssql := quark.MSSQL()
oracle := quark.Oracle()
| Constructor | Name() |
|---|---|
quark.PostgreSQL() | postgres |
quark.MySQL() | mysql |
quark.MariaDB() | mariadb |
quark.SQLite() | sqlite |
quark.MSSQL() | mssql |
quark.Oracle() | oracle |
Interface
The interface is declared in github.com/jcsvwinston/quark/quarkdriver, the
package a driver module imports, and its methods are documented there.
quark.Dialect is an alias of the same type: a dialect written against either
name is the other, so an application keeps writing
quark.WithDialect(quark.PostgreSQL()) and nothing changes for it.
type Dialect interface {
Name() string
Placeholder(index int) string
Quote(identifier string) string
Placeholders(n int) []string
LimitOffset(limit, offset int) string
SupportsReturning() bool
Returning(columns ...string) string
SupportsLastInsertID() bool
LastInsertIDQuery(table, pkColumn string) string
CurrentTimestamp() string
BuildRoutineQuery(routine string, argCount int) string
BuildProcedureCall(procedure string, argCount int) string
JSONExtract(column, path string) (sql string, args []any, err error)
AlterTableAddColumn(table, column, dataType string) string
AlterTableDropColumn(table, column string) string
AlterTableAlterColumn(table, column, newDataType string) string
RenameColumn(table, oldName, newName string) string
RenameTable(oldName, newName string) string
SupportsTransactionalDDL() bool
LockSuffix(opts LockOptions) (tableHint, suffix string, err error)
UpsertSQL(conflictCols, updateCols []string, argOffset int) string
}
Placeholder Examples
| Dialect | Placeholder |
|---|---|
| PostgreSQL | $1, $2 |
| MySQL / MariaDB | ? |
| SQLite | ? |
| SQL Server | @p1, @p2 |
| Oracle | :1, :2 |
Returning and Insert IDs
| Dialect | Preferred generated-ID path |
|---|---|
| PostgreSQL | RETURNING |
| SQLite | RETURNING |
| MariaDB | RETURNING |
| MySQL | LastInsertId |
| SQL Server | SCOPE_IDENTITY() |
| Oracle | RETURNING ... INTO handling in Quark write path |
Upsert
| Dialect | Shape |
|---|---|
| PostgreSQL | ON CONFLICT (...) DO UPDATE SET ... |
| SQLite | ON CONFLICT (...) DO UPDATE SET ... |
| MySQL / MariaDB | ON DUPLICATE KEY UPDATE ... |
| SQL Server | MERGE built by Quark's CRUD layer. |
| Oracle | MERGE built by Quark's CRUD layer. |
Detection
d, err := quark.DetectDialect("postgres")
d, err = quark.DetectDialectByName("mariadb")
DetectDialectByName first checks custom dialect registrations, then falls back
to built-in detection.
Custom Dialects
quark.RegisterDialect("cockroach", myDialect)
d, err := quark.DetectDialectByName("cockroach")
if err != nil {
return err
}
client, err := quark.New("pgx", dsn, quark.WithDialect(d))
Custom dialects must implement the full Dialect interface. A partial dialect
will fail at compile time if assigned to a quark.Dialect variable.
RegisterDialect, DetectDialect and DetectDialectByName are safe to call
from any goroutine: the registry is guarded by a lock, so registering a dialect
while another goroutine builds a client is not a data race. Registering a name
again replaces the dialect registered under it.
Where the contract lives
The dialect contract is declared in quarkdriver, and package quark keeps
every name as an alias of the same type, or as a function that calls the one in
quarkdriver. Nothing is deprecated: the quark names are the application's,
the quarkdriver names are the driver author's, and they are interchangeable.
In quarkdriver | Also named in quark |
|---|---|
Dialect, LockOptions, LockMode (LockNone, LockForUpdate, LockForShare) | same names |
SavepointDialect, ColumnTypeMapper | same names |
MigrationLocker, MigrationLock, DBConnector, DBConn, Result, Row | same names |
SchemaIntrospector, Executor, and the schema model: Schema, Table, Column, Index, ForeignKey, Check | same names |
ErrUnsupportedFeature, ErrLockTimeout | same names, same values: errors.Is matches under either |
RegisterDialect(name, d), LookupDialect(name) | quark.RegisterDialect writes the same registry; quark.DetectDialect and DetectDialectByName read it before the built-in names |
ColumnTyper, AutoIncrementer, IdempotentDDL, ColumnAlterer, ObjectDropper, ReferentialActioner, TableRebuilder and their types | only in quarkdriver |
DetectDialect, DetectDialectByName, ErrDialectNotSupported and the six
built-in dialects stay in package quark: they are implementations, not
contract.
Because an alias is the same type, the only visible difference is the type's
package path: %T, reflect.Type.String() and reflect.Type.PkgPath()
report quarkdriver.Schema where they reported quark.Schema in v1.15.
Code that compares a printed type name sees the new one.
From a driver module
A driver module for an engine Quark does not ship writes and registers its
dialect against quarkdriver alone, without importing package quark:
package extdriver
import (
"database/sql"
"fmt"
"github.com/jcsvwinston/quark/quarkdriver"
)
type dialect struct{ /* … */ }
var _ quarkdriver.Dialect = dialect{}
func (dialect) LockSuffix(opts quarkdriver.LockOptions) (string, string, error) {
if opts.IsZero() {
return "", "", nil
}
return "", "", fmt.Errorf("%w: no row locks", quarkdriver.ErrUnsupportedFeature)
}
// … the other Dialect methods …
func init() {
sql.Register("extsql", &extDriver{})
quarkdriver.RegisterDialect("extsql", dialect{})
}
An application then opens quark.New("extsql", dsn), and the client resolves
the dialect by that name. A dialect that wraps a built-in one — a CockroachDB
driver on quark.PostgreSQL() — imports package quark to reach it, and may.
Writing a driver builds a whole driver module this way, from an empty module to one that passes the conformance kit, and lists what the contract cannot express yet.
What the schema path asks a dialect
Migrate, PlanMigration, Sync, ApplyPlan and the tables Quark keeps for
itself (the migrator's ledger, ApplyPlan's checkpoint, Backfill's state)
write DDL. What they write is the dialect's answer to a handful of questions,
each an optional interface in github.com/jcsvwinston/quark/quarkdriver
that Quark asserts on the dialect. None of them is answered from Name(): the
name is how a dialect is registered and found, not what it can do.
A dialect that does not implement one gets the default in the table: a
portable spelling where one exists, or ErrUnsupportedFeature where none does.
It never gets DDL chosen for another engine's name. A question Quark starts
asking later arrives the same way, as another optional interface with a
default: within v1, Quark adds no method to Dialect, so a dialect written
today keeps compiling.
ColumnTyper, AutoIncrementer, IdempotentDDL, ColumnAlterer,
ObjectDropper, ReferentialActioner and TableRebuilder are
experimental in the
extension contract: no v1 release breaks them, but
no engine outside Quark has implemented them yet, so v2 may change their
shape without a deprecation period.
| Interface | Asked for | Default when not implemented |
|---|---|---|
quarkdriver.ColumnTyper | the column type of each portable kind (ColumnType(ColumnSpec)), and the literal of a boolean DEFAULT (BoolLiteral) | the portable types below, TRUE/FALSE |
quarkdriver.AutoIncrementer | the definition of a single-column integer key the engine numbers itself, and the catalog type of that column | BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, catalog type BIGINT |
quarkdriver.IdempotentDDL | creating a table or an index only when it is missing, and which errors mean "it exists already" | CREATE TABLE IF NOT EXISTS, CREATE [UNIQUE] INDEX IF NOT EXISTS, no error counts as "exists" |
quarkdriver.SchemaIntrospector (also quark.SchemaIntrospector) | the current schema; Sync reads the table's columns through it, and ApplyPlan, on an engine without transactional DDL, reads it before it trusts a checkpoint | IntrospectSchema and Sync return ErrUnsupportedFeature; ApplyPlan trusts a checkpoint as recorded |
quarkdriver.ColumnTypeMapper (also quark.ColumnTypeMapper) | a column type a hand-built Plan carries, in the engine's form (TEXT → CLOB on Oracle) | the type is used as written |
Dialect.SupportsTransactionalDDL() (required) | whether ApplyPlan runs a plan in one transaction, and whether Sync wraps its changes in one | — every dialect answers |
quarkdriver.ColumnAlterer | the statements of an in-place column change (OpAlterColumn): type, nullability, default, primary key; it may read the catalog through the Executor it is given | the dialect's own AlterTableAlterColumn for the type, ALTER COLUMN … SET/DROP NOT NULL, SET/DROP DEFAULT, ADD CONSTRAINT pk_<table> PRIMARY KEY; dropping a key returns ErrUnsupportedFeature, since its name is in the catalog |
quarkdriver.ObjectDropper | dropping an index, a foreign key and a CHECK by name | DROP INDEX <index>, ALTER TABLE <table> DROP CONSTRAINT <name> |
quarkdriver.ReferentialActioner | each referential action a foreign key names in ON DELETE and ON UPDATE — AddForeignKey, OpAddForeignKey, the foreign keys of OpCreateTable: write it, leave it out because the engine does it without a clause, or refuse it | every action written as given |
quarkdriver.TableRebuilder | whether the engine changes a table by rebuilding it, SQLite's way | in-place ALTER statements |
A method of ColumnTyper, AutoIncrementer, IdempotentDDL or
ObjectDropper that returns "" takes the default for that one question, so a
dialect can answer only what its engine spells differently.
quarkdriver.Executor is the same type as quark.Executor: the interface a
*sql.DB, a *sql.Tx and a *sql.Conn share. The same holds for every type
of the contract that package quark also names (see
Where the contract lives).
Quark classifies a field's Go type into a quarkdriver.ColumnKind before it
asks — it unwraps pointers, sql.Null[T], quark.JSON[T] and
quark.Array[T] — so a dialect never sees a Go type. A type registered with
quark.RegisterTypeMapper is consulted first, and receives the dialect's
Name().
| Kind | Go types | Portable type | PostgreSQL | MySQL / MariaDB | SQLite | SQL Server | Oracle |
|---|---|---|---|---|---|---|---|
KindString | string | VARCHAR(255), VARCHAR(N) with size=N | TEXT | VARCHAR(255) | TEXT | NVARCHAR(255) | VARCHAR2(255) |
KindText | Quark's own long text | TEXT | TEXT | TEXT | TEXT | NVARCHAR(MAX) | CLOB |
KindInt16 / KindInt32 / KindInt64 | integers by width, time.Duration | SMALLINT / INTEGER / BIGINT | same | same | same | same | NUMBER(5) / NUMBER(10) / NUMBER(19) |
KindFloat32 / KindFloat64 | float32 / float64 | REAL / DOUBLE PRECISION | same | REAL / DOUBLE | REAL / REAL | REAL / FLOAT(53) | BINARY_FLOAT / BINARY_DOUBLE |
KindDecimal | a float with precision=P,scale=S | DECIMAL(P,S) | same | same | same | same | NUMBER(P,S) |
KindBool | bool | BOOLEAN | BOOLEAN | BOOLEAN | BOOLEAN | BIT | NUMBER(1) |
KindTime | time.Time | TIMESTAMP | TIMESTAMP | DATETIME | DATETIME | DATETIME2 | TIMESTAMP |
KindBytes | []byte | BLOB | BYTEA | BLOB | BLOB | VARBINARY(MAX) | BLOB |
KindJSON | quark.JSON[T], quark.Array[T], maps, slices of non-scalars | TEXT | JSONB | JSON | TEXT | NVARCHAR(MAX) | CLOB |
KindUUID | 16-byte arrays (uuid.UUID) | CHAR(36) | UUID | CHAR(36) | UUID | NCHAR(36) | VARCHAR2(36) |
KindIP | net.IP | VARCHAR(45) | INET | VARCHAR(45) | TEXT | NVARCHAR(45) | VARCHAR2(45) |
KindArray | slices of a scalar | TEXT | the element type [] | JSON | TEXT | NVARCHAR(MAX) | CLOB |
KindRange | quark.Range[T] | TEXT | TSTZRANGE, INT8RANGE, INT4RANGE, NUMRANGE | JSON | TEXT | NVARCHAR(MAX) | CLOB |
KindOther | anything else | TEXT | TEXT | TEXT | TEXT | TEXT | TEXT |
The per-Go-type result, generated from the code, is the type matrix.
The auto-increment keys of the built-ins: BIGSERIAL PRIMARY KEY
(PostgreSQL), BIGINT AUTO_INCREMENT PRIMARY KEY (MySQL, MariaDB),
INTEGER PRIMARY KEY AUTOINCREMENT (SQLite, catalog type INTEGER),
BIGINT IDENTITY(1,1) PRIMARY KEY (SQL Server) and
NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY (Oracle). The default is the
SQL standard's identity column: an engine that does not accept it fails the
CREATE TABLE instead of creating a key that stays NULL.
SupportsTransactionalDDL() is true for PostgreSQL, SQL Server and SQLite and
false for MySQL, MariaDB and Oracle, which commit around every DDL statement;
ApplyPlan takes its resumable, checkpointed path there, and asks the
dialect's SchemaIntrospector whether the operations a checkpoint records are
still in the schema before it skips them. Oracle's dialect used
to answer true, which nothing acted on while ApplyPlan decided by the
dialect's name.
PostgreSQL and SQLite take the IdempotentDDL default. MySQL and MariaDB
implement it because they have no IF NOT EXISTS on CREATE INDEX (a
duplicate index name, error 1061, counts as "exists"); SQL Server guards each
CREATE with a lookup in sys.tables / sys.indexes; Oracle writes a plain
CREATE TABLE and counts ORA-00955, ORA-01408 and ORA-02264 as "exists".
For ApplyPlan, PostgreSQL implements ColumnAlterer only to read the name of
the primary-key constraint it drops, and takes the standard statements
otherwise; MySQL and MariaDB restate the column with MODIFY COLUMN; SQL Server
treats a default as the named constraint it is there; Oracle writes one
MODIFY with only what changes. MySQL (DROP INDEX … ON, DROP FOREIGN KEY,
DROP CHECK), MariaDB (the same, with DROP CONSTRAINT for a CHECK: it has
no DROP CHECK) and SQL Server (DROP INDEX … ON) implement ObjectDropper. SQLite implements TableRebuilder: it has no ALTER COLUMN
and no ADD/DROP CONSTRAINT, so a column change, a foreign key and a CHECK
rebuild the table inside the plan's transaction. The rebuild reads SQLite's own
catalog, so only an engine that keeps that catalog can answer true.
SQL Server and Oracle implement ReferentialActioner, because each lacks
actions of the SQL standard's five. Quark asks about every action a foreign key
names, upper case and single-spaced, and acts on the answer: ActionWritten
writes the clause as the caller gave it, ActionImplied leaves it out because
the engine does that action for a foreign key that names none, and
ActionUnsupported refuses the foreign key with ErrUnsupportedFeature, naming
the clause and the action, before any statement is sent. ApplyPlan asks about
every foreign key of the plan before its first operation, so a refused plan
changes nothing, on an engine without transactional DDL too.
| Action | PostgreSQL, SQLite | MySQL, MariaDB | SQL Server | Oracle ON DELETE | Oracle ON UPDATE |
|---|---|---|---|---|---|
NO ACTION | written | written | written | left out: the engine's default | left out: the engine's default |
RESTRICT | written | written | refused | refused | refused |
CASCADE | written | written | written | written | refused |
SET NULL | written | written | written | written | refused |
SET DEFAULT | written | written, not done (below) | written | refused | refused |
Oracle has no ON UPDATE clause, and no clause for NO ACTION in either
(ORA-02000 for both); ON DELETE SET DEFAULT is ORA-03001. SQL Server has no
RESTRICT. Until Quark asked, each of these reached the engine and failed
there. SQL Server also refuses a foreign key that would make a cycle or a
second cascade path to one table (Msg 1785); that depends on the other foreign
keys of the schema, not on one action, and the engine's error says so.
MySQL and MariaDB accept SET DEFAULT and do not do it: InnoDB refuses the
delete or the update, as NO ACTION would, and MariaDB records RESTRICT in
its catalog, so a Schema that declares SET DEFAULT keeps planning the
foreign key again there. Their
dialects write it all the same, because refusing it would fail a statement that
succeeds today. Declare NO ACTION, or RESTRICT, which is what the engine
does.
Wrapping a built-in dialect
A dialect that embeds a built-in one to change a method — its name, say —
promotes only the methods of the Dialect interface. The optional interfaces
are not promoted through an embedded interface value, so the wrapper has to
forward the ones the built-in implements, or the schema path takes the
defaults:
type cockroach struct{ quark.Dialect }
func (cockroach) Name() string { return "cockroach" }
func (d cockroach) ColumnType(s quarkdriver.ColumnSpec) string {
return d.Dialect.(quarkdriver.ColumnTyper).ColumnType(s)
}
func (d cockroach) BoolLiteral(v bool) string {
return d.Dialect.(quarkdriver.ColumnTyper).BoolLiteral(v)
}
func (d cockroach) AutoIncrementColumn() (string, string) {
return d.Dialect.(quarkdriver.AutoIncrementer).AutoIncrementColumn()
}
func (d cockroach) IntrospectSchema(ctx context.Context, exec quark.Executor) (quark.Schema, error) {
return d.Dialect.(quark.SchemaIntrospector).IntrospectSchema(ctx, exec)
}
func (d cockroach) AlterColumn(ctx context.Context, exec quarkdriver.Executor, c quarkdriver.ColumnChange) ([]string, error) {
return d.Dialect.(quarkdriver.ColumnAlterer).AlterColumn(ctx, exec, c)
}
Which interfaces each built-in implements is pinned by Quark's own tests. A
wrapper of quark.PostgreSQL(), like the one above, forwards ColumnTyper,
AutoIncrementer, ColumnAlterer and SchemaIntrospector — and
MigrationLocker too, for the migrator's lock, which the example leaves out. A
wrapper of quark.SQLite() forwards ColumnTyper, AutoIncrementer,
TableRebuilder and SchemaIntrospector. A wrapper of quark.MSSQL() or
quark.Oracle() also forwards ReferentialActioner, or its foreign keys are
written with the actions its engine refuses.
Checking a dialect
github.com/jcsvwinston/quark/quarkdriver/drivertest is the conformance kit a
driver module runs from its own tests. VerifyDialect checks a dialect against
a live engine the test opens — every method of Dialect and every optional
interface the dialect implements, through Quark's queries and its schema path —
and judges each one by what the engine then holds or refuses, not by the text
of a statement:
func TestDialectConformance(t *testing.T) {
dsn := os.Getenv("EXTSQL_TEST_DSN")
if dsn == "" {
t.Skip("EXTSQL_TEST_DSN not set")
}
db, err := sql.Open("extsql", dsn)
if err != nil {
t.Fatal(err)
}
defer db.Close()
drivertest.VerifyDialect(t, drivertest.DialectCase{
Dialect: extsql.Dialect(),
DB: db,
DriverName: "extsql",
})
}
DialectCase field | What it is for |
|---|---|
Dialect | The dialect under test. Required. |
DB | A pool on a live engine of the dialect's kind, with more than one connection. The kit creates and drops tables named qk_kit_*. nil runs only the checks that need no engine, and logs that the rest were skipped. |
DriverName | The database/sql name DB was opened with. When set, the kit also checks that quark.NewWithDB(DriverName, DB) resolves this dialect without WithDialect — that the driver registered it under its name. |
Routines | A function and a procedure the test created on the engine, for BuildRoutineQuery and BuildProcedureCall. nil skips those two checks. |
The subtests are named after the member of the contract they check —
engine/Placeholder, engine/UpsertSQL, engine/ColumnTyper/KindTime,
engine/ObjectDropper/DropCheck — so a failure names the method to fix. An
optional interface the dialect does not implement is logged with the default
Quark uses instead, and the check exercises that default: a dialect that leaves
out AutoIncrementer on an engine without SQL-standard identity columns fails
engine/AutoIncrementer, not a test somewhere downstream. A wrapper that forgot
to forward an interface of the built-in it embeds shows up the same way: as the
check that needed it failing, or logged as skipped.
The six built-in dialects run the kit against their engines in Quark's own CI,
and the kit found two of them wrong before it shipped: MariaDB dropped a CHECK
with MySQL's DROP CHECK, and SQL Server accepted a shared lock that skips
locked rows, which the engine refuses. engine/ReferentialActioner tries the
SQL standard's five actions in each clause of a foreign key: one the dialect
has Quark write must be accepted, one it leaves out must be what the engine
does without a clause, and one it refuses must fail with
ErrUnsupportedFeature before a statement is sent.
github.com/jcsvwinston/quark/quarkdriver/drivertest/suite is the engine
suite: the checks the six built-in engines run on a whole *quark.Client —
CRUD, the query builder, transactions and savepoints, hooks, soft deletes,
locking, preloads and joins, subqueries and window functions, batch writes,
JSON and arrays, time zones, Sync and reversible migrations:
func TestEngineSuite(t *testing.T) {
client, err := quark.New("extsql", os.Getenv("EXTSQL_TEST_DSN"))
if err != nil {
t.Fatal(err)
}
defer client.Close()
suite.Run(t, client)
}
Both packages import Quark and the standard library only: running them adds no driver and no container library to the module's dependencies. They grow with the contract — a release that adds an optional interface adds its check — so a driver that pins a newer Quark may meet a check it did not run before.