Skip to main content
Version: 1.17.0

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()
ConstructorName()
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​

DialectPlaceholder
PostgreSQL$1, $2
MySQL / MariaDB?
SQLite?
SQL Server@p1, @p2
Oracle:1, :2

Returning and Insert IDs​

DialectPreferred generated-ID path
PostgreSQLRETURNING
SQLiteRETURNING
MariaDBRETURNING
MySQLLastInsertId
SQL ServerSCOPE_IDENTITY()
OracleRETURNING ... INTO handling in Quark write path

Upsert​

DialectShape
PostgreSQLON CONFLICT (...) DO UPDATE SET ...
SQLiteON CONFLICT (...) DO UPDATE SET ...
MySQL / MariaDBON DUPLICATE KEY UPDATE ...
SQL ServerMERGE built by Quark's CRUD layer.
OracleMERGE 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 quarkdriverAlso named in quark
Dialect, LockOptions, LockMode (LockNone, LockForUpdate, LockForShare)same names
SavepointDialect, ColumnTypeMappersame names
MigrationLocker, MigrationLock, DBConnector, DBConn, Result, Rowsame names
SchemaIntrospector, Executor, and the schema model: Schema, Table, Column, Index, ForeignKey, Checksame names
ErrUnsupportedFeature, ErrLockTimeoutsame 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 typesonly 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.

InterfaceAsked forDefault when not implemented
quarkdriver.ColumnTyperthe column type of each portable kind (ColumnType(ColumnSpec)), and the literal of a boolean DEFAULT (BoolLiteral)the portable types below, TRUE/FALSE
quarkdriver.AutoIncrementerthe definition of a single-column integer key the engine numbers itself, and the catalog type of that columnBIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, catalog type BIGINT
quarkdriver.IdempotentDDLcreating 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 checkpointIntrospectSchema 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.ColumnAltererthe statements of an in-place column change (OpAlterColumn): type, nullability, default, primary key; it may read the catalog through the Executor it is giventhe 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.ObjectDropperdropping an index, a foreign key and a CHECK by nameDROP INDEX <index>, ALTER TABLE <table> DROP CONSTRAINT <name>
quarkdriver.ReferentialActionereach 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 itevery action written as given
quarkdriver.TableRebuilderwhether the engine changes a table by rebuilding it, SQLite's wayin-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().

KindGo typesPortable typePostgreSQLMySQL / MariaDBSQLiteSQL ServerOracle
KindStringstringVARCHAR(255), VARCHAR(N) with size=NTEXTVARCHAR(255)TEXTNVARCHAR(255)VARCHAR2(255)
KindTextQuark's own long textTEXTTEXTTEXTTEXTNVARCHAR(MAX)CLOB
KindInt16 / KindInt32 / KindInt64integers by width, time.DurationSMALLINT / INTEGER / BIGINTsamesamesamesameNUMBER(5) / NUMBER(10) / NUMBER(19)
KindFloat32 / KindFloat64float32 / float64REAL / DOUBLE PRECISIONsameREAL / DOUBLEREAL / REALREAL / FLOAT(53)BINARY_FLOAT / BINARY_DOUBLE
KindDecimala float with precision=P,scale=SDECIMAL(P,S)samesamesamesameNUMBER(P,S)
KindBoolboolBOOLEANBOOLEANBOOLEANBOOLEANBITNUMBER(1)
KindTimetime.TimeTIMESTAMPTIMESTAMPDATETIMEDATETIMEDATETIME2TIMESTAMP
KindBytes[]byteBLOBBYTEABLOBBLOBVARBINARY(MAX)BLOB
KindJSONquark.JSON[T], quark.Array[T], maps, slices of non-scalarsTEXTJSONBJSONTEXTNVARCHAR(MAX)CLOB
KindUUID16-byte arrays (uuid.UUID)CHAR(36)UUIDCHAR(36)UUIDNCHAR(36)VARCHAR2(36)
KindIPnet.IPVARCHAR(45)INETVARCHAR(45)TEXTNVARCHAR(45)VARCHAR2(45)
KindArrayslices of a scalarTEXTthe element type []JSONTEXTNVARCHAR(MAX)CLOB
KindRangequark.Range[T]TEXTTSTZRANGE, INT8RANGE, INT4RANGE, NUMRANGEJSONTEXTNVARCHAR(MAX)CLOB
KindOtheranything elseTEXTTEXTTEXTTEXTTEXTTEXT

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.

ActionPostgreSQL, SQLiteMySQL, MariaDBSQL ServerOracle ON DELETEOracle ON UPDATE
NO ACTIONwrittenwrittenwrittenleft out: the engine's defaultleft out: the engine's default
RESTRICTwrittenwrittenrefusedrefusedrefused
CASCADEwrittenwrittenwrittenwrittenrefused
SET NULLwrittenwrittenwrittenwrittenrefused
SET DEFAULTwrittenwritten, not done (below)writtenrefusedrefused

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 fieldWhat it is for
DialectThe dialect under test. Required.
DBA 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.
DriverNameThe 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.
RoutinesA 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.