Skip to main content
Version: 1.17.0

Extension contract

Quark calls code you write in a few dozen places: a hook on a model, a cache store, a middleware, a dialect for an engine Quark does not ship. Each of those places is an exported interface or function type of quark or quarkdriver. This page lists all of them — every exported type of the two packages that code outside them can implement and hand to Quark — and says, for each one, whether you are meant to implement it and what Quark promises about its shape.

What the columns mean​

The two columns are Quark's compatibility promise for each type. They narrow the versioning promise in one place — your own implementations of a type marked no — and the project recorded the rule, with its reasons, in a decision record on 2026-10-06.

Extension point.

  • yes — Quark calls your implementation, and that is what the type is for. Within v1, Quark does not add a method to an interface marked yes, nor remove one or change its signature; it does not change the signature of a function type marked yes; and it does not add, remove or change a field of quarkdriver.Classifier, whose predicates are all required. A type marked yes stays an extension point for all of v1. A new capability arrives as a separate, optional interface that Quark checks for with a type assertion, and a type that does not implement it gets a documented default — the way SavepointDialect and SchemaIntrospector extend Dialect, and CacheLocker extends CacheStore — so an implementation written today keeps compiling and keeps working.
  • no — plumbing. Quark, the standard library or Quark's own types implement it; your code receives it, calls its methods or passes along the values Quark gives it, and that use is covered by the versioning promise: within v1 no method you call is removed or changes its signature, and the function types marked no keep theirs. Your own implementation of an interface marked no is not covered: Quark may add a method to that interface in a minor release — never in a patch — and the release notes name the interface and the method.

Stability.

  • stable — Within v1 it changes only as the column above allows. A change that would break an implementation waits for v2, and is announced in the release notes with a deprecation first.
  • experimental — Nothing breaks within v1 either: a minor or a patch release only adds. The shape is not settled yet — no implementation outside Quark's repository has used it — so v2 may change it without a deprecation period.
  • internal-use — Exported because Quark's own code needs it: what quark gen writes, or an earlier contract kept so that code built against it still works. Do not implement it by hand. Within v1 it keeps its shape, so code an earlier quark gen wrote keeps compiling.

Only v2 may add, remove or change a method of an extension point; within v2.x the same rules apply again, to the v2 contract.

Several types of quarkdriver are also named in package quark by an alias — quark.Dialect is quarkdriver.Dialect, the same type, not a copy. The table lists each type once, under the package that declares it, and gives the alias beside it.

The contract​

TypeExtension pointStabilityHow it reaches Quark, and where Quark uses it
quark.TableNameryesstableA model's TableName() string. Quark asks once per model type, when it builds the model's metadata; Migrate and the query builder use the answer.
quark.ValidatoryesstableA model's Validate(ctx) error. Client.Validate calls it before the validate struct tags; Create, CreateBatch, Upsert and UpsertBatch call Client.Validate before any SQL, and an error aborts the write.
quark.BeforeCreateHookyesstableOn the model. Create, CreateBatch, Upsert and UpsertBatch, once per entity, before the INSERT; an error aborts it. See Hooks.
quark.AfterCreateHookyesstableOn the model. Create, after the INSERT — inside a transaction, after the commit.
quark.BeforeUpdateHookyesstableOn the model. Update, UpdateFields, UpdateBatch and Tracked.Save, before the UPDATE.
quark.AfterUpdateHookyesstableOn the model. Update, UpdateFields and Tracked.Save, after the UPDATE.
quark.BeforeDeleteHookyesstableOn the model. Delete and HardDelete, before the DELETE.
quark.AfterDeleteHookyesstableOn the model. Delete and HardDelete, after the DELETE.
quark.BeforeFindHookyesstableOn the model. List, First, Find, Iter and Cursor, before the SELECT; an error aborts the read.
quark.AfterFindHookyesstableOn the model. The same reads, once per query, after the rows are scanned.
quark.ShardKeyeryesstableA model's ShardKey() string, read by WithShardKeyOf(ctx, entity), which puts the key where a ShardRouter resolves it.
quark.ClientProvideryesstableWhat For[T], Notify and NewRoutine take. *Client, *TenantRouter and *ShardRouter implement it; a router of your own that returns a *Client for a context does too.
quark.OptionnostableThe values New takes. Build them with the With… constructors (WithDialect, WithMiddleware, …).
quark.ScopeyesstableA reusable query modifier, func(*Query[T]) *Query[T], applied by Query[T].Apply.
quark.ExprnostableAn expression node. Build it with Col, Lit, Eq, And, Or, Not, Func, Case and the rest, and pass it to WhereExpr or HavingExpr.
quark.MiddlewareyesstableWithMiddleware. Wraps the statements of queries and writes; schema work, savepoints, Client.Exec and Client.RawQuery do not pass through it.
quark.ExecFuncyesstableWhat Middleware.WrapExec receives and returns.
quark.QueryFuncyesstableWhat Middleware.WrapQuery receives and returns.
quark.QueryRowFuncyesstableWhat Middleware.WrapQueryRow receives and returns.
quark.QueryObserveryesstableWithQueryObserver. Called after the statements of queries and writes, and of Client.Exec and Client.RawQuery; not after CreateBatch, schema work or savepoints.
quark.CacheStoreyesstableWithCacheStore. Serves the reads of a query with Cache(ttl); a write to the cached table invalidates them.
quark.CacheLockeryesstableOptional on a CacheStore. With WithCacheCrossInstance(), Quark takes its lock so that one process recomputes an expired key while the others wait and re-read it.
quark.EventBusyesstableClient.UseEventBus. Publish receives every committed Create, Update and Delete, after the commit.
quark.EventnostableWhat Quark hands EventBus.Publish: the kind, the table and the model value.
quark.TypeMapperyesstableRegisterTypeMapper(reflect.Type, TypeMapper). Migrate and PlanMigration ask it for the column type of a Go type, with the dialect's name.
quark.TypedScannernointernal-useWritten by quark gen and registered with RegisterTypedScanner; reads of the model scan through it instead of reflection.
quark.TypedBindernointernal-useWritten by quark gen and registered with RegisterTypedBinder; writes of the model bind through it.
quark.ShardFuncyesstableThe third argument of NewShardRouter: the shard that owns a key. HashShardFunc is the built-in.
quark.ShardResolveryesstableThe second argument of NewShardRouter: the shard key of a context. DefaultShardResolver is the built-in.
quarkdriver.Classifieryesstablequarkdriver.Register(engine, c), from a driver module's init. IsUniqueViolation, IsDeadlock, the deadlock retry and the read-replica failover consult it.
quarkdriver.SQLStateryesstableOn a driver's error type. Quark reads its code as a PostgreSQL SQLSTATE before it consults any classifier; pgx and lib/pq implement it. A driver for another engine registers a Classifier instead.
quarkdriver.ListenerFactoryyesstablequarkdriver.RegisterListenerFactory(engine, f). Called when an application opens a listener with NewListenerFactory(client).CreateListener().
quarkdriver.Listener (quark.EventListener)yesstableWhat a ListenerFactory returns; the application receives it as a quark.EventListener. See Events.
quarkdriver.IdentifierValidatornostableWhat Quark hands a ListenerFactory to validate channel names with: its SQLGuard.
quarkdriver.NewListenerFuncnointernal-useThe listener contract before ListenerFactory, kept so a driver built against it still registers. Deprecated; removed in v2.
quarkdriver.Dialect (quark.Dialect)yesstablequarkdriver.RegisterDialect(name, d), or WithDialect(d). Every statement Quark writes goes through it. See Dialects API.
quarkdriver.SavepointDialect (quark.SavepointDialect)yesstableOptional on a Dialect. Tx.Savepoint, Tx.RollbackTo and Tx.ReleaseSavepoint; without it, the ANSI statements.
quarkdriver.ColumnTypeMapper (quark.ColumnTypeMapper)yesstableOptional on a Dialect. Translates the column type of a hand-built plan before ApplyPlan writes it.
quarkdriver.SchemaIntrospector (quark.SchemaIntrospector)yesstableOptional on a Dialect. Client.IntrospectSchema, PlanMigration and Sync; without it they return ErrUnsupportedFeature.
quarkdriver.MigrationLocker (quark.MigrationLocker)yesstableOptional on a Dialect. Client.AcquireMigrationLock and the migrator's schema lock; without it, ErrUnsupportedFeature.
quarkdriver.MigrationLock (quark.MigrationLock)yesstableWhat a MigrationLocker returns; the caller releases it.
quarkdriver.ColumnTyperyesexperimentalOptional on a Dialect. The column types Migrate, Sync and PlanMigration write, and those of Quark's own bookkeeping tables; without it, portable types.
quarkdriver.AutoIncrementeryesexperimentalOptional on a Dialect. The self-numbered integer primary key Migrate and ApplyPlan write.
quarkdriver.IdempotentDDLyesexperimentalOptional on a Dialect. How Migrate and Quark's bookkeeping tables create a table or an index only when it is missing.
quarkdriver.ColumnAltereryesexperimentalOptional on a Dialect. The statements of ApplyPlan's column changes.
quarkdriver.ObjectDropperyesexperimentalOptional on a Dialect. The statements with which ApplyPlan drops an index, a foreign key or a CHECK.
quarkdriver.ReferentialActioneryesexperimentalOptional on a Dialect. Which referential actions a foreign key's ON DELETE and ON UPDATE can name on the engine: written, left out as the engine's default, or refused before any statement is sent.
quarkdriver.TableRebuilderyesexperimentalOptional on a Dialect. Tells ApplyPlan to rebuild a table instead of altering it, as SQLite requires.
quarkdriver.Executor (quark.Executor)nostableWhat Quark hands IntrospectSchema, AlterColumn and the middleware functions: a *sql.DB, *sql.Tx or *sql.Conn.
quarkdriver.DBConnector (quark.DBConnector)nostableWhat Quark hands AcquireMigrationLock: an adapter over the client's pool.
quarkdriver.DBConn (quark.DBConn)nostableThe dedicated connection a DBConnector returns.
quarkdriver.Result (quark.Result)nostableWhat DBConn.ExecContext returns.
quarkdriver.Row (quark.Row)nostableWhat DBConn.QueryRowContext returns.

Implementing an extension point​

Assert the interface at compile time, next to the type that implements it, so a mismatch fails your build rather than leaving Quark to skip a method it cannot find:

var (
_ quark.TableNamer = User{}
_ quark.Validator = (*User)(nil)
_ quark.BeforeCreateHook = (*User)(nil)
_ quarkdriver.Dialect = (*VerticaDialect)(nil)
_ quarkdriver.SQLStater = (*Error)(nil)
)

The model conventions — TableName, Validate and the hooks — are found by a type assertion on the model, so a method with the wrong signature is not an error: Quark does not see it. The assertion is what tells you.

If you need your own implementation of a type marked no — a test double of an Executor, say, or an executor a middleware passes on in place of the one it was handed — embed the interface in your type. A method Quark adds to it in a minor release is then promoted from the value you embed, and your type keeps compiling:

type countingExecutor struct {
quark.Executor // the value Quark handed you
statements atomic.Int64
}

func (c *countingExecutor) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
c.statements.Add(1)
return c.Executor.ExecContext(ctx, query, args...)
}

A dialect for an engine Quark does not ship needs only quarkdriver; the Dialects API shows one registered without importing package quark, and how to check it with the conformance kit. Writing a driver builds a whole driver module — the classifier, the dialect and the optional interfaces it implements — from the template Quark tests in CI.

How this page is kept true​

The table is checked on every change to Quark. A test computes, from the compiled packages, every exported type of quark and quarkdriver that code outside them can implement, and fails when the table above misses one, names one twice, names something that is not one, or uses a value outside the vocabulary of this page. Quark's CI also records the signature of every method and function type listed here, and of the fields of Classifier, so a change to any of them cannot land without that record changing in the same pull request. A second test holds the rows marked yes to the rule above: it keeps the shape of every extension point as it was when the rule was adopted — or, for a newer one, when it was added — and fails when one of them gains, loses or changes a method or a field, or stops being marked yes.