Documentation
¶
Overview ¶
Package quark provides a modern, type-safe ORM for Go. It supports multiple SQL dialects and is designed to be framework-agnostic.
Index ¶
- Constants
- Variables
- func Call(ctx context.Context, provider ClientProvider, procedure string, args ...any) error
- func CanonicalType(s string) string
- func CheckGeneratedDrift(t reflect.Type) (drifted bool, hasGenerated bool)
- func DefaultShardResolver(ctx context.Context) string
- func GeneratedBinderRegistered(t reflect.Type) bool
- func HashModelFields(fields []ModelField) string
- func ModelHash(t reflect.Type) string
- func Notify(ctx context.Context, provider ClientProvider, channel, payload string) error
- func RegisterDialect(name string, d Dialect)
- func RegisterGeneratedMeta(t reflect.Type, meta GeneratedMeta)
- func RegisterTypeMapper(t reflect.Type, m TypeMapper)
- func RegisterTypedBinder(t reflect.Type, fn TypedBinder)
- func RegisterTypedScanner(t reflect.Type, fn TypedScanner)
- func ScanTarget(ptr any) any
- func ShardKeyFromContext(ctx context.Context) string
- func Sticky(ctx context.Context) context.Context
- func StubBinder(any, BindMode) ([]string, []any, error)
- func StubScanner(*sql.Rows, any) error
- func WithShardKey(ctx context.Context, key string) context.Context
- type AfterCreateHook
- type AfterDeleteHook
- type AfterFindHook
- type AfterUpdateHook
- type Array
- type AuditConfig
- type BackfillSpec
- type BaseMiddleware
- type BaseQuery
- type BeforeCreateHook
- type BeforeDeleteHook
- type BeforeFindHook
- type BeforeUpdateHook
- type BindMode
- type CacheConfig
- type CacheStore
- type Check
- type Client
- func (c *Client) AcquireMigrationLock(ctx context.Context, name string, timeout time.Duration) (MigrationLock, error)
- func (c *Client) AddForeignKey(ctx context.Context, table, constraintName string, columns []string, ...) error
- func (c *Client) ApplyPlan(ctx context.Context, plan Plan) error
- func (c *Client) Backfill(ctx context.Context, spec BackfillSpec) error
- func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error)
- func (c *Client) Close() error
- func (c *Client) CreateIndex(ctx context.Context, table, indexName string, columns []string, unique bool) error
- func (c *Client) Dialect() Dialect
- func (c *Client) DisableAuditLog()
- func (c *Client) EnableAuditLog(ctx context.Context, cfg AuditConfig) error
- func (c *Client) Exec(ctx context.Context, query string, args ...any) error
- func (c *Client) GetClient(ctx context.Context) (*Client, error)
- func (c *Client) IntrospectSchema(ctx context.Context) (Schema, error)
- func (c *Client) Migrate(ctx context.Context, models ...any) error
- func (c *Client) MigrateRegistered(ctx context.Context) error
- func (c *Client) PlanMigration(ctx context.Context, models ...any) (Plan, error)
- func (c *Client) PlanMigrationRegistered(ctx context.Context) (Plan, error)
- func (c *Client) Raw() *sql.DB
- func (c *Client) RawQuery(ctx context.Context, query string, args ...any) (*sql.Rows, error)
- func (c *Client) RegisterModel(models ...any) error
- func (c *Client) RegisteredModels() []any
- func (c *Client) Sync(ctx context.Context, opts SyncOptions, models ...any) error
- func (c *Client) Tx(ctx context.Context, fn func(tx *Tx) error) error
- func (c *Client) UseEventBus(bus EventBus)
- func (c *Client) Validate(ctx context.Context, model any) error
- func (c *Client) WithOptions(opts ...any) (*Client, error)
- type ClientProvider
- type Column
- type ColumnTypeMapper
- type Cursor
- type DBConn
- type DBConnector
- type Dialect
- type Event
- type EventBus
- type EventListener
- type EventPayload
- type ExecFunc
- type Executor
- type Expr
- func And(parts ...Expr) Expr
- func Cmp(lhs Expr, op string, rhs Expr) Expr
- func Col(name string) Expr
- func DenseRank() Expr
- func Eq(lhs, rhs Expr) Expr
- func Exists(s *Subquery) Expr
- func Func(name string, args ...Expr) Expr
- func Gt(lhs, rhs Expr) Expr
- func Gte(lhs, rhs Expr) Expr
- func In(lhs Expr, values ...Expr) Expr
- func InSub(lhs Expr, s *Subquery) Expr
- func Lag(col Expr, offset int) Expr
- func Lead(col Expr, offset int) Expr
- func Lit(v any) Expr
- func Lt(lhs, rhs Expr) Expr
- func Lte(lhs, rhs Expr) Expr
- func Ne(lhs, rhs Expr) Expr
- func Not(e Expr) Expr
- func NotExists(s *Subquery) Expr
- func NotIn(lhs Expr, values ...Expr) Expr
- func NotInSub(lhs Expr, s *Subquery) Expr
- func Or(parts ...Expr) Expr
- func Over(inner Expr, w *Window) Expr
- func Rank() Expr
- func RowNumber() Expr
- func Sub(s *Subquery) Expr
- type FieldMeta
- type ForeignKey
- type GeneratedMeta
- type Index
- type JSON
- type JoinBuilder
- type Limits
- type ListenerFactory
- type LockMode
- type LockOptions
- type LoggerEventBus
- type MSSQLDialect
- func (d *MSSQLDialect) AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)
- func (m *MSSQLDialect) AlterTableAddColumn(table, column, dataType string) string
- func (m *MSSQLDialect) AlterTableAlterColumn(table, column, newDataType string) string
- func (m *MSSQLDialect) AlterTableDropColumn(table, column string) string
- func (m *MSSQLDialect) BuildProcedureCall(procedure string, argCount int) string
- func (m *MSSQLDialect) BuildRoutineQuery(routine string, argCount int) string
- func (m *MSSQLDialect) CurrentTimestamp() string
- func (d *MSSQLDialect) IntrospectSchema(ctx context.Context, exec Executor) (Schema, error)
- func (m *MSSQLDialect) JSONExtract(column, path string) (string, []any, error)
- func (m *MSSQLDialect) LastInsertIDQuery(table, pkColumn string) string
- func (m *MSSQLDialect) LimitOffset(limit, offset int) string
- func (m *MSSQLDialect) LockSuffix(opts LockOptions) (string, string, error)
- func (d *MSSQLDialect) Name() string
- func (m *MSSQLDialect) Placeholder(index int) string
- func (m *MSSQLDialect) Placeholders(n int) []string
- func (m *MSSQLDialect) Quote(identifier string) string
- func (m *MSSQLDialect) ReleaseSavepointStmt(name string) string
- func (m *MSSQLDialect) RenameColumn(table, oldName, newName string) string
- func (m *MSSQLDialect) RenameTable(oldName, newName string) string
- func (m *MSSQLDialect) Returning(columns ...string) string
- func (m *MSSQLDialect) RollbackToSavepointStmt(name string) string
- func (m *MSSQLDialect) SavepointStmt(name string) string
- func (m *MSSQLDialect) SupportsLastInsertID() bool
- func (m *MSSQLDialect) SupportsReturning() bool
- func (m *MSSQLDialect) SupportsTransactionalDDL() bool
- func (m *MSSQLDialect) UpsertSQL(conflictCols, updateCols []string, _ int) string
- type MariaDBDialect
- func (d *MariaDBDialect) AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)
- func (m *MariaDBDialect) AlterTableAlterColumn(table, column, newDataType string) string
- func (m *MariaDBDialect) CreateSequence(name string, start, increment int64) string
- func (m *MariaDBDialect) CreateSystemVersionedTable(table string, columnDefs string) string
- func (m *MariaDBDialect) HistoryBetween(table, from, to string) string
- func (m *MariaDBDialect) HistoryQuery(table string) string
- func (d *MariaDBDialect) IntrospectSchema(ctx context.Context, exec Executor) (Schema, error)
- func (m *MariaDBDialect) JSONExtract(column, path string) (string, []any, error)
- func (m *MariaDBDialect) JSONTable(source, path string, columns ...string) string
- func (m *MariaDBDialect) LastInsertIDQuery(table, pkColumn string) string
- func (m *MariaDBDialect) LimitOffset(limit, offset int) string
- func (m *MariaDBDialect) LockSuffix(opts LockOptions) (string, string, error)
- func (d *MariaDBDialect) Name() string
- func (m *MariaDBDialect) NextVal(sequenceName string) string
- func (m *MariaDBDialect) RenameColumn(table, oldName, newName string) string
- func (m *MariaDBDialect) Returning(columns ...string) string
- func (m *MariaDBDialect) SupportsLastInsertID() bool
- func (m *MariaDBDialect) SupportsReturning() bool
- func (m *MariaDBDialect) SupportsTransactionalDDL() bool
- func (m *MariaDBDialect) UpsertSQL(conflictCols, updateCols []string, argOffset int) string
- type Middleware
- type MigrationLock
- type MigrationLocker
- type ModelField
- type ModelMeta
- type MySQLDialect
- func (d *MySQLDialect) AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)
- func (m *MySQLDialect) AlterTableAddColumn(table, column, dataType string) string
- func (m *MySQLDialect) AlterTableAlterColumn(table, column, newDataType string) string
- func (m *MySQLDialect) AlterTableDropColumn(table, column string) string
- func (m *MySQLDialect) BuildProcedureCall(procedure string, argCount int) string
- func (m *MySQLDialect) BuildRoutineQuery(routine string, argCount int) string
- func (m *MySQLDialect) CurrentTimestamp() string
- func (d *MySQLDialect) IntrospectSchema(ctx context.Context, exec Executor) (Schema, error)
- func (m *MySQLDialect) JSONExtract(column, path string) (string, []any, error)
- func (m *MySQLDialect) LastInsertIDQuery(table, pkColumn string) string
- func (m *MySQLDialect) LimitOffset(limit, offset int) string
- func (m *MySQLDialect) LockSuffix(opts LockOptions) (string, string, error)
- func (d *MySQLDialect) Name() string
- func (m *MySQLDialect) Placeholder(index int) string
- func (m *MySQLDialect) Placeholders(n int) []string
- func (m *MySQLDialect) Quote(identifier string) string
- func (m *MySQLDialect) RenameColumn(table, oldName, newName string) string
- func (m *MySQLDialect) RenameTable(oldName, newName string) string
- func (m *MySQLDialect) Returning(columns ...string) string
- func (m *MySQLDialect) SupportsLastInsertID() bool
- func (m *MySQLDialect) SupportsReturning() bool
- func (m *MySQLDialect) SupportsTransactionalDDL() bool
- func (m *MySQLDialect) UpsertSQL(conflictCols, updateCols []string, _ int) string
- type Nullable
- type OTelEventBus
- type OpAddCheck
- type OpAddColumn
- type OpAddForeignKey
- type OpAlterColumn
- type OpCreateIndex
- type OpCreateTable
- type OpDropCheck
- type OpDropColumn
- type OpDropForeignKey
- type OpDropIndex
- type OpDropTable
- type Operation
- type Option
- func WithCacheJitter(pct float64) Option
- func WithCacheStore(s CacheStore) Option
- func WithCacheXFetchBeta(beta float64) Option
- func WithDeadlockRetry(maxAttempts int) Option
- func WithDefaultTZ(loc *time.Location) Option
- func WithDialect(d Dialect) Option
- func WithLimits(l Limits) Option
- func WithLogger(l *slog.Logger) Option
- func WithMiddleware(m Middleware) Option
- func WithQueryObserver(o QueryObserver) Option
- func WithReplicaDownCooldown(d time.Duration) Option
- func WithReplicaStrategy(s ReplicaStrategy) Option
- func WithReplicas(dsns ...string) Option
- func WithSlowQueryThreshold(d time.Duration) Option
- type OracleDialect
- func (d *OracleDialect) AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)
- func (o *OracleDialect) AlterTableAddColumn(table, column, dataType string) string
- func (o *OracleDialect) AlterTableAlterColumn(table, column, newDataType string) string
- func (o *OracleDialect) AlterTableDropColumn(table, column string) string
- func (o *OracleDialect) BuildProcedureCall(procedure string, argCount int) string
- func (o *OracleDialect) BuildRoutineQuery(routine string, argCount int) string
- func (o *OracleDialect) CurrentTimestamp() string
- func (o *OracleDialect) IntrospectSchema(ctx context.Context, exec Executor) (Schema, error)
- func (o *OracleDialect) JSONExtract(column, path string) (string, []any, error)
- func (o *OracleDialect) LastInsertIDQuery(table, pkColumn string) string
- func (o *OracleDialect) LimitOffset(limit, offset int) string
- func (o *OracleDialect) LockSuffix(opts LockOptions) (string, string, error)
- func (o *OracleDialect) MapColumnType(t string) string
- func (d *OracleDialect) Name() string
- func (o *OracleDialect) Placeholder(index int) string
- func (o *OracleDialect) Placeholders(n int) []string
- func (o *OracleDialect) Quote(identifier string) string
- func (o *OracleDialect) ReleaseSavepointStmt(name string) string
- func (o *OracleDialect) RenameColumn(table, oldName, newName string) string
- func (o *OracleDialect) RenameTable(oldName, newName string) string
- func (o *OracleDialect) Returning(columns ...string) string
- func (o *OracleDialect) RollbackToSavepointStmt(name string) string
- func (o *OracleDialect) SavepointStmt(name string) string
- func (o *OracleDialect) SupportsLastInsertID() bool
- func (o *OracleDialect) SupportsReturning() bool
- func (o *OracleDialect) SupportsTransactionalDDL() bool
- func (o *OracleDialect) UpsertSQL(conflictCols, updateCols []string, _ int) string
- type Page
- type Plan
- type PoolOption
- type PostgresDialect
- func (d *PostgresDialect) AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)
- func (p *PostgresDialect) AlterTableAddColumn(table, column, dataType string) string
- func (p *PostgresDialect) AlterTableAlterColumn(table, column, newDataType string) string
- func (p *PostgresDialect) AlterTableDropColumn(table, column string) string
- func (p *PostgresDialect) BuildProcedureCall(procedure string, argCount int) string
- func (p *PostgresDialect) BuildRoutineQuery(routine string, argCount int) string
- func (p *PostgresDialect) CurrentTimestamp() string
- func (d *PostgresDialect) IntrospectSchema(ctx context.Context, exec Executor) (Schema, error)
- func (p *PostgresDialect) JSONExtract(column, path string) (string, []any, error)
- func (p *PostgresDialect) LastInsertIDQuery(table, pkColumn string) string
- func (p *PostgresDialect) LimitOffset(limit, offset int) string
- func (p *PostgresDialect) LockSuffix(opts LockOptions) (string, string, error)
- func (d *PostgresDialect) Name() string
- func (p *PostgresDialect) Placeholder(index int) string
- func (p *PostgresDialect) Placeholders(n int) []string
- func (p *PostgresDialect) Quote(identifier string) string
- func (p *PostgresDialect) RenameColumn(table, oldName, newName string) string
- func (p *PostgresDialect) RenameTable(oldName, newName string) string
- func (p *PostgresDialect) Returning(columns ...string) string
- func (p *PostgresDialect) SupportsLastInsertID() bool
- func (p *PostgresDialect) SupportsReturning() bool
- func (p *PostgresDialect) SupportsTransactionalDDL() bool
- func (p *PostgresDialect) UpsertSQL(conflictCols, updateCols []string, _ int) string
- type Predicate
- type Query
- func (q *Query[T]) Apply(scopes ...Scope[T]) *Query[T]
- func (q *Query[T]) AsSubquery() (*Subquery, error)
- func (q *Query[T]) Avg(column string) (float64, error)
- func (q *Query[T]) Cache(ttl time.Duration, tags ...string) *Query[T]
- func (q *Query[T]) Count() (int64, error)
- func (q *Query[T]) Create(entity *T) error
- func (q *Query[T]) CreateBatch(entities []*T) error
- func (q *Query[T]) Cursor() (*Cursor[T], error)
- func (q *Query[T]) Delete(entity *T) (int64, error)
- func (q *Query[T]) DeleteBatch(ids []any) (int64, error)
- func (q *Query[T]) DeleteBy() (int64, error)
- func (q *Query[T]) Distinct() *Query[T]
- func (q *Query[T]) Except(other *Query[T]) *Query[T]
- func (q *Query[T]) Find(id any) (T, error)
- func (q *Query[T]) First() (T, error)
- func (q *Query[T]) ForShare() *Query[T]
- func (q *Query[T]) ForUpdate() *Query[T]
- func (q *Query[T]) GroupBy(columns ...string) *Query[T]
- func (q *Query[T]) HardDelete(entity *T) (int64, error)
- func (q *Query[T]) Having(column string, operator string, value any) *Query[T]
- func (q *Query[T]) HavingAggregate(fn, column, operator string, value any) *Query[T]
- func (q *Query[T]) HavingExpr(e Expr) *Query[T]
- func (q *Query[T]) Intersect(other *Query[T]) *Query[T]
- func (q *Query[T]) Iter(fn func(T) error) error
- func (q *Query[T]) Join(table string) *JoinBuilder[T]
- func (q *Query[T]) LeftJoin(table string) *JoinBuilder[T]
- func (q *Query[T]) Limit(n int) *Query[T]
- func (q *Query[T]) List() ([]T, error)
- func (q *Query[T]) Max(column string) (float64, error)
- func (q *Query[T]) Min(column string) (float64, error)
- func (q *Query[T]) MustAsSubquery() *Subquery
- func (q *Query[T]) NoWait() *Query[T]
- func (q *Query[T]) Offset(n int) *Query[T]
- func (q *Query[T]) OnlyTrashed() *Query[T]
- func (q *Query[T]) Or(fn func(*Query[T]) *Query[T]) *Query[T]
- func (q *Query[T]) OrderBy(column string, direction string) *Query[T]
- func (q *Query[T]) Paginate(pageSize, page int) (*Page[T], error)
- func (q *Query[T]) Preload(relations ...string) *Query[T]
- func (q *Query[T]) Restore(entity *T) (int64, error)
- func (q *Query[T]) RightJoin(table string) *JoinBuilder[T]
- func (q *Query[T]) Select(columns ...string) *Query[T]
- func (q *Query[T]) SelectExpr(alias string, e Expr) *Query[T]
- func (q *Query[T]) SkipLocked() *Query[T]
- func (q *Query[T]) Sum(column string) (float64, error)
- func (q *Query[T]) Track() *TrackedQuery[T]
- func (q *Query[T]) Union(other *Query[T]) *Query[T]
- func (q *Query[T]) UnionAll(other *Query[T]) *Query[T]
- func (q *Query[T]) Unscoped() *Query[T]
- func (q *Query[T]) Update(entity *T) (int64, error)
- func (q *Query[T]) UpdateBatch(entities []*T) error
- func (q *Query[T]) UpdateFields(entity *T, fields ...string) (int64, error)
- func (q *Query[T]) UpdateMap(data map[string]any) (int64, error)
- func (q *Query[T]) Upsert(entity *T, conflictCols []string, updateCols []string) error
- func (q *Query[T]) UpsertBatch(entities []*T, conflictCols []string, updateCols []string) error
- func (q *Query[T]) Where(column string, operator string, value any) *Query[T]
- func (q *Query[T]) WhereBetween(column string, start, end any) *Query[T]
- func (q *Query[T]) WhereExpr(e Expr) *Query[T]
- func (q *Query[T]) WhereIn(column string, values []any) *Query[T]
- func (q *Query[T]) WhereJSON(column, path, operator string, value any) *Query[T]
- func (q *Query[T]) WhereNot(column string, operator string, value any) *Query[T]
- func (q *Query[T]) WhereP(preds ...Predicate) *Query[T]
- func (q *Query[T]) WhereSubquery(column, operator, subquery string) *Query[T]
- func (q *Query[T]) With(name string, sub *Subquery) *Query[T]
- func (q *Query[T]) WithRecursive(name string, sub *Subquery) *Query[T]
- func (q *Query[T]) WithTrashed() *Query[T]
- type QueryEvent
- type QueryFunc
- type QueryObserver
- type QueryRowFunc
- type RelationMeta
- type ReplicaStrategy
- type Result
- type Routine
- type Row
- type SQLGuard
- type SQLiteDialect
- func (s *SQLiteDialect) AlterTableAddColumn(table, column, dataType string) string
- func (s *SQLiteDialect) AlterTableAlterColumn(table, column, newDataType string) string
- func (s *SQLiteDialect) AlterTableDropColumn(table, column string) string
- func (s *SQLiteDialect) BuildProcedureCall(procedure string, argCount int) string
- func (s *SQLiteDialect) BuildRoutineQuery(routine string, argCount int) string
- func (s *SQLiteDialect) CurrentTimestamp() string
- func (d *SQLiteDialect) IntrospectSchema(ctx context.Context, exec Executor) (Schema, error)
- func (s *SQLiteDialect) JSONExtract(column, path string) (string, []any, error)
- func (s *SQLiteDialect) LastInsertIDQuery(table, pkColumn string) string
- func (s *SQLiteDialect) LimitOffset(limit, offset int) string
- func (s *SQLiteDialect) LockSuffix(opts LockOptions) (string, string, error)
- func (d *SQLiteDialect) Name() string
- func (s *SQLiteDialect) Placeholder(index int) string
- func (s *SQLiteDialect) Placeholders(n int) []string
- func (s *SQLiteDialect) Quote(identifier string) string
- func (s *SQLiteDialect) RenameColumn(table, oldName, newName string) string
- func (s *SQLiteDialect) RenameTable(oldName, newName string) string
- func (s *SQLiteDialect) Returning(columns ...string) string
- func (s *SQLiteDialect) SupportsLastInsertID() bool
- func (s *SQLiteDialect) SupportsReturning() bool
- func (s *SQLiteDialect) SupportsTransactionalDDL() bool
- func (s *SQLiteDialect) UpsertSQL(conflictCols, updateCols []string, _ int) string
- type SavepointDialect
- type Schema
- type SchemaIntrospector
- type Scope
- type ShardFunc
- type ShardResolver
- type ShardRouter
- type Subquery
- type SyncOptions
- type Table
- type TenantConfig
- type TenantRouter
- type TenantStrategy
- type Tracked
- type TrackedQuery
- type Tx
- func (t *Tx) Commit() error
- func (t *Tx) OnCommit(fn func(context.Context) error)
- func (t *Tx) OnRollback(fn func(context.Context) error)
- func (t *Tx) ReleaseSavepoint(name string) error
- func (t *Tx) Rollback() error
- func (t *Tx) RollbackTo(name string) error
- func (t *Tx) Savepoint(name string) error
- func (t *Tx) Tx(ctx context.Context, fn func(tx *Tx) error) error
- type TypeMapper
- type TypeOptions
- type TypedBinder
- type TypedColumn
- func (c TypedColumn[T]) Between(lo, hi T) Predicate
- func (c TypedColumn[T]) Eq(v T) Predicate
- func (c TypedColumn[T]) Gt(v T) Predicate
- func (c TypedColumn[T]) Gte(v T) Predicate
- func (c TypedColumn[T]) In(values ...T) Predicate
- func (c TypedColumn[T]) IsNotNull() Predicate
- func (c TypedColumn[T]) IsNull() Predicate
- func (c TypedColumn[T]) Lt(v T) Predicate
- func (c TypedColumn[T]) Lte(v T) Predicate
- func (c TypedColumn[T]) Name() string
- func (c TypedColumn[T]) Neq(v T) Predicate
- func (c TypedColumn[T]) NotIn(values ...T) Predicate
- type TypedScanner
- type TypedStringColumn
- type Window
Constants ¶
const GenContractVersion = 3
GenContractVersion is the version of the code-generation contract: the shape of the generated files and the signatures of the RegisterTyped* functions below. The generator stamps a matching `//quark:gen vN` header into every file it emits.
Bump this on any breaking change to the generated-file shape or the registrar signatures. Generated code that registers a different version is ignored at lookup time (the operation falls back to reflection), so a stale binary never calls into incompatible generated code.
v2 (F6-2): generated files emit a real typed scanner (read path) instead of the F6-1 stub. v3 (F6-3a): integer-PK models also emit a real INSERT binder. Older files are ignored and fall back to reflection.
const RowLevelSecurity = RowLevelSecurityClient
RowLevelSecurity is the legacy name for RowLevelSecurityClient. The constant value is identical, so existing code and serialized configs continue to work without changes.
Deprecated: use RowLevelSecurityClient. The alias is scheduled for removal in v1.0. The name change clarifies that this strategy is client-side WHERE injection, not engine-enforced RLS — see ADR-0012 and (when available) RowLevelSecurityNative for the engine-enforced PostgreSQL variant introduced in Fase 5.
Variables ¶
var ( // ErrNotFound indicates that no record was found for the given criteria. ErrNotFound = errors.New("record not found") // ErrInvalidModel indicates that the provided model is invalid or not registered. ErrInvalidModel = errors.New("invalid model") // ErrInvalidQuery indicates that the query is malformed or invalid. ErrInvalidQuery = errors.New("invalid query") // ErrInvalidIdentifier indicates that a table or column identifier is invalid. // Re-exported from internal/guard, where ValidateIdentifier wraps it with %w, // so errors.Is(err, ErrInvalidIdentifier) works for a rejected identifier from // any call site (Where/OrderBy/GroupBy columns, table names, migration ops, // CTEs, event channels…) — consistent with ErrInvalidJoin / ErrInvalidJSONPath. ErrInvalidIdentifier = guard.ErrInvalidIdentifier // ErrInvalidJSONPath indicates that a JSON path passed to WhereJSON is malformed // or contains characters that could enable SQL injection. Quark accepts dotted // paths shaped like "user.name"; see guard.ValidateJSONPath for the grammar. // Array indexes and engine-specific syntax are out of scope for WhereJSON; // use RawQuery for those. ErrInvalidJSONPath = errors.New("invalid JSON path") // ErrInvalidJoin indicates that a JOIN ... ON clause passed to Join, // LeftJoin, or RightJoin does not match the minimal identifier-only // grammar Quark accepts while a structured Join().On() builder is pending // (Phase 2 AST). See guard.ValidateJoinOn for the grammar; use a // structured Join (when available) or RawQuery for shapes outside it. ErrInvalidJoin = errors.New("invalid JOIN ON clause") // ErrStaleEntity indicates that an optimistic-locking update failed // because the row's version column had been bumped by another writer // since the entity was loaded. The caller should reload the row, replay // the change against the fresh state, and retry — or surface the // conflict to the user. Returned by Update / UpdateFields / Tracked.Save // when the model carries a quark:"version" field. ErrStaleEntity = errors.New("stale entity (optimistic-locking conflict)") // ErrUnsupportedFeature indicates that a feature is not supported by the // active database dialect. Returned by builder methods (e.g. ForUpdate // on SQLite) so callers can branch by dialect or fall back to a different // strategy. The error message includes the dialect name and the feature // being requested. ErrUnsupportedFeature = errors.New("feature not supported by dialect") // ErrInvalidTimezone indicates that a model field carries a // quark:"tz=..." tag whose value is not a valid IANA timezone name // (i.e. time.LoadLocation rejected it). It is surfaced fail-fast by // Client.RegisterModel and Client.Migrate so an invalid timezone // breaks the application at startup, not on the first query that // touches the column. The wrapped error names the field, the column // and the offending timezone string. ErrInvalidTimezone = errors.New("invalid column timezone") // ErrDialectNotSupported indicates that the database dialect is not supported. ErrDialectNotSupported = errors.New("dialect not supported") // ErrConnection indicates a database connection error. ErrConnection = errors.New("database connection error") // ErrTimeout indicates that a query timed out. ErrTimeout = errors.New("query timeout") // ErrConstraintViolation indicates a database constraint violation. ErrConstraintViolation = errors.New("constraint violation") // ErrEventEmitFailed indicates that an EventBus.Publish call failed // AFTER the originating write was already persisted. The data is // committed; only the event emission failed. Callers that see this // wrapped on a non-transactional CRUD return must NOT retry the // write (that would double-write) — they should retry the emit or // rely on the subscriber being idempotent (at-least-once delivery, // no outbox; see ADR-0013). Under an explicit transaction the emit // runs post-commit via Tx.OnCommit and the failure is logged with // the OTel-style event `quark.event.emit_failure` rather than // propagated (the commit already returned success). ErrEventEmitFailed = errors.New("event emit failed after commit") // ErrListenerClosed indicates an operation was attempted on an // EventListener (PostgreSQL LISTEN/NOTIFY) after Close was called. // The dedicated connection has been returned to the pool; create a // fresh listener via ListenerFactory.CreateListener. See ADR-0019. ErrListenerClosed = errors.New("event listener closed") // ErrNoSubscription indicates Receive was called on an EventListener // that has no active channel subscription — Listen must be called at // least once before Receive. See ADR-0019. ErrNoSubscription = errors.New("event listener has no channel subscribed") )
Common errors returned by quark operations.
var ErrGeneratedStub = errors.New("quark: generated code is an F6-1 stub; the typed fast path lands in F6-2/F6-3")
ErrGeneratedStub is returned by the placeholder scanner/binder that F6-1 generated code registers. F6-1 ships the generation pipeline and the registry contract but not the typed fast path itself (that is F6-2 for scanning and F6-3 for binding); until then the generated functions are inert. They are never reached at runtime in F6-1 because the hot paths do not yet consult the registry, and once they do, the contract-version gate keeps a v1 stub from being used by a newer runtime.
var ErrLockTimeout = errors.New("migration lock acquisition timed out")
ErrLockTimeout is returned by AcquireMigrationLock when the lock cannot be acquired within the given timeout. Distinct from ErrUnsupportedFeature (which means the dialect doesn't model distributed locks at all). Distinct from generic driver errors.
var HasPlaceholders = guard.HasPlaceholders
HasPlaceholders checks if a query string contains parameter placeholders.
Functions ¶
func Call ¶
Call executes a stored procedure that does not return a result set, but may modify OUT parameters.
func CanonicalType ¶ added in v0.11.0
CanonicalType normalizes a Go type string so the reflection renderer (reflect.Type.String) and the go/types renderer agree. They diverge on alias spellings: reflect prints byte as uint8, rune as int32, and the empty interface as "interface {}", while go/types may print "byte", "rune", and "any". Both sides run their type strings through this, so the hash is stable regardless of which spelling the source used.
func CheckGeneratedDrift ¶ added in v0.11.0
CheckGeneratedDrift reports whether model t has generated code whose recorded ModelHash no longer matches the model's current shape — i.e. the struct changed but `quark gen` was not re-run. It returns (drifted, hasGenerated): hasGenerated is false when t has no generated code at all (nothing to drift from). This is the optional stale-codegen check ADR-0014 requires F6-1 to make possible; the runtime never calls it automatically.
func DefaultShardResolver ¶ added in v1.0.0
DefaultShardResolver reads the shard key set by WithShardKey. Pass it to NewShardRouter for the common case.
func GeneratedBinderRegistered ¶ added in v0.11.0
GeneratedBinderRegistered reports whether a compatible generated binder is registered for t that actually handles inserts — i.e. a real binder, not the stub (which returns ErrGeneratedStub). Intended for tests that want to assert the generated write path is exercised rather than reflection.
func HashModelFields ¶ added in v0.11.0
func HashModelFields(fields []ModelField) string
HashModelFields returns a deterministic hash of a model's persisted shape from its fields. It is the single source of truth for the model hash: the runtime (ModelHash) and the generator both call it, so an unchanged model hashes identically on both sides. Field order does not matter.
func ModelHash ¶ added in v0.11.0
ModelHash returns a deterministic hash of model type t's persisted shape: for each exported field carrying a `db` tag, the field name, column name, primary-key flag, and Go type. It is the reflection-side caller of HashModelFields; the generator computes the same hash from the AST. The F6-1 conformance test asserts the two agree for a sample model, guarding against the two-tag-interpreters drift risk noted in ADR-0014.
func Notify ¶
func Notify(ctx context.Context, provider ClientProvider, channel, payload string) error
Notify triggers a database PubSub notification (PostgreSQL `pg_notify`). It is unrelated to EventBus.Publish — Notify is the raw LISTEN/NOTIFY emit, not a CRUD lifecycle event. Only PostgreSQL is supported; other dialects return an error.
func RegisterDialect ¶
RegisterDialect allows developers to register custom database dialects. This enables support for proprietary or non-standard databases.
Example:
quark.RegisterDialect("cockroach", myCockroachDialect)
The registered dialect can then be used with:
client, err := quark.New(db, quark.WithDialect(quark.DetectDialectByName("cockroach")))
func RegisterGeneratedMeta ¶ added in v0.11.0
func RegisterGeneratedMeta(t reflect.Type, meta GeneratedMeta)
RegisterGeneratedMeta records the contract version and model hash for a generated model. Called from the init() of a generated file; not intended for hand use.
func RegisterTypeMapper ¶ added in v0.3.0
func RegisterTypeMapper(t reflect.Type, m TypeMapper)
RegisterTypeMapper registers a custom Go-type → SQL-type mapping for the migration / sync layer. Pointer types are stripped before registration: registering for time.Duration also covers *time.Duration. Re-registering the same type overwrites the previous mapper.
Example: storing google/uuid.UUID as native UUID on Postgres and as a 36-char string on the rest:
quark.RegisterTypeMapper(reflect.TypeOf(uuid.UUID{}), func(d string, _ quark.TypeOptions) string {
if d == "postgres" {
return "UUID"
}
return "VARCHAR(36)"
})
The mapper is consulted by client.Migrate and client.Sync. database/sql's Scanner / driver.Valuer interfaces still apply to the read/write side — a type registered here must also implement those interfaces (or be already supported by the underlying driver) for round-trip to work.
func RegisterTypedBinder ¶ added in v0.11.0
func RegisterTypedBinder(t reflect.Type, fn TypedBinder)
RegisterTypedBinder records the generated insert/update binder for model type t. Called from the init() of a generated file; not intended for hand use.
func RegisterTypedScanner ¶ added in v0.11.0
func RegisterTypedScanner(t reflect.Type, fn TypedScanner)
RegisterTypedScanner records the generated row scanner for model type t. Called from the init() of a generated file; not intended for hand use.
func ScanTarget ¶ added in v0.11.0
ScanTarget returns the rows.Scan target for a model field pointer, matching the no-timezone scan behavior of the reflection path (including the string/[]byte time parsing that drivers like SQLite require). It is called by generated scanners — which run only when the per-column timezone feature is inactive, so a nil location is always correct. Not intended for hand use.
func ShardKeyFromContext ¶ added in v1.0.0
ShardKeyFromContext returns the shard key set by WithShardKey, or "".
func Sticky ¶ added in v0.13.0
Sticky returns a context that pins subsequent read queries to the primary connection instead of a read replica. Use it for read-your-writes: a read that must observe a write you just made (replicas are eventually consistent, so a normal read may be stale). Reads inside Client.Tx already use the transaction's connection and need no Sticky.
It is a no-op when no replicas are configured.
func StubBinder ¶ added in v0.11.0
StubBinder is the inert binder registered by F6-1 generated code. See ErrGeneratedStub.
func StubScanner ¶ added in v0.11.0
StubScanner is the inert scanner registered by F6-1 generated code. See ErrGeneratedStub.
Types ¶
type AfterCreateHook ¶
AfterCreateHook is executed after an entity is created.
type AfterDeleteHook ¶
AfterDeleteHook is executed after an entity is deleted.
type AfterFindHook ¶ added in v0.9.0
AfterFindHook is executed once the rows of a read query have been scanned into the result slice (or single value). The hook receives the request context and fires synchronously on the result-bearing return path; returning a non-nil error replaces the would-be-returned value with that error.
Implement on a *T where T is the model type. The hook fires exactly once per query — not once per row.
Typical uses: post-scan enrichment, lazy join hydration not covered by Query.Preload, or read auditing.
type AfterUpdateHook ¶
AfterUpdateHook is executed after an entity is updated.
type Array ¶ added in v0.6.0
type Array[T any] struct { V []T }
Array[T] is the typed wrapper for a column that holds a list of T. It round-trips through JSON regardless of dialect, so the same model definition works on every supported engine without per-dialect Scan / Value code:
Postgres → JSONB MySQL / MariaDB → JSON SQLite → TEXT SQL Server → NVARCHAR(MAX) Oracle → CLOB
The semantic clarity vs. `JSON[[]T]`: `Tags Array[string]` reads as "the column holds a list of strings"; the helper methods (`Len`, `Slice`, `Contains`) carry that intent through call sites that read the field.
Trade-off: this wrapper does NOT use Postgres' native `INT[]` / `TEXT[]` array types — operators like `@>`, `&&`, and `array_agg` won't fire on Array[T] columns. For PG-native arrays with operators drop down to `pgx`/`pgtype.Array` directly or use `RawQuery`. The "neutral wrapper" spec in TASKS § Bloque B explicitly asked for a dialect-independent type that doesn't import `pgtype`; JSON-backed is the simplest shape that satisfies that constraint.
Example:
type Post struct {
ID int64 `db:"id" pk:"true"`
Tags quark.Array[string] `db:"tags"`
}
p := Post{Tags: quark.Array[string]{V: []string{"go", "orm"}}}
_ = client.Migrate(ctx, &Post{})
_ = quark.For[Post](ctx, client).Create(&p)
func (Array[T]) Len ¶ added in v0.6.0
Len returns the number of elements in the array. Safe on the zero value (returns 0).
func (*Array[T]) Scan ¶ added in v0.6.0
Scan implements sql.Scanner. Accepts []byte and string sources (the two forms drivers return for JSON columns). NULL clears V to nil. An empty / whitespace-only payload also resolves to nil so a column default of `'[]'` round-trips through the zero value cleanly.
func (Array[T]) Slice ¶ added in v0.6.0
func (a Array[T]) Slice() []T
Slice returns the underlying slice. Mutations on the returned slice affect the Array's storage — useful for appending without re-allocating, dangerous if the Array value is used after.
func (Array[T]) Value ¶ added in v0.6.0
Value implements driver.Valuer by JSON-marshalling V. A nil V serialises to `[]` (empty array) rather than `null` — the more useful default when the column is also used in SQL operations. Pair with quark.Nullable[Array[T]] when you need to distinguish NULL from empty.
The result is a string, not []byte, for the same reason as JSON.Value: a []byte binds as VARBINARY on go-mssqldb and corrupts the NVARCHAR(MAX) column via an implicit UTF-16 reinterpretation.
type AuditConfig ¶ added in v0.9.0
type AuditConfig struct {
// UserFromContext, when set, extracts the acting user identifier
// from the request context for the `user_id` column. Returns ""
// when unknown. nil leaves user_id empty.
UserFromContext func(context.Context) string
// TenantFromContext, when set, extracts the tenant identifier for
// the `tenant_id` column. Returns "" when unknown. nil leaves
// tenant_id empty. (When using a TenantRouter the value is
// already on the context the resolver reads — reuse the same
// extractor here.)
TenantFromContext func(context.Context) string
// IncludeTables, when non-empty, restricts auditing to exactly
// these table names. Empty means "all tables" (subject to
// ExcludeTables). The audit table itself (`quark_audit`) is
// always excluded regardless of this list — auditing the audit
// log would recurse.
IncludeTables []string
// ExcludeTables names tables that are never audited. Takes
// precedence over IncludeTables.
ExcludeTables []string
}
AuditConfig configures the optional audit log (F5-7). Pass it to Client.EnableAuditLog. The zero value audits every table with empty user/tenant attribution; populate the callbacks and filters to taste.
type BackfillSpec ¶ added in v0.6.0
type BackfillSpec struct {
// Name uniquely identifies this backfill across runs. Used as
// the primary key of `quark_backfill_state`. Two backfills with
// the same Name share the same resume token — that's how a
// retry resumes. Different backfills MUST use different Names
// or they'll trample each other's state.
Name string
// Table is the source table being iterated.
Table string
// PKColumn is the column the helper orders + paginates by.
// Defaults to "id" if empty. The column must be a sortable
// integer-like type (int64 / bigint) — text PKs aren't
// supported in F3-6-core. Composite PKs aren't supported
// either; the helper takes the first PK column only.
PKColumn string
// BatchSize is the number of PKs fetched per round-trip.
// Defaults to 1000. Larger batches are more efficient but
// hold locks longer; tune for your workload.
BatchSize int
// Process is invoked once per batch with the PK list in
// ascending order. The callback can do any SQL it wants
// (typically UPDATE ... WHERE id IN (...) or similar). Errors
// abort the backfill and propagate; the state table records
// the highest PK from successful batches, so a retry resumes
// from there.
Process func(ctx context.Context, batchPKs []int64) error
}
BackfillSpec describes a single backfill operation. A backfill iterates a table by primary key in batches, invokes a user callback per batch, and persists the highest PK seen so a process kill (or a deliberate retry) resumes at the next un-processed row rather than re-running the entire table.
The user callback receives the slice of PKs in the current batch and is responsible for whatever data work the backfill needs — the helper deliberately doesn't read row contents itself. Why: backfill SQL is rarely "SELECT * + transform"; it's usually "UPDATE ... WHERE id IN (...)" or "INSERT ... SELECT ... WHERE id IN (...)" where the user knows the relevant columns and the helper would just be in the way trying to scan them generically.
type BaseMiddleware ¶
type BaseMiddleware struct{}
BaseMiddleware provides default implementations that pass through to the next handler. Embed this in your middleware so you only need to override the methods you care about.
func (BaseMiddleware) WrapExec ¶
func (BaseMiddleware) WrapExec(next ExecFunc) ExecFunc
func (BaseMiddleware) WrapQuery ¶
func (BaseMiddleware) WrapQuery(next QueryFunc) QueryFunc
func (BaseMiddleware) WrapQueryRow ¶
func (BaseMiddleware) WrapQueryRow(next QueryRowFunc) QueryRowFunc
type BaseQuery ¶
type BaseQuery struct {
// contains filtered or unexported fields
}
BaseQuery holds the non-generic state of a database query.
type BeforeCreateHook ¶
BeforeCreateHook is executed before an entity is created.
type BeforeDeleteHook ¶
BeforeDeleteHook is executed before an entity is deleted.
type BeforeFindHook ¶ added in v0.9.0
BeforeFindHook is executed before a read query is dispatched — once per call to Query.List, Query.First, Query.Find, or the streaming variants (Query.Iter / Query.Cursor). The hook receives the request context the query was constructed with and fires synchronously: returning a non-nil error aborts the query before any SQL is emitted, and the error propagates to the caller.
Implement on a *T where T is the model type. Quark resolves the hook by checking whether `&T{}` satisfies the interface — no registration needed.
Typical uses: injecting an additional tenant predicate, enabling soft-delete semantics by default, or logging read intent.
type BeforeUpdateHook ¶
BeforeUpdateHook is executed before an entity is updated.
type BindMode ¶ added in v0.11.0
type BindMode int
BindMode selects which column set a TypedBinder produces. Insert binds every persisted column; Update binds the non-PK columns of a full-row update. (Partial updates — buildUpdateMap / UpdateFields — are handled by F6-3 and may extend this enum; new values append, never reorder.)
type CacheConfig ¶
CacheConfig holds the caching parameters for a specific query.
type CacheStore ¶
type CacheStore interface {
// Get retrieves a value from the cache.
Get(ctx context.Context, key string) ([]byte, error)
// Set stores a value in the cache with a specific TTL and associated tags.
Set(ctx context.Context, key string, val []byte, ttl time.Duration, tags ...string) error
// Delete removes a specific key.
Delete(ctx context.Context, key string) error
// InvalidateTags removes all entries associated with the given tags (usually table names).
InvalidateTags(ctx context.Context, tags ...string) error
}
CacheStore defines the contract for any caching backend. Implementations should be provided in separate packages (e.g., quark/cache/redis).
type Check ¶ added in v0.6.0
Check is one CHECK constraint declared on a table. Expression is the raw catalog text — dialect-specific phrasing, parenthesisation, and whitespace. The introspector deliberately does NOT normalise the expression because expression-equivalence across dialects is an AST-level problem (`(x > 0)` vs `x > 0`, `'a' IN ('a','b')` vs `'a' = ANY (ARRAY['a','b'])`, etc.) that belongs to F3-3's diff engine, not to the catalog reader.
Name comes from the catalog. Inline anonymous checks (`age INTEGER CHECK (age > 0)` without an explicit `CONSTRAINT <name>`) get dialect-generated names (`age_check`, `CK__table__age__hash`, etc.). F3-3-core's `Diff` matches checks **by name only** — there is no fallback to expression equivalence for anonymous ones, because expression equivalence is AST-level work that's out of scope for the diff layer (each dialect emits its own canonical form; comparing them is its own problem). If your checks must round-trip cleanly cross-dialect, give them explicit `CONSTRAINT <name>` clauses in DDL. TODO(F3-3-checks-anon): consider an opt-in pass that matches anonymous checks by normalised expression once the AST equivalence work lands.
Coverage: PostgreSQL, MySQL, MariaDB, and MSSQL implement the introspector. **SQLite** returns `Checks=nil` because SQLite has no catalog for CHECK constraints — the only path is parsing `sqlite_master.sql` DDL, which is brittle and intentionally out of scope for the catalog-reader layer. A future F3-2-checks-sqlite follow-up could add DDL parsing if user demand justifies it.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is the main entry point for quark ORM operations. It wraps a database connection and provides type-safe query building.
func New ¶
New creates a new quark Client with the given driver name and data source.
Example:
client, err := quark.New("sqlite", "example.db",
quark.WithMaxOpenConns(25),
quark.WithMaxIdleConns(5),
)
For PostgreSQL:
client, err := quark.New("pgx", "postgres://user:pass@localhost/db",
quark.WithMaxOpenConns(25),
)
The dialect is auto-detected from the driver name. You can override it with WithDialect().
func (*Client) AcquireMigrationLock ¶ added in v0.6.0
func (c *Client) AcquireMigrationLock(ctx context.Context, name string, timeout time.Duration) (MigrationLock, error)
AcquireMigrationLock attempts to acquire a cluster-wide advisory lock named `name` for migration operations. The first concurrent caller wins; subsequent callers block up to `timeout` (or receive ErrLockTimeout if the timeout elapses).
Typical use:
lock, err := client.AcquireMigrationLock(ctx, "schema-migrations", 30*time.Second)
if err != nil {
return err
}
defer lock.Release(ctx)
if err := client.Migrate(ctx, &User{}, &Order{}); err != nil {
return err
}
Behaviour per dialect (see TASKS § F3-1):
- PostgreSQL: `pg_advisory_lock(hashtext(name))` on a dedicated connection. Released by `pg_advisory_unlock` on Release.
- MySQL / MariaDB: `GET_LOCK(name, timeout_seconds)` + `RELEASE_LOCK`.
- MSSQL: `sp_getapplock @LockMode='Exclusive', @LockOwner='Session'`
- `sp_releaseapplock`.
- Oracle: `DBMS_LOCK.ALLOCATE_UNIQUE` + `REQUEST(X_MODE, release_on_commit => FALSE)` — session-scoped, survives the implicit commits of DDL. Requires `GRANT EXECUTE ON DBMS_LOCK` (see ADR-0018).
- SQLite: returns `ErrUnsupportedFeature` — no distributed-lock primitive; use a `BEGIN IMMEDIATE` transaction inside the process for single-writer semantics.
func (*Client) AddForeignKey ¶
func (c *Client) AddForeignKey(ctx context.Context, table, constraintName string, columns []string, refTable string, refColumns []string, onDelete, onUpdate string) error
AddForeignKey adds a FOREIGN KEY constraint to an existing table. constraintName is the constraint identifier; refTable is the referenced table; columns and refColumns are matched by position.
Example:
client.AddForeignKey(ctx, "orders", "fk_orders_user", []string{"user_id"}, "users", []string{"id"}, "CASCADE", "SET NULL")
func (*Client) ApplyPlan ¶ added in v0.6.0
ApplyPlan executes the operations in `plan` against the database in the order they appear. It dispatches each op to the appropriate per-dialect DDL — column ops via the `Dialect.AlterTable*` helpers, index / FK / table ops via inline dispatch using the existing `Client.CreateIndex` / `Client.AddForeignKey` where applicable.
**Transactional behaviour** (F3-4-tx):
**PostgreSQL, MSSQL, SQLite** — DDL is transactional on these engines. ApplyPlan opens a BEGIN, runs all ops, and COMMITs. On ANY failure the transaction is rolled back, leaving the schema in its pre-plan state. This is the safety net users should rely on when running migrations against production.
**MySQL, MariaDB, Oracle** — DDL implicitly commits the current transaction on every statement, so wrapping is pointless. Instead, ApplyPlan uses a **resumable** path backed by a `quark_migration_state` checkpoint table (F3-4-resumable). Each successfully applied op is recorded by `(plan_hash, op_index)`; a re-invocation against the same plan (identified by `Plan.Hash()`) skips ops that were already recorded. A mid-plan failure can therefore be fixed (the underlying constraint addressed, the connection restored, etc.) and resumed simply by calling ApplyPlan again with the same plan — no re-applying earlier successful ops, no manual state management.
Drift detection: if the plan changed between runs (different ops, different table names, anything that flips the hash), the state from the prior run doesn't apply and the new plan starts fresh from op 0. This is the safety boundary that prevents "resume from op 3" against a plan whose op 3 means something different.
**Concurrency**: on non-transactional engines, two processes calling ApplyPlan against the same plan simultaneously race on the state table — both read `resumeFrom = -1`, both try to apply op 0, one (or both) of them hit DDL errors (table already exists) and a `duplicate-PK` on the state insert. `Client.AcquireMigrationLock` (F3-1) is the right primitive to serialise — wrap your ApplyPlan call:
lock, err := client.AcquireMigrationLock(ctx, "schema", 30*time.Second) if err != nil { return err } defer lock.Release(ctx) err = client.ApplyPlan(ctx, plan)
The reason this isn't done automatically inside ApplyPlan: not every dialect implements `MigrationLocker` yet (Oracle is pending) and the lock name / timeout are workflow choices that belong to the caller. Future: a `Client.MigrateAtomic` wrapper that bundles lock + plan + apply, target for F3-5.
The returned error carries the index of the op that failed and the op's String() rendering for debuggability, regardless of which path was taken.
Operation-specific caveats:
- **OpAlterColumn** today only emits DDL for the Type change via `Dialect.AlterTableAlterColumn`. Nullable and Default deltas are NOT emitted as DDL yet — they're logged as TODO and the column lands in the requested type but keeps its old nullable/default. F3-3-execute-alter follow-up will close this.
- **OpDropForeignKey on SQLite** returns `ErrUnsupportedFeature` because SQLite doesn't support `ALTER TABLE DROP CONSTRAINT`; a real drop would require a full table rebuild via the 12-step procedure documented in the SQLite manual. That belongs to F3-3-execute-sqlite-rebuild, a separate follow-up.
- **OpDropCheck on SQLite** has the same limitation as OpDropForeignKey for the same reason — returns `ErrUnsupportedFeature`.
All other ops work uniformly across the 4 CI dialects + SQLite (for the supported subset).
func (*Client) Backfill ¶ added in v0.6.0
func (c *Client) Backfill(ctx context.Context, spec BackfillSpec) error
Backfill iterates BackfillSpec.Table by primary key in batches, invoking BackfillSpec.Process for each batch. Persists the highest PK seen in `quark_backfill_state` so a kill / error / deliberate retry resumes at the next un-processed PK instead of re-running the entire table.
Workflow when something goes wrong:
- Backfill processes batches 0..N successfully; state.last_pk records the max PK from batch N.
- Batch N+1 fails (callback error, process killed, DB connection lost, etc.). State remains at batch N's max.
- Caller fixes the underlying issue and re-invokes Backfill with the same Name. The helper reads state.last_pk and starts from `WHERE pk > last_pk` — no re-processing of batches 0..N.
Once the helper sees an empty batch (no PKs left), the backfill is complete. A subsequent re-invocation with the same Name finds nothing to do and returns nil immediately — idempotent.
Concurrency: like ApplyPlan's resumable path, Backfill is not safe for concurrent invocations against the same Name. Wrap with Client.AcquireMigrationLock if you need cross-process serialisation. The state table's primary key on Name prevents silent corruption — concurrent runs would race on the UPDATE, not produce divergent state.
func (*Client) BeginTx ¶
BeginTx starts a new database transaction with the given options.
Example:
tx, err := client.BeginTx(ctx, nil)
if err != nil { log.Fatal(err) }
defer tx.Rollback()
quark.ForTx[User](ctx, tx).Create(&user)
tx.Commit()
func (*Client) Close ¶
Close closes the underlying database connection and any read-replica pools (F6-5). The primary's error is returned; replica close errors are joined so none is silently dropped.
func (*Client) CreateIndex ¶
func (c *Client) CreateIndex(ctx context.Context, table, indexName string, columns []string, unique bool) error
CreateIndex creates an index on the given table and columns. If unique is true, a UNIQUE INDEX is created. If the index already exists the error is silently ignored for compatible dialects.
Example:
client.CreateIndex(ctx, "users", "idx_users_email", []string{"email"}, true)
func (*Client) DisableAuditLog ¶ added in v0.9.0
func (c *Client) DisableAuditLog()
DisableAuditLog turns audit logging back off. Subsequent CRUD stops writing quark_audit rows; the table and its existing rows are left intact. Safe to call when auditing was never enabled.
func (*Client) EnableAuditLog ¶ added in v0.9.0
func (c *Client) EnableAuditLog(ctx context.Context, cfg AuditConfig) error
EnableAuditLog turns on the optional audit log for the Client. It migrates the `quark_audit` table (idempotent) and installs the configuration so every subsequent Create / Update / Delete writes an audit row.
The audit row is written **inline on the same connection / transaction as the CRUD operation**, not post-commit. When the operation runs inside Client.Tx, the audit row joins that transaction and commits (or rolls back) atomically with the data — you never end up with committed data lacking its audit trail, nor an audit row for rolled-back work. This is the "junto al commit, no después" guarantee from ADR-0013; it is intentionally stronger than the post-commit `OnCommit` emission used by the EventBus (F5-6), where losing an event on a crash is acceptable but losing an audit record is not. For non-transactional CRUD the audit INSERT is a separate statement immediately after the write — there is a small crash window there, closed by wrapping your writes in Client.Tx.
The diff payload depends on the operation:
- Create: the full inserted row, `{column: value}`.
- Delete: the full row being deleted, `{column: value}`.
- Update via Query.Update / Query.UpdateFields: the new values, `{column: value}` (no prior value — there is no snapshot).
- Update via Tracked.Save: a per-column delta, `{column: {"old": …, "new": …}}`, because the tracked snapshot carries the prior values.
Bulk paths (CreateBatch / UpdateBatch / DeleteBatch / DeleteBy / UpdateMap) are not audited — same scope boundary as hooks and events.
EnableAuditLog is intended to be called once at setup. Calling it again replaces the configuration.
func (*Client) Exec ¶
Exec executes a raw SQL statement (INSERT, UPDATE, DELETE, DDL). This is primarily used for migrations and schema changes.
func (*Client) IntrospectSchema ¶ added in v0.6.0
IntrospectSchema reads the current state of the database's schema and returns it as a dialect-neutral Schema. It's the first half of the F3 migration story: the diff comparator (F3-3) takes the Schema produced here plus the Schema derived from the Go models and emits the operations needed to bring them into alignment.
Supported dialects: PostgreSQL, SQLite, MySQL, MariaDB, MSSQL, Oracle. A dialect that doesn't implement SchemaIntrospector returns `ErrUnsupportedFeature`.
Surface: tables, columns (primary-key membership included via Column.PrimaryKey), non-PK indexes, foreign keys, and CHECK constraints. SQLite returns `Checks=nil` (the only catalog read it doesn't implement; see the Check godoc for the rationale).
Code that reads Schema should treat the unpopulated slices as "not yet introspected" (or, for SQLite Checks, "intentionally not surfaced"), not "no constraints exist".
func (*Client) Migrate ¶
Migrate creates tables for the given models if they don't exist. This is a simplistic auto-migration tool for development. It uses the "db" and "pk" tags to generate CREATE TABLE statements. It also creates join tables for many-to-many relations.
func (*Client) MigrateRegistered ¶ added in v0.6.0
MigrateRegistered is a convenience for `Migrate(ctx, c.RegisteredModels()...)`. Returns nil immediately if no models have been registered (no-op rather than error — letting the caller initialise the Client in stages without worrying about the registration phase).
func (*Client) PlanMigration ¶ added in v0.6.0
PlanMigration computes the Plan of operations needed to align the database schema with the Go-side models. It does NOT execute anything — the returned Plan is inert.
The pipeline is:
- Build a `desired` Schema by reflecting on the model structs (the same metadata path Client.Migrate uses to render CREATE TABLE DDL).
- Read the `current` Schema via Client.IntrospectSchema.
- Call Diff to produce the ordered op list.
- Wrap the ops in a Plan.
Surface caveats — known asymmetries between desired and current:
- **Type strings**: the desired Schema uses the migrator's `SQLTypeWithOpts` output (`BIGINT`, `VARCHAR(255)`) while the introspector returns whatever the catalog stores (lowercase, parameter-bearing, or canonical form per dialect). The diff's [normalizeType] helper collapses these to a comparable form (case-fold, PG `character varying` ↔ `varchar` alias, MySQL display-width strip), so a clean round-trip works on all 5 supported dialects since F3-3-types. Edge cases not yet normalised: PG `int8`/`int4`/`int2` aliases (don't arise from introspection; only relevant if the desired Schema is hand-constructed with those names).
- **Indexes / FKs / CHECK** declared on the model: struct tags don't yet carry index or FK metadata (CreateIndex / AddForeignKey are explicit calls). PlanMigration's `desired` Schema carries columns plus the synthesised m2m join tables (see below) — indexes and FKs present in the database but not in the model would show up as OpDropIndex / OpDropForeignKey if Diff were left to its own devices. To avoid that, PlanMigration **copies** the indexes / FKs / checks from the current schema into the desired schema before diffing, on the assumption that schema-level objects not declared in models are managed manually. A future F3-3-plan-indexes follow-up will let struct tags drive these.
- **m2m join tables ARE part of the desired schema**: for every `rel:"many_to_many"` + `m2m:"join:fk:ref_fk"` tag, the desired Schema includes the join table with the same shape Client.Migrate's createJoinTables emits (two int FK columns, composite PK). Without this, the diff would propose DROPping a table Quark itself created — destroying the join rows if the plan were applied (superapp finding, task_b03f2155). An explicit model mapping the join-table name takes precedence over the synthetic shape.
SQLite quirk: same Checks=nil handling as the rest of F3-3-core — no spurious drops when the database doesn't introspect checks.
func (*Client) PlanMigrationRegistered ¶ added in v0.6.0
PlanMigrationRegistered is a convenience for `PlanMigration(ctx, c.RegisteredModels()...)`. Returns an empty Plan when no models are registered (consistent with the IsEmpty() semantics used elsewhere — no models means nothing to plan against).
func (*Client) Raw ¶
Raw returns the underlying *sql.DB for advanced operations. Use with caution - this bypasses quark's safety features.
func (*Client) RawQuery ¶
RawQuery executes a raw SQL query with the given arguments. By default, this requires placeholders to prevent SQL injection. Enable with WithLimits(Limits{AllowRawQueries: true}).
func (*Client) RegisterModel ¶ added in v0.6.0
RegisterModel records one or more model values on the Client so follow-on calls like Client.MigrateRegistered and Client.PlanMigrationRegistered don't need the caller to re-list them. The typical setup pattern:
client, _ := quark.New(...)
client.RegisterModel(&User{}, &Order{}, &Invoice{})
if err := client.MigrateRegistered(ctx); err != nil { ... }
Each model goes through the same reflection validation as Client.Migrate (must be a struct value or pointer to struct; nil is rejected) so registration fails fast on a bad model rather than at migration time.
Calling RegisterModel multiple times APPENDS to the registry — it does NOT deduplicate. If you call `RegisterModel(&User{})` twice, Client.MigrateRegistered will see User twice. That's intentional: Quark doesn't try to be clever about identity of reflect.Type-equal values; the caller controls the list.
Safe for concurrent use — the registry is mutex-protected. In practice you'll call this once at startup, not from request handlers.
The per-Client registry is intentionally additive — it does NOT replace the global type-meta cache in `internal/schema`, which is correct as global state (deterministic per `reflect.Type`). F3-7 only adds per-Client state for "which models does this Client manage", not for "what's the cached meta of type X".
func (*Client) RegisteredModels ¶ added in v0.6.0
RegisteredModels returns a snapshot of the models registered on this Client via Client.RegisterModel. The returned slice is a COPY — mutations to the returned value don't affect the internal registry. Order matches the registration order.
Useful for introspection / debugging and for the user CLI wrappers (e.g. `quarkmigrate.Run` could accept a Client with pre-registered models instead of taking the variadic models argument — though that's not yet wired).
func (*Client) Sync ¶
Sync synchronizes the database schema with the provided models. It detects missing columns, renames, and can drop columns if safe mode is disabled.
func (*Client) Tx ¶
Tx executes fn within a transaction. If fn returns nil, the transaction is committed. If fn returns an error or panics, the transaction is rolled back.
Example:
err := client.Tx(ctx, func(tx *quark.Tx) error {
quark.ForTx[User](ctx, tx).Create(&user)
quark.ForTx[Order](ctx, tx).Create(&order)
return nil // auto-commit
})
When WithDeadlockRetry(maxAttempts) is configured (F4-7), a deadlock raised from inside fn triggers a fresh-transaction retry with exponential backoff + jitter, up to maxAttempts total attempts. Non-deadlock errors propagate immediately. Disabled by default — callers explicitly opt in to retry semantics.
func (*Client) UseEventBus ¶ added in v0.9.0
UseEventBus wires an EventBus to the Client's CRUD pipeline. After this call, every Create / Update / Delete publishes a lifecycle Event once the write is durable: post-commit via Tx.OnCommit when the operation runs inside an explicit transaction, or inline after the statement for non-transactional CRUD.
Delivery is synchronous and at-least-once with no outbox (ADR-0013). A Publish failure never rolls back the committed write. In the non-transactional path the failure surfaces to the CRUD caller wrapped in ErrEventEmitFailed; in the transactional path it is logged with the event `quark.event.emit_failure` (the commit has already returned success, so there is nothing to propagate to).
Passing nil disables emission. UseEventBus is intended to be called once at setup, before queries run.
type ClientProvider ¶
ClientProvider is an interface that provides a database client. Both *Client and *TenantRouter implement this.
type Column ¶ added in v0.6.0
Column is one column in a table.
`Type` is the raw dialect-native type string as returned by the catalog (e.g. `INTEGER`, `bigint`, `character varying(255)`, `NVARCHAR(MAX)`). Normalisation to a cross-dialect form is the diff layer's responsibility (F3-3), not the introspector's.
`Default` is `nil` when no default is set, and a `*string` when one is present — preserving the distinction between "no default" and "default is the empty string". The value is the raw dialect-native expression: a literal, a function call (`CURRENT_TIMESTAMP`, `gen_random_uuid()`), or `NULL`.
`PrimaryKey` reports whether the column is part of the table's primary key (true for every member of a composite key). Populated by Client.IntrospectSchema on all six engines and by the models→Schema pipeline behind Client.PlanMigration, and compared by Diff — a PK mismatch surfaces as an OpAlterColumn, which the executor rejects loudly (changing a PK needs a table rebuild, out of ApplyPlan's scope). `Type` stays the BARE data type even when PrimaryKey is set: the constraint rendering (PRIMARY KEY, auto-increment) is the executor's job at CREATE TABLE time, mirroring Client.Migrate.
type ColumnTypeMapper ¶ added in v1.0.0
ColumnTypeMapper is the optional Dialect interface for translating a neutral/foreign column-type string into the dialect's native form before it reaches DDL. Kept as a stand-alone interface (same pattern as SchemaIntrospector / MigrationLocker) so custom dialects don't have to grow the method to keep compiling — dialects that don't implement it leave the type string untouched.
It exists because a hand-built Plan can carry a generic type like "TEXT" (every engine except Oracle accepts it); Oracle's CLOB is the native equivalent. MapColumnType must be idempotent — a type that is already dialect-native (e.g. "NUMBER(19)") passes through unchanged.
type Cursor ¶
type Cursor[T any] struct { // contains filtered or unexported fields }
Cursor provides manual iteration over query results. Similar to sql.Rows but type-safe for model T.
func (*Cursor[T]) Close ¶
Close releases resources and notifies observers.
F5-4: when the underlying rows close cleanly (no rows.Err) and the model implements AfterFindHook, the hook fires here exactly once. A row-level error short-circuits AfterFind because the read effectively did not "succeed". Errors from AfterFind replace the would-be-returned err, mirroring Query.Iter.
type DBConn ¶ added in v0.6.0
type DBConn interface {
ExecContext(ctx context.Context, query string, args ...any) (Result, error)
QueryRowContext(ctx context.Context, query string, args ...any) Row
Close() error
}
DBConn is the per-connection subset the lock implementations consume. Wraps *sql.Conn so the locks can ExecContext and Close on a single connection without coupling to database/sql package types directly.
type DBConnector ¶ added in v0.6.0
DBConnector is the narrow subset of *sql.DB the lock implementations need. It exists so the optional-interface contract doesn't drag the full Executor surface into MigrationLocker. The only implementation of this in practice is *sql.DB itself; the alias keeps tests honest without re-exporting database/sql.
type Dialect ¶
type Dialect interface {
// Name returns the dialect name (e.g., "postgres", "mysql", "sqlite").
Name() string
// Placeholder returns the placeholder for the given parameter index.
// PostgreSQL: $1, $2, etc.
// MySQL/SQLite: ?
// MSSQL: @p1, @p2, etc.
// Oracle: :1, :2, etc.
Placeholder(index int) string
// Quote returns a quoted identifier (table/column name).
// PostgreSQL: "identifier"
// MySQL: `identifier`
// MSSQL: [identifier]
// SQLite/Oracle: "identifier"
Quote(identifier string) string
// Placeholders returns a slice of placeholders for n parameters.
Placeholders(n int) []string
// LimitOffset returns the LIMIT/OFFSET clause for the given parameters.
LimitOffset(limit, offset int) string
// SupportsReturning indicates if the dialect supports RETURNING clause.
SupportsReturning() bool
// Returning returns the RETURNING clause for the given columns.
// Returns empty string if not supported.
Returning(columns ...string) string
// SupportsLastInsertID indicates if the dialect supports LastInsertId().
SupportsLastInsertID() bool
// LastInsertIDQuery returns the query to get the last insert ID.
// Used for dialects that don't support RETURNING.
LastInsertIDQuery(table, pkColumn string) string
// CurrentTimestamp returns the SQL function for current timestamp.
CurrentTimestamp() string
// BuildRoutineQuery returns the SQL for a table-valued function or routine returning rows.
// E.g., Postgres: SELECT * FROM func($1, $2)
BuildRoutineQuery(routine string, argCount int) string
// BuildProcedureCall returns the SQL for calling a procedure (pure logic / OUT params).
// E.g., MySQL: CALL proc(?, ?)
BuildProcedureCall(procedure string, argCount int) string
// JSONExtract returns the SQL expression to extract a value from a JSON column,
// the bind args required by that expression, or an error if the path is
// malformed.
//
// The returned SQL fragment uses literal '?' as a neutral bind marker; the
// caller (typically buildWhereClause) substitutes each '?' for the dialect's
// placeholder syntax (`$N`, `?`, `@pN`, `:N`) at the appropriate arg index.
//
// The path is validated by guard.ValidateJSONPath before it reaches the
// SQL. Every dialect that can bind the path does so — never interpolating
// it — which closes the SQL-injection vector that existed while the path
// was concatenated with fmt.Sprintf. Oracle is the one exception: its
// JSON_VALUE rejects a bound path (ORA-40454), so the validated path is
// inlined as a literal, made safe by the same [A-Za-z0-9_.] restriction
// that makes Quote(validatedIdentifier) safe.
//
// Example outputs (with column "data" and path "user.name"):
// Postgres: jsonb_extract_path_text(("data")::jsonb, ?, ?) / args=["user","name"]
// MySQL: JSON_EXTRACT(`data`, ?) / args=["$.user.name"]
// SQLite: JSON_EXTRACT("data", ?) / args=["$.user.name"]
// MSSQL: JSON_VALUE([data], ?) / args=["$.user.name"]
// Oracle: JSON_VALUE("DATA", '$.user.name') / args=nil (path inlined, see above)
JSONExtract(column, path string) (sql string, args []any, err error)
// AlterTableAddColumn returns SQL to add a column to a table.
// E.g., PostgreSQL: ALTER TABLE "users" ADD COLUMN "email" VARCHAR(255)
AlterTableAddColumn(table, column, dataType string) string
// AlterTableDropColumn returns SQL to drop a column from a table.
// E.g., PostgreSQL: ALTER TABLE "users" DROP COLUMN "email"
AlterTableDropColumn(table, column string) string
// AlterTableAlterColumn returns SQL to alter a column's type.
// E.g., PostgreSQL: ALTER TABLE "users" ALTER COLUMN "email" TYPE VARCHAR(255)
AlterTableAlterColumn(table, column, newDataType string) string
// RenameColumn returns SQL to rename a column.
// E.g., PostgreSQL: ALTER TABLE "users" RENAME COLUMN "old_name" TO "new_name"
RenameColumn(table, oldName, newName string) string
// RenameTable returns SQL to rename a table.
// E.g., PostgreSQL: ALTER TABLE "users" RENAME TO "accounts"
RenameTable(oldName, newName string) string
// SupportsTransactionalDDL indicates if the dialect supports DDL in transactions.
SupportsTransactionalDDL() bool
// LockSuffix returns the SQL fragments needed to attach a pessimistic
// lock to a SELECT.
//
// - tableHint is appended after the FROM clause's table name. MSSQL
// uses this slot for `WITH (UPDLOCK, ROWLOCK)`-style hints; the
// row-level locking dialects return "" here.
// - suffix is appended at the very end of the SELECT (after ORDER BY
// and LIMIT/OFFSET) — `FOR UPDATE [SKIP LOCKED|NOWAIT]` for the
// PG/MySQL/Oracle/MariaDB family.
//
// Returning ErrUnsupportedFeature signals "this dialect doesn't speak
// pessimistic locks at this level" — SQLite is the canonical case.
// LockOptions.IsZero() input must always return ("", "", nil).
LockSuffix(opts LockOptions) (tableHint, suffix string, err error)
// UpsertSQL returns the dialect-specific upsert (INSERT … ON CONFLICT … DO UPDATE)
// fragment that is appended after the VALUES clause.
// conflictCols: columns that define the conflict target (e.g. primary key or unique index).
// updateCols: columns to update on conflict; if empty defaults to all non-conflict columns.
// argOffset: current placeholder index (1-based) so positional dialects stay in sync.
// Returns the SQL fragment and the additional argument list (for the SET clause values).
UpsertSQL(conflictCols, updateCols []string, argOffset int) string
}
Dialect defines the interface for database-specific SQL generation. Each supported database (PostgreSQL, MySQL, SQLite, etc.) implements this interface.
func DetectDialect ¶
DetectDialect attempts to auto-detect the dialect from a driver name.
func DetectDialectByName ¶
DetectDialectByName attempts to get a dialect by name from all registered dialects including custom ones. This is useful when you know the exact dialect name.
type Event ¶ added in v0.9.0
Event is a single CRUD lifecycle event published to an EventBus after the originating write is durable. Kind is one of "created", "updated", or "deleted"; Table is the affected table name; Payload is the model value involved in the operation (the inserted / updated / deleted struct pointer).
Implementations are free to type-switch on Payload to recover the concrete model. Quark emits a value that satisfies this interface; see Client.UseEventBus.
type EventBus ¶
EventBus receives CRUD lifecycle events. Implement it to route events to a logger, OpenTelemetry, or an external broker (NATS / Kafka / Redis Streams). Wire an implementation with Client.UseEventBus.
Delivery semantics are **synchronous, at-least-once, no outbox**: the event is published after the write commits (post-commit via Tx.OnCommit under an explicit transaction, or inline after the statement for non-transactional CRUD). If Publish returns an error the data is already persisted — the failure does not roll anything back. See ErrEventEmitFailed and ADR-0013 for the rationale and the explicit decision NOT to ship a transactional outbox in v0.9.
type EventListener ¶
type EventListener interface {
// Listen subscribes to a specific channel.
Listen(ctx context.Context, channel string) error
// Unlisten unsubscribes from a channel.
Unlisten(ctx context.Context, channel string) error
// Receive blocks until an event is received, returning the payload or an error.
Receive(ctx context.Context) (EventPayload, error)
// Close terminates the listener connection.
Close() error
}
EventListener defines an interface for listening to database events. This is typically implemented via PubSub mechanisms like PostgreSQL's LISTEN/NOTIFY.
type EventPayload ¶
EventPayload represents a message received from a database event channel.
type ExecFunc ¶
type ExecFunc func(ctx context.Context, exec Executor, sqlStr string, args []any) (sql.Result, error)
ExecFunc is the signature for SQL execution functions used by middleware.
type Executor ¶
type Executor interface {
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}
Executor is the common interface for *sql.DB and *sql.Tx. It allows Query[T] to execute against either a raw connection or a transaction.
type Expr ¶ added in v0.4.0
Expr is the composable expression node Phase 2's query builder rests on. Each node implements ToSQL, returning a SQL fragment with `?` as a neutral bind marker plus the args that fill those markers.
Callers (WhereExpr, HavingExpr) hand the rendered fragment to the existing buildWhereClause / substitutePathMarkers pipeline that already rewrites `?` to the dialect's placeholder syntax at the correct arg index. That keeps the AST dialect-agnostic at construction time and lets us compose deep trees without juggling indices.
The exported constructors below (Col, Lit, And, Or, Not, Cmp, Eq, Ne, Lt, Gt, Lte, Gte, In, NotIn, Func) are the v0.4 AST surface. Subqueries, Cast, and Exists arrive in the subqueries-and-CTE PR.
func And ¶ added in v0.4.0
And composes two or more expressions with logical AND. Empty And is a no-op (renders to ""). Single-element And renders the inner expression without parentheses; two or more get wrapped so precedence is explicit.
func Cmp ¶ added in v0.4.0
Cmp is the general comparison constructor. Operator goes through SQLGuard.ValidateOperator so the AST cannot smuggle arbitrary tokens into the SQL surface.
func Col ¶ added in v0.4.0
Col references a column by name. The name is validated through SQLGuard.ValidateIdentifier — the AST inherits the same identifier safety the rest of the builder enforces. The wildcard "*" is accepted as-is for use inside aggregate calls (e.g. Func("COUNT", Col("*"))).
func Eq ¶ added in v0.4.0
Eq, Ne, Lt, Gt, Lte, Gte are the syntactic shortcuts for Cmp with the most common operators. Built on top of Lit / Col for the typical "Col(x) = Lit(v)" shape.
func Exists ¶ added in v0.4.0
Exists renders `EXISTS (<subquery>)`. Typically used as a top-level WhereExpr predicate.
Example:
hasOrders, _ := quark.For[Order](ctx, client).
Where("user_id", "=", quark.Col("users.id")).
Select("1").
AsSubquery()
q := quark.For[User](ctx, client).WhereExpr(quark.Exists(hasOrders))
(Correlated subqueries require the inner query to reference the outer table by qualified name; see the JoinOn grammar for what's accepted.)
func Func ¶ added in v0.4.0
Func calls a SQL function. The name is normalised to upper-case and matched against a whitelist; unknown names return ErrInvalidQuery rather than reaching the SQL surface. Empty argument list is allowed — emit a bare "FUN()" — for COUNT(*), use Col("*") explicitly.
func In ¶ added in v0.4.0
In renders "lhs IN (v1, v2, …)". Empty values list is a logic error and returns ErrInvalidQuery — `WHERE x IN ()` is non-portable across dialects (Postgres errors, MySQL silently matches nothing) so we refuse to emit it. Use a no-rows query instead.
func InSub ¶ added in v0.4.0
InSub renders `lhs IN (<subquery>)`. Useful for the common `WHERE x IN (SELECT id FROM ...)` shape; the inner Subquery should `Select("…")` exactly one column for the comparison.
func Lag ¶ added in v0.4.0
Lag renders `LAG(<col>, <offset>)`. The offset is bound as a parameter so the path is uniform with the rest of the AST — no SQL-surface integers, no per-dialect numeric formatting concerns.
func Lit ¶ added in v0.4.0
Lit binds a Go value as a SQL parameter. The value never reaches the SQL surface — it always travels through args, regardless of how nested the expression tree is.
func Not ¶ added in v0.4.0
Not negates an expression. Renders as "NOT (<inner>)" so precedence is explicit.
func Or ¶ added in v0.4.0
Or composes two or more expressions with logical OR. Same parenthesis rules as And.
func Over ¶ added in v0.4.0
Over wraps an inner Expr with a Window: `<inner> OVER (<window>)`. Typical use is wrapping a window-function leaf (RowNumber, Rank, DenseRank, Lag, Lead) but any aggregate function from the AST whitelist (`COUNT`, `SUM`, etc.) is also valid as the inner — the SQL spec defines them all as windowable.
type ForeignKey ¶ added in v0.6.0
type ForeignKey struct {
Name string
Columns []string
RefTable string
RefColumns []string
OnDelete string
OnUpdate string
}
ForeignKey is one FOREIGN KEY constraint declared on a table. It captures the surface that Client.AddForeignKey takes as input so the diff comparator (F3-3) can match introspected FKs against Go-side declarations symmetrically.
Columns and RefColumns are positionally matched — `Columns[i]` references `RefColumns[i]` (FK constraints can span multiple columns, e.g. composite FKs).
OnDelete / OnUpdate are normalised to the SQL-standard verbose form (`"CASCADE"`, `"SET NULL"`, `"SET DEFAULT"`, `"RESTRICT"`, `"NO ACTION"`) regardless of how the underlying catalog encodes them (PG single-char `confdeltype`, MSSQL `delete_referential_action_desc` with underscores, etc.). The empty string never appears here in practice — every catalog returns a verbose label.
Catalog asymmetry to know about: when a foreign key is declared without an explicit ON DELETE/ON UPDATE clause, **MariaDB** stores the SQL-standard default as `"RESTRICT"` in `INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS`, while **MySQL**, **PostgreSQL**, **MSSQL**, and **SQLite** store it as `"NO ACTION"`. In SQL semantics RESTRICT and NO ACTION are equivalent in immediate-check mode (the only mode these engines support); the difference is purely how each catalog labels the default. The introspector reports what the catalog says rather than normalising to a single canonical form — the diff layer (F3-3) treats the two as equivalent on the MySQL/MariaDB side.
Name comes from the catalog. SQLite returns `""` for inline FKs declared without an explicit `CONSTRAINT <name>` clause; the diff layer handles unnamed FKs by matching on (columns, ref_table, ref_columns) tuples.
type GeneratedMeta ¶ added in v0.11.0
type GeneratedMeta struct {
// ContractVersion is the GenContractVersion the file was generated for.
ContractVersion int
// ModelHash is ModelHash(t) computed at generation time.
ModelHash string
}
GeneratedMeta carries the bookkeeping a generated file registers alongside its scanner/binder: the contract version it was emitted against and a hash of the model's shape at generation time. The hash lets an optional drift check (CheckGeneratedDrift) warn when a model has changed but `quark gen` was not re-run.
type Index ¶ added in v0.6.0
Index is one secondary (non-primary-key) index on a table. The PK is a constraint rather than an index in the diff model and lives on the Column (via Column.PrimaryKey), not here. The introspector deliberately filters PK-backing indexes per dialect so `Table.Indexes` only carries what F3-3 diffs need to add/drop.
Columns is the ordered list of column names as the index defines them (the order is significant for B-tree indexes — a (a,b) index is not the same as (b,a)).
type JSON ¶ added in v0.3.0
type JSON[T any] struct { V T }
JSON[T] wraps a Go value so it round-trips through a SQL JSON column via encoding/json. Use it when you want a typed view of a JSON-shaped column without writing scan / value code by hand.
JSON implements database/sql.Scanner and database/sql/driver.Valuer, so every driver Quark supports handles the round-trip with no extra reflect in Quark's hot paths. The migrate layer detects JSON[T] and emits the dialect-native JSON column type:
Postgres → JSONB MySQL / MariaDB → JSON SQLite → TEXT (with json_* functions available at query time) SQL Server → NVARCHAR(MAX) Oracle → CLOB
Example:
type Settings struct{ Theme string `json:"theme"`; Volume int `json:"volume"` }
type Profile struct {
ID int64 `db:"id" pk:"true"`
Settings quark.JSON[Settings] `db:"settings"`
}
p := Profile{Settings: quark.JSON[Settings]{V: Settings{Theme: "dark", Volume: 7}}}
_ = client.Migrate(ctx, &Profile{})
_ = quark.For[Profile](ctx, client).Create(&p)
func (*JSON[T]) Scan ¶ added in v0.3.0
Scan implements sql.Scanner. Accepts []byte and string sources (the two forms drivers return for JSON columns). NULL clears V to its zero value rather than erroring; pair with quark.Nullable[JSON[T]] when you want to distinguish NULL from "valid but empty" payloads.
func (JSON[T]) Value ¶ added in v0.3.0
Value implements driver.Valuer by JSON-marshalling V into the column.
The marshalled JSON is returned as a string, not []byte: go-mssqldb binds a []byte parameter as VARBINARY, and storing that into the NVARCHAR(MAX) JSON column triggers an implicit VARBINARY→NVARCHAR conversion that reinterprets the UTF-8 bytes as UTF-16 and corrupts the payload. Returning a string binds as NVARCHAR (and as the equivalent text type on every other driver), so the round-trip is clean across all dialects.
type JoinBuilder ¶ added in v0.4.0
type JoinBuilder[T any] struct { // contains filtered or unexported fields }
JoinBuilder is the structured form returned by Join, LeftJoin, and RightJoin. Complete the JOIN by chaining `.On(left, op, right)` (the typed identifier form) or `.OnRaw(onClause)` (the legacy free-form string for ON clauses outside the simple binary grammar). Both chain-terminate by returning *Query[T] so subsequent builder calls pick up where the JOIN left off.
JoinBuilder values are immutable; the underlying *Query[T] is cloned before the JOIN is appended, matching the rest of the builder's thread-safety contract.
func (*JoinBuilder[T]) On ¶ added in v0.4.0
func (b *JoinBuilder[T]) On(left, op, right string) *Query[T]
On appends an INNER/LEFT/RIGHT JOIN with a single binary identifier comparison as the ON clause: `<left> <op> <right>`. The three arguments are concatenated as `left + " " + op + " " + right` and the resulting clause is validated as a whole against `guard.ValidateJoinOn` at exec time, surfacing `ErrInvalidJoin` for any shape outside the identifier-only grammar (literal RHS, function calls, parens, comments, mismatched operators). The grammar accepts the binary comparison operators `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=` and AND-chained compound clauses.
Most JOINs need only this form — the typical `users.id = orders.user_id` shape. For multi-condition ON clauses or any expression the ValidateJoinOn grammar accepts (AND-chained identifier comparisons), use `OnRaw`.
Example:
quark.For[Order](ctx, client).
Join("users").On("users.id", "=", "orders.user_id").
List()
func (*JoinBuilder[T]) OnRaw ¶ added in v0.4.0
func (b *JoinBuilder[T]) OnRaw(onClause string) *Query[T]
OnRaw appends the JOIN with a free-form ON clause string. The clause must match the minimal identifier-only grammar that `guard.ValidateJoinOn` enforces (AND-chained binary comparisons of qualified identifiers, e.g. `users.id = orders.user_id AND users.tenant_id = orders.tenant_id`). Literals, function calls, subqueries, and parentheses are rejected. Drop down to `RawQuery` for shapes outside this grammar.
OnRaw is the migration path for callers of the v0.3.x string-raw `Join(table, onClause)`: rewrite as `Join(table).OnRaw(onClause)`.
type Limits ¶
type Limits struct {
MaxQueryLength int
MaxResults int
MaxJoins int
MaxWhereConditions int
QueryTimeout time.Duration
AllowRawQueries bool
SafeMigrations bool
}
Limits defines security and performance limits for queries.
type ListenerFactory ¶ added in v0.9.0
type ListenerFactory struct {
// contains filtered or unexported fields
}
ListenerFactory is a dialect-agnostic factory for creating [EventListener]s over a database PubSub channel (PostgreSQL LISTEN/NOTIFY). It is unrelated to the EventBus CRUD-event interface above — this is the *inbound* channel-listener side.
Renamed from the v0.8.0 `EventBus` struct in v0.9.0 to free the `EventBus` name for the CRUD-event interface. The PostgreSQL listener is implemented over a dedicated pool connection (ADR-0019); other dialects return ErrDialectNotSupported.
func NewListenerFactory ¶ added in v0.9.0
func NewListenerFactory(client *Client) *ListenerFactory
NewListenerFactory creates a ListenerFactory for the given client.
Renamed from `NewEventBus` in v0.9.0; see ListenerFactory.
func (*ListenerFactory) CreateListener ¶ added in v0.9.0
func (f *ListenerFactory) CreateListener() (EventListener, error)
CreateListener returns an EventListener for the client's dialect.
PostgreSQL is supported: the listener pins a dedicated connection from the pool (acquired lazily on the first Listen) and consumes notifications via pgx's WaitForNotification (ADR-0019). It is single-goroutine — see EventListener and pgListener. Every other dialect returns ErrDialectNotSupported: LISTEN/NOTIFY has no portable equivalent and Quark does not emulate it with polling.
type LockMode ¶ added in v0.4.0
type LockMode int
LockMode is the kind of pessimistic lock requested for a SELECT.
const ( // LockNone means no lock clause is emitted (the default). LockNone LockMode = iota // LockForUpdate locks the rows for update; other transactions cannot // read-with-lock or write the matching rows until the current // transaction commits or rolls back. Most engines support it. LockForUpdate // read-with-lock but not write. Supported on PG / MySQL 8+ / MariaDB; // not on SQLite. MSSQL approximates with HOLDLOCK. LockForShare )
type LockOptions ¶ added in v0.4.0
LockOptions describes the pessimistic-lock behaviour for a SELECT. The zero value (LockMode == LockNone) emits nothing — callers opt in via ForUpdate / ForShare on Query[T].
func (LockOptions) IsZero ¶ added in v0.4.0
func (o LockOptions) IsZero() bool
IsZero reports whether the options request no lock at all. Used by dialects to short-circuit their LockSuffix implementations.
type LoggerEventBus ¶ added in v0.9.0
type LoggerEventBus struct {
// contains filtered or unexported fields
}
LoggerEventBus is an in-tree EventBus that writes each event to a slog.Logger at Info level. Useful for development, smoke tests, and as a reference implementation for custom buses.
func NewLoggerEventBus ¶ added in v0.9.0
func NewLoggerEventBus(logger *slog.Logger) *LoggerEventBus
NewLoggerEventBus returns a LoggerEventBus writing to logger. A nil logger falls back to slog.Default.
type MSSQLDialect ¶
type MSSQLDialect struct {
// contains filtered or unexported fields
}
MSSQLDialect implements the Microsoft SQL Server dialect.
func (*MSSQLDialect) AcquireMigrationLock ¶ added in v0.6.0
func (d *MSSQLDialect) AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)
AcquireMigrationLock uses `sp_getapplock` with @LockOwner='Session', scoped to the dedicated connection's session. Returns an integer status: 0 (granted immediately), 1 (granted after wait), -1 (timeout), -2 (cancel), -3 (deadlock), -999 (parameter / fatal error). We map -1 to `ErrLockTimeout` and the others to descriptive errors.
Timeout is in milliseconds; we round the Go Duration to ms.
func (*MSSQLDialect) AlterTableAddColumn ¶
func (m *MSSQLDialect) AlterTableAddColumn(table, column, dataType string) string
func (*MSSQLDialect) AlterTableAlterColumn ¶
func (m *MSSQLDialect) AlterTableAlterColumn(table, column, newDataType string) string
func (*MSSQLDialect) AlterTableDropColumn ¶
func (m *MSSQLDialect) AlterTableDropColumn(table, column string) string
func (*MSSQLDialect) BuildProcedureCall ¶
func (m *MSSQLDialect) BuildProcedureCall(procedure string, argCount int) string
func (*MSSQLDialect) BuildRoutineQuery ¶
func (m *MSSQLDialect) BuildRoutineQuery(routine string, argCount int) string
func (*MSSQLDialect) CurrentTimestamp ¶
func (m *MSSQLDialect) CurrentTimestamp() string
func (*MSSQLDialect) IntrospectSchema ¶ added in v0.6.0
IntrospectSchema reads the MSSQL schema via `sys.tables`, `sys.columns`, `sys.types`, and `sys.default_constraints`. MSSQL stores defaults in a separate catalog joined on parent object_id + column_id, so default-extraction needs a LEFT JOIN; everything else is a straight catalog read.
MSSQL caveats handled here:
- System-shipped tables are filtered via `is_ms_shipped = 0` plus `name NOT LIKE 'quark[_]%' ESCAPE`-style char class (the `[_]` bracket prevents `_` from being interpreted as the wildcard).
- Type reassembly: `sys.types` returns the bare type name (`varchar`, `decimal`); we glue `(N)` / `(P,S)` / `(MAX)` onto it from the adjacent columns. `max_length = -1` is the MSSQL convention for VARCHAR(MAX) / NVARCHAR(MAX). For nvarchar, `max_length` is bytes (chars * 2), so we divide by 2 when emitting the displayed type — matches what a user would write in DDL.
- Default values: MSSQL wraps them in parens (`(0)`, `(getdate())`, `('draft')`). We pass that through raw — the diff layer (F3-3) is responsible for unwrapping if it needs to compare against the Go-side DDL.
func (*MSSQLDialect) JSONExtract ¶
func (m *MSSQLDialect) JSONExtract(column, path string) (string, []any, error)
func (*MSSQLDialect) LastInsertIDQuery ¶
func (m *MSSQLDialect) LastInsertIDQuery(table, pkColumn string) string
func (*MSSQLDialect) LimitOffset ¶
func (m *MSSQLDialect) LimitOffset(limit, offset int) string
func (*MSSQLDialect) LockSuffix ¶ added in v0.4.0
func (m *MSSQLDialect) LockSuffix(opts LockOptions) (string, string, error)
MSSQL uses table hints attached to the FROM clause (not a SELECT suffix). Single-row pessimistic-style locks come from (UPDLOCK, ROWLOCK) for ForUpdate and (HOLDLOCK, ROWLOCK) for ForShare. SkipLocked maps to READPAST; NoWait has no direct hint, so it errors out rather than silently blocking.
func (*MSSQLDialect) Placeholder ¶
func (m *MSSQLDialect) Placeholder(index int) string
func (*MSSQLDialect) Placeholders ¶
func (m *MSSQLDialect) Placeholders(n int) []string
func (*MSSQLDialect) Quote ¶
func (m *MSSQLDialect) Quote(identifier string) string
func (*MSSQLDialect) ReleaseSavepointStmt ¶ added in v1.1.0
func (m *MSSQLDialect) ReleaseSavepointStmt(name string) string
func (*MSSQLDialect) RenameColumn ¶
func (m *MSSQLDialect) RenameColumn(table, oldName, newName string) string
func (*MSSQLDialect) RenameTable ¶
func (m *MSSQLDialect) RenameTable(oldName, newName string) string
func (*MSSQLDialect) Returning ¶
func (m *MSSQLDialect) Returning(columns ...string) string
func (*MSSQLDialect) RollbackToSavepointStmt ¶ added in v1.1.0
func (m *MSSQLDialect) RollbackToSavepointStmt(name string) string
func (*MSSQLDialect) SavepointStmt ¶ added in v1.1.0
func (m *MSSQLDialect) SavepointStmt(name string) string
func (*MSSQLDialect) SupportsLastInsertID ¶
func (m *MSSQLDialect) SupportsLastInsertID() bool
func (*MSSQLDialect) SupportsReturning ¶
func (m *MSSQLDialect) SupportsReturning() bool
func (*MSSQLDialect) SupportsTransactionalDDL ¶
func (m *MSSQLDialect) SupportsTransactionalDDL() bool
func (*MSSQLDialect) UpsertSQL ¶
func (m *MSSQLDialect) UpsertSQL(conflictCols, updateCols []string, _ int) string
UpsertSQL for MSSQL: uses MERGE statement appended as a WITH-style hint. MSSQL requires MERGE syntax which cannot be appended to a plain INSERT, so we return a marker that buildUpsert handles specially.
type MariaDBDialect ¶
type MariaDBDialect struct {
MySQLDialect // embed MySQL — identical wire protocol and driver
}
MariaDBDialect implements the MariaDB dialect. MariaDB is a fork of MySQL with significant additions:
- RETURNING clause in INSERT/DELETE/UPDATE (10.5+)
- Native sequences via CREATE SEQUENCE (10.3+)
- Temporal tables / system-versioned tables (10.3.4+)
- JSON_TABLE support (10.6+)
- INTERSECT / EXCEPT set operations (10.3+)
- Descending indexes (10.6+)
- UUID() and UUID_SHORT() built-ins
- IGNORE INDEX / USE INDEX hints identical to MySQL
func (*MariaDBDialect) AcquireMigrationLock ¶ added in v0.6.0
func (d *MariaDBDialect) AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)
func (*MariaDBDialect) AlterTableAlterColumn ¶
func (m *MariaDBDialect) AlterTableAlterColumn(table, column, newDataType string) string
AlterTableAlterColumn uses MODIFY COLUMN (same as MySQL).
func (*MariaDBDialect) CreateSequence ¶
func (m *MariaDBDialect) CreateSequence(name string, start, increment int64) string
CreateSequence returns the DDL to create a named sequence (MariaDB 10.3+).
func (*MariaDBDialect) CreateSystemVersionedTable ¶
func (m *MariaDBDialect) CreateSystemVersionedTable(table string, columnDefs string) string
CreateSystemVersionedTable returns the DDL for a system-versioned (temporal) table. Requires MariaDB 10.3.4+.
func (*MariaDBDialect) HistoryBetween ¶
func (m *MariaDBDialect) HistoryBetween(table, from, to string) string
HistoryBetween returns SELECT … FOR SYSTEM_TIME BETWEEN for a time range.
func (*MariaDBDialect) HistoryQuery ¶
func (m *MariaDBDialect) HistoryQuery(table string) string
HistoryQuery returns SELECT … FOR SYSTEM_TIME ALL to query full row history.
func (*MariaDBDialect) IntrospectSchema ¶ added in v0.6.0
func (*MariaDBDialect) JSONExtract ¶
func (m *MariaDBDialect) JSONExtract(column, path string) (string, []any, error)
JSONExtract uses the MariaDB / MySQL JSON_VALUE syntax (10.2.3+). MariaDB also accepts the arrow operator col->>'$.key' from 10.4.3+.
func (*MariaDBDialect) JSONTable ¶
func (m *MariaDBDialect) JSONTable(source, path string, columns ...string) string
JSONTable returns a JSON_TABLE expression (MariaDB 10.6+). source: SQL expression producing JSON; path: root path e.g. '$[*]'; columns: column definitions e.g. "id INT PATH '$.id'".
The path is validated against guard.ValidateJSONTablePath (JSONPath grammar rooted at "$"). source and columns must be trusted strings — the JSON_TABLE row syntax intermixes column types and PATH literals, so binding it as a parameter is not possible. Callers MUST NOT pass user-controlled values for source or columns. If invalid, the returned SQL embeds an obvious sentinel that fails parsing at execution time, surfacing the misuse rather than silently producing executable injection.
TODO(public-api): when JSONTable graduates from internal-only to a public builder, change the signature to return (string, error) so callers can detect validation failure with errors.Is rather than scanning the SQL for JSON_TABLE_PATH_INVALID.
func (*MariaDBDialect) LastInsertIDQuery ¶
func (m *MariaDBDialect) LastInsertIDQuery(table, pkColumn string) string
LastInsertIDQuery is kept as fallback for engines older than 10.5.
func (*MariaDBDialect) LimitOffset ¶
func (m *MariaDBDialect) LimitOffset(limit, offset int) string
LimitOffset for MariaDB uses standard LIMIT … OFFSET … syntax (unlike MySQL which uses LIMIT offset, count).
func (*MariaDBDialect) LockSuffix ¶ added in v0.4.0
func (m *MariaDBDialect) LockSuffix(opts LockOptions) (string, string, error)
MariaDB pessimistic locking. FOR UPDATE [SKIP LOCKED|NOWAIT] works on MariaDB 10.6+ just like MySQL. Shared locks are the one divergence: MariaDB has no FOR SHARE (that is MySQL-8 syntax — it raises Error 1064); it uses the older LOCK IN SHARE MODE, which does not accept SKIP LOCKED or NOWAIT. So emit LOCK IN SHARE MODE for ForShare, and reject the modifiers rather than emitting SQL MariaDB can't parse. (BB-3)
func (*MariaDBDialect) NextVal ¶
func (m *MariaDBDialect) NextVal(sequenceName string) string
NextVal returns the SQL expression that reads the next value from a sequence.
func (*MariaDBDialect) RenameColumn ¶
func (m *MariaDBDialect) RenameColumn(table, oldName, newName string) string
RenameColumn uses the standard SQL syntax supported since MariaDB 10.4.2.
func (*MariaDBDialect) Returning ¶
func (m *MariaDBDialect) Returning(columns ...string) string
Returning generates a RETURNING clause compatible with MariaDB 10.5+.
func (*MariaDBDialect) SupportsLastInsertID ¶
func (m *MariaDBDialect) SupportsLastInsertID() bool
SupportsLastInsertID returns false when RETURNING is used. The ORM prefers RETURNING over LAST_INSERT_ID() for MariaDB.
func (*MariaDBDialect) SupportsReturning ¶
func (m *MariaDBDialect) SupportsReturning() bool
SupportsReturning returns true: MariaDB 10.5+ supports RETURNING in INSERT … RETURNING, DELETE … RETURNING and UPDATE … RETURNING.
func (*MariaDBDialect) SupportsTransactionalDDL ¶
func (m *MariaDBDialect) SupportsTransactionalDDL() bool
SupportsTransactionalDDL returns false — MariaDB (like MySQL) performs implicit commits around DDL statements.
type Middleware ¶
type Middleware interface {
WrapExec(next ExecFunc) ExecFunc
WrapQuery(next QueryFunc) QueryFunc
WrapQueryRow(next QueryRowFunc) QueryRowFunc
}
Middleware wraps query execution for cross-cutting concerns like logging, retry logic, caching, rate limiting, etc. It intercepts all types of database interactions (Exec, Query, QueryRow).
type MigrationLock ¶ added in v0.6.0
type MigrationLock interface {
// Release relinquishes the lock and returns the underlying
// connection to the pool. Safe to call multiple times; subsequent
// calls are no-ops. Returns an error only if the release RPC fails
// — not if the lock was already released.
Release(ctx context.Context) error
}
MigrationLock is the handle returned by Client.AcquireMigrationLock. The caller must invoke Release before the *Client is closed; the lock is held by a dedicated connection for its entire lifetime so a process panic / Client.Close releases it automatically through the underlying driver's session teardown.
The lock guarantees mutual exclusion across processes sharing the same database. Concurrent acquirers of the same `name` block up to the requested timeout; the first one wins, the rest receive `ErrLockTimeout` if the timeout elapses.
type MigrationLocker ¶ added in v0.6.0
type MigrationLocker interface {
AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)
}
MigrationLocker is the optional interface a Dialect implements to support distributed migration locks. PG / MySQL / MariaDB / MSSQL / Oracle implement it; SQLite does not.
Kept as an optional interface — not a required method on Dialect — so custom Dialect implementations downstream don't have to grow this method to keep compiling. They opt in if and when they need distributed-lock support.
type ModelField ¶ added in v0.11.0
type ModelField struct {
// Name is the Go struct field name.
Name string
// Column is the column name from the db tag (sizing options stripped).
Column string
// GoType is the field's Go type, run through CanonicalType.
GoType string
// IsPK reports whether the field carries `pk:"true"`.
IsPK bool
}
ModelField is one persisted field of a model, reduced to the attributes that define the model's shape. Both the runtime (from reflection) and the generator (from the AST) build a slice of these and feed it to HashModelFields, so the two derive identical hashes for an unchanged model. Exported so the generator can construct and hash fields with the exact same algorithm as the runtime.
type ModelMeta ¶
ModelMeta is the cached metadata for a model struct.
func GetModelMeta ¶
GetModelMeta returns the cached metadata for model type T.
func GetModelMetaByType ¶
GetModelMetaByType returns the cached metadata for a reflect.Type.
type MySQLDialect ¶
type MySQLDialect struct {
// contains filtered or unexported fields
}
MySQLDialect implements the MySQL dialect.
func (*MySQLDialect) AcquireMigrationLock ¶ added in v0.6.0
func (d *MySQLDialect) AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)
AcquireMigrationLock uses MySQL's `GET_LOCK(name, timeout_seconds)`, which is session-bound (released when the connection ends). Returns 1 on success, 0 on timeout, NULL on error. We dedicate a connection for the lock's lifetime; `Release` calls `RELEASE_LOCK` and returns the connection to the pool.
Timeout argument: GET_LOCK accepts seconds (negative = wait forever); we round Duration to seconds. Sub-second timeouts are rounded UP to 1 second — the next-best approximation of the caller's intent given the protocol granularity.
MariaDB shares MySQL's GET_LOCK semantics — same code path.
func (*MySQLDialect) AlterTableAddColumn ¶
func (m *MySQLDialect) AlterTableAddColumn(table, column, dataType string) string
func (*MySQLDialect) AlterTableAlterColumn ¶
func (m *MySQLDialect) AlterTableAlterColumn(table, column, newDataType string) string
func (*MySQLDialect) AlterTableDropColumn ¶
func (m *MySQLDialect) AlterTableDropColumn(table, column string) string
func (*MySQLDialect) BuildProcedureCall ¶
func (m *MySQLDialect) BuildProcedureCall(procedure string, argCount int) string
func (*MySQLDialect) BuildRoutineQuery ¶
func (m *MySQLDialect) BuildRoutineQuery(routine string, argCount int) string
func (*MySQLDialect) CurrentTimestamp ¶
func (m *MySQLDialect) CurrentTimestamp() string
func (*MySQLDialect) IntrospectSchema ¶ added in v0.6.0
IntrospectSchema reads the MySQL/MariaDB schema using `INFORMATION_SCHEMA.TABLES` and `INFORMATION_SCHEMA.COLUMNS`. Both engines share the same catalog structure for the column-level surface, so a single implementation covers them (the two Dialect types just delegate here).
MySQL caveats handled here:
- Scope: `TABLE_SCHEMA = DATABASE()` — the current database, which is MySQL's analogue of PG's `current_schema()`. Cross-database introspection is out of scope (caller would need to switch DBs explicitly).
- Type representation: we use `COLUMN_TYPE` (full type string with parameters and modifiers — `int(11) unsigned`, `varchar(255)`, `decimal(10,2)`) instead of reassembling from `DATA_TYPE`. MySQL returns this verbatim, which means the round-trip vs the Go migrate-side DDL is comparable without per-type switches.
- System tables: MySQL exposes `mysql`, `information_schema`, `performance_schema`, `sys` as system databases. Our scope is the user's current DB, so those don't surface; we additionally filter `quark_%` for our internal tables.
func (*MySQLDialect) JSONExtract ¶
func (m *MySQLDialect) JSONExtract(column, path string) (string, []any, error)
func (*MySQLDialect) LastInsertIDQuery ¶
func (m *MySQLDialect) LastInsertIDQuery(table, pkColumn string) string
func (*MySQLDialect) LimitOffset ¶
func (m *MySQLDialect) LimitOffset(limit, offset int) string
func (*MySQLDialect) LockSuffix ¶ added in v0.4.0
func (m *MySQLDialect) LockSuffix(opts LockOptions) (string, string, error)
MySQL 8.0+ supports SKIP LOCKED and NOWAIT. Older 5.x ignores those modifiers (driver does not error; lock still taken).
func (*MySQLDialect) Placeholder ¶
func (m *MySQLDialect) Placeholder(index int) string
func (*MySQLDialect) Placeholders ¶
func (m *MySQLDialect) Placeholders(n int) []string
func (*MySQLDialect) Quote ¶
func (m *MySQLDialect) Quote(identifier string) string
func (*MySQLDialect) RenameColumn ¶
func (m *MySQLDialect) RenameColumn(table, oldName, newName string) string
func (*MySQLDialect) RenameTable ¶
func (m *MySQLDialect) RenameTable(oldName, newName string) string
func (*MySQLDialect) Returning ¶
func (m *MySQLDialect) Returning(columns ...string) string
func (*MySQLDialect) SupportsLastInsertID ¶
func (m *MySQLDialect) SupportsLastInsertID() bool
func (*MySQLDialect) SupportsReturning ¶
func (m *MySQLDialect) SupportsReturning() bool
func (*MySQLDialect) SupportsTransactionalDDL ¶
func (m *MySQLDialect) SupportsTransactionalDDL() bool
type Nullable ¶ added in v0.3.0
Nullable[T] is a generic wrapper for a column value that may be SQL NULL. It is a thin alias of database/sql.Null[T] (Go 1.22+) so a Nullable[T] already implements both database/sql.Scanner and database/sql/driver.Valuer — drivers handle the round-trip through their existing fast paths and Quark's reflect-based scan / write code does not need to special-case it.
Replace the long-standing pointer-as-nullable idiom (e.g. *time.Time, *string) with Nullable[T] when you want explicit "is set" semantics without a heap allocation per field. The migrate layer recognises the type and emits the SQL type for T (no NOT NULL, since the column is nullable by definition).
Example:
type Profile struct {
ID int64 `db:"id" pk:"true"`
Bio quark.Nullable[string] `db:"bio"`
Born quark.Nullable[time.Time] `db:"born"`
}
p := Profile{
Bio: quark.SomeOf("hello"),
Born: quark.NullOf[time.Time](),
}
type OTelEventBus ¶ added in v0.9.0
type OTelEventBus struct {
// contains filtered or unexported fields
}
OTelEventBus is an in-tree EventBus that records each event as a structured log line tagged for OpenTelemetry log/trace correlation. It deliberately does NOT pull in the OTel SDK — Quark's otel package owns the tracer/meter wiring, and forcing a span here would couple the core package to the SDK. Instead it writes a correlation-ready slog record; deployments running the otel bridge pick it up as a log record on the active span. Swap in a real span-emitting bus by implementing EventBus yourself if you need first-class spans.
func NewOTelEventBus ¶ added in v0.9.0
func NewOTelEventBus(logger *slog.Logger) *OTelEventBus
NewOTelEventBus returns an OTelEventBus. A nil logger falls back to slog.Default.
type OpAddCheck ¶ added in v0.6.0
OpAddCheck / OpDropCheck — emitted only when both schemas populate Checks (which means neither side is SQLite — see the SQLite Checks=nil contract in Check's godoc). When one side is SQLite, Diff skips the check comparison rather than treating Checks=nil as "no checks" (which would falsely emit DropCheck for every check on the other side).
func (OpAddCheck) String ¶ added in v0.6.0
func (o OpAddCheck) String() string
type OpAddColumn ¶ added in v0.6.0
OpAddColumn is emitted when the desired schema adds a column to a table both sides have. Carries the full Column so the executor can splice the right `<type> [NULL|NOT NULL] [DEFAULT ...]` into the ALTER TABLE ADD COLUMN statement.
func (OpAddColumn) String ¶ added in v0.6.0
func (o OpAddColumn) String() string
type OpAddForeignKey ¶ added in v0.6.0
type OpAddForeignKey struct {
Table string
ForeignKey ForeignKey
}
OpAddForeignKey / OpDropForeignKey — same model as indexes: name is the match key. SQLite's "" name for inline FKs (see schema.ForeignKey godoc) is handled by Diff matching on (Columns, RefTable, RefColumns) when Name is "" on both sides.
func (OpAddForeignKey) String ¶ added in v0.6.0
func (o OpAddForeignKey) String() string
type OpAlterColumn ¶ added in v0.6.0
OpAlterColumn is emitted when both sides have a column with the same name but at least one attribute differs (Type, Nullable, or Default). The op carries BOTH the old and the new column so the executor / CLI can render the delta precisely and so resumable migrations (F3-4) can decide whether the alter is safe to retry.
Diff convention: we emit AT MOST ONE OpAlterColumn per (table, column). If both Type and Nullable changed, a single op describes both deltas; the executor decides whether per-attribute ALTERs are needed or a single multi-attribute ALTER is supported.
func (OpAlterColumn) String ¶ added in v0.6.0
func (o OpAlterColumn) String() string
type OpCreateIndex ¶ added in v0.6.0
OpCreateIndex / OpDropIndex are emitted by Diff when the index list of a table both sides have differs. We match indexes by name — there's no fuzzy "same columns, different name" matching in this PR. Renames look like DROP + CREATE, which is what the engines do anyway. If F3-3-execute later wants to detect renames for safety reasons, that's a separate pass.
func (OpCreateIndex) String ¶ added in v0.6.0
func (o OpCreateIndex) String() string
type OpCreateTable ¶ added in v0.6.0
type OpCreateTable struct {
Table Table
}
OpCreateTable is emitted when the desired schema has a table that the current schema lacks. The full Table value (columns, indexes, FKs, checks) is carried so the executor can emit CREATE TABLE + CREATE INDEX + ALTER TABLE ADD CONSTRAINT in the right order.
func (OpCreateTable) String ¶ added in v0.6.0
func (o OpCreateTable) String() string
type OpDropCheck ¶ added in v0.6.0
func (OpDropCheck) String ¶ added in v0.6.0
func (o OpDropCheck) String() string
type OpDropColumn ¶ added in v0.6.0
OpDropColumn is emitted when the current schema has a column the desired schema lacks. Destructive — same caveat as OpDropTable.
func (OpDropColumn) String ¶ added in v0.6.0
func (o OpDropColumn) String() string
type OpDropForeignKey ¶ added in v0.6.0
type OpDropForeignKey struct {
Table string
ForeignKey string // catalog name; "" on SQLite inline FKs
}
func (OpDropForeignKey) String ¶ added in v0.6.0
func (o OpDropForeignKey) String() string
type OpDropIndex ¶ added in v0.6.0
func (OpDropIndex) String ¶ added in v0.6.0
func (o OpDropIndex) String() string
type OpDropTable ¶ added in v0.6.0
type OpDropTable struct {
Table string
}
OpDropTable is emitted when the current schema has a table that the desired schema lacks. Destructive; F3-3 doesn't gate on a "safe mode" flag — that belongs to the executor / CLI (F3-4 + F3-5) which can prompt or refuse to drop without an explicit flag.
func (OpDropTable) String ¶ added in v0.6.0
func (o OpDropTable) String() string
type Operation ¶ added in v0.6.0
type Operation interface {
// String returns a stable human-readable description for the
// plan output and test failure messages. Format is
// `<VERB> <subject>` — no DDL syntax, since DDL depends on the
// dialect.
String() string
// contains filtered or unexported methods
}
Operation is one unit of schema change emitted by Diff. It's a dialect-neutral plan node: each op carries the identifiers and neutral shape needed to reconstruct a single DDL statement, and the executor (F3-3-execute, follow-up PR) translates it to per-dialect SQL via the existing migrator helpers.
Operation is a sealed interface — the concrete types in this file are the only valid implementations. F3-3 deliberately models ops as values rather than method calls so the diff stays inspectable (the CLI plan command in F3-5 can render each op as text without touching SQL) and testable (unit tests assert on op structure, not on emitted SQL).
func Diff ¶ added in v0.6.0
Diff returns the ordered list of [Operation]s that, applied in order, would bring `current` into alignment with `desired`. Both arguments are dialect-neutral Schema values typically produced by Client.IntrospectSchema (for `current`) and by a future models-to-schema pass (for `desired`, F3-3-plan).
Ordering rules:
- Tables present in desired but not in current → OpCreateTable first (so subsequent ops can reference them).
- Per table that both sides have, in this exact order: a) ADD COLUMN then ALTER COLUMN (so the new shape is in place before in-place alters). b) DROP CHECK → DROP FK → DROP INDEX → DROP COLUMN (reverse-dependency order: drop the dependent constraint before the column it references). c) CREATE INDEX after all column changes (add/alter/drop) so new indexes can reference new columns and don't trip over dropped ones. d) ADD FOREIGN KEY after CREATE INDEX (FKs typically require an index on the referencing column). e) ADD CHECK last.
- Tables present in current but not in desired → OpDropTable LAST (so FK references from other dropped tables are already gone).
Diff is pure and deterministic: same input always produces the same output, and tables/columns/indexes are sorted by name within each step so the plan is reviewable in tests and CLI output.
Diff intentionally does NOT compare:
- Column.Type strings across dialects (PG "varchar(255)" vs MSSQL "nvarchar(255)" — F3-2 doesn't normalise types, F3-3 compares the strings verbatim and emits an OpAlterColumn if they differ. The caller is expected to feed two schemas from the same dialect, OR explicitly accept the alter noise.).
- Check.Expression text (each dialect has its own canonical form — see the Check godoc). When both sides have a check by the same name, Diff treats them as equal regardless of expression text. AST-level equivalence is out of scope for this PR.
- Checks on a side where Checks=nil (the SQLite contract). When desired.Checks=nil OR current.Checks=nil for a table, the check comparison for that table is skipped entirely.
type Option ¶
type Option func(*Client)
Option configures a Client.
func WithCacheJitter ¶ added in v0.8.0
WithCacheJitter tunes the ±jitter factor applied to every TTL when a CacheStore is installed (F4-5, ADR-0011). Default 0.1 (±10%). Range [0, 1]; values outside are clamped. Setting to 0 disables jitter but keeps singleflight and XFetch on — the "todo o nada" of ADR-0011 applies to the wrapper's installation, not to each individual protection. No effect when WithCacheStore is not used.
func WithCacheStore ¶
func WithCacheStore(s CacheStore) Option
WithCacheStore sets the caching backend for the client.
func WithCacheXFetchBeta ¶ added in v0.8.0
WithCacheXFetchBeta tunes the XFetch probabilistic-early-refresh parameter (F4-5, ADR-0011). Default 1.0; range >= 0. Higher β makes early refresh more aggressive; β = 0 disables XFetch entirely (still keeps singleflight and jitter active). No effect when WithCacheStore is not used.
Both the on/off flag AND the stored beta are written on every call, so a sequence like WithCacheXFetchBeta(2.0) → WithCacheXFetchBeta(0) leaves no residual β behind: the final state is XFetch off with stampedeXFetchBeta = 0.
func WithDeadlockRetry ¶ added in v0.8.0
WithDeadlockRetry enables transparent retry of Client.Tx when the transaction is killed by a deadlock (F4-7).
maxAttempts is the total number of attempts (1 = no retry, the historical behaviour; 0 or negative also disables). When the transaction closure returns an error that isDeadlock recognises from the active driver (PG 40P01, MySQL 1213, MSSQL 1205, Oracle ORA-00060), the runner sleeps with exponential backoff + jitter and re-executes the closure against a fresh transaction. Non- deadlock errors propagate immediately.
The retry wraps the ENTIRE closure, not individual queries — a deadlock aborts the whole tx, so re-running just the failed query would race against a half-committed state. Disabled by default to keep the historical at-most-once-per-call semantics; opt in when the workload genuinely deadlocks under contention.
client, _ := quark.New("pgx", dsn, quark.WithDeadlockRetry(3))
func WithDefaultTZ ¶ added in v0.7.0
WithDefaultTZ sets the fallback timezone for time.Time columns that do not carry their own quark:"tz=..." tag.
The contract (see docs/adr/0010): time.Time values always go to the database as UTC — the column stores the same instant regardless of engine — and are converted to loc in memory when scanned back. loc therefore affects only how the struct field reads in Go, not what is persisted.
A column-level quark:"tz=America/New_York" tag always overrides this default. When neither a default nor a tag is set, time.Time values pass through to the driver untouched (the historical v0.6 behaviour), so this feature is fully opt-in.
client, _ := quark.New("pgx", dsn, quark.WithDefaultTZ(time.UTC))
func WithDialect ¶
WithDialect sets the SQL dialect for the client. If not set, the dialect will be auto-detected from the database driver.
func WithLimits ¶
WithLimits sets the security and performance limits.
func WithLogger ¶
WithLogger sets the logger for the client. Passing nil is a no-op: the client keeps its default (slog.Default) rather than dropping to a nil logger. This guarantees the internal log sites (slow-query log, deadlock-retry, post-commit hook errors, event-emit failures) always have a non-nil logger to write to.
func WithMiddleware ¶
func WithMiddleware(m Middleware) Option
WithMiddleware adds middleware to the query execution chain. Middleware is applied in the order they are added.
func WithQueryObserver ¶
func WithQueryObserver(o QueryObserver) Option
WithQueryObserver adds a query observer to the client. Multiple observers can be added and will be called in order.
func WithReplicaDownCooldown ¶ added in v0.13.0
WithReplicaDownCooldown sets how long a read replica stays out of rotation after a transient connection failure before it is retried (F6-6, ADR-0015). The default is 5s. Tune it to your topology: shorter for same-AZ replicas, longer for cross-region ones where a flap is costlier to re-probe. Only meaningful alongside WithReplicas; a non-positive value keeps the default.
func WithReplicaStrategy ¶ added in v1.0.0
func WithReplicaStrategy(s ReplicaStrategy) Option
WithReplicaStrategy sets how a routed read picks among healthy replicas (F6-5, ADR-0015): ReplicaRoundRobin (default), ReplicaRandom, or ReplicaLeastConn. Only meaningful alongside WithReplicas with more than one replica; with a single replica all strategies pick it. Every strategy honours the F6-6 health cooldown.
client, err := quark.New("pgx", primaryDSN,
quark.WithReplicas(replica1DSN, replica2DSN),
quark.WithReplicaStrategy(quark.ReplicaLeastConn),
)
func WithReplicas ¶ added in v0.13.0
WithReplicas registers read-replica DSNs (F6-5, ADR-0015). New opens one read-only connection pool per DSN (same pool options and dialect as the primary) and routes reads to them — both multi-row reads (List / Iter / eager-loading) and single-row reads (First / Find / Count / aggregates) — while writes always go to the primary. Pass the same engine's replica endpoints; a read-your-writes path uses Sticky. The selection strategy defaults to round-robin; change it with WithReplicaStrategy.
Opt-in: without it, every operation uses the single primary connection, unchanged. Reads inside Client.Tx and under RowLevelSecurityNative always use the primary regardless (see ADR-0015).
Failover (F6-6): if a replica fails a read with a transient connection error, the read fails over to the primary and the replica is taken out of rotation for a cooldown (default 5s), then retried — a downed replica degrades performance, not correctness.
client, err := quark.New("pgx", primaryDSN,
quark.WithReplicas(replica1DSN, replica2DSN),
quark.WithMaxOpenConns(16),
)
func WithSlowQueryThreshold ¶ added in v0.8.0
WithSlowQueryThreshold enables structured slow-query logging.
Every query, exec, query-row, raw-query and raw-exec whose duration exceeds the threshold is emitted at WARN through Client.logger with structured attributes (duration_ms, threshold_ms, operation, table, rows, sql). Bind arguments are NOT included — the SQL is the parameterised form, mirroring the F4-2 span redaction principle: logs MUST NOT see user values they have no authority to retain.
A threshold of 0 or negative (the default) disables the feature entirely — the check is a single cheap comparison on the hot observer path. Recommended starting point: 100ms.
client, _ := quark.New("pgx", dsn, quark.WithSlowQueryThreshold(100*time.Millisecond))
type OracleDialect ¶
type OracleDialect struct {
// contains filtered or unexported fields
}
OracleDialect implements the Oracle Database dialect.
func (*OracleDialect) AcquireMigrationLock ¶ added in v1.0.0
func (d *OracleDialect) AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)
AcquireMigrationLock uses Oracle's `DBMS_LOCK` package — the session- scoped advisory lock that mirrors PG's `pg_advisory_lock` and MySQL's `GET_LOCK`. `ALLOCATE_UNIQUE(name)` maps the lock name to a handle (deterministic per name), and `REQUEST(handle, X_MODE, release_on_commit => FALSE)` takes the exclusive lock. Because the lock is session-scoped and explicitly NOT released on commit, it survives Oracle's implicit DDL commits and needs no open transaction — which is what lets it fit the connection-only DBConn interface (a lock-table `SELECT … FOR UPDATE` would need a held transaction the interface doesn't expose). See ADR-0018.
REQUEST returns 0 (acquired), 1 (timeout), 2 (deadlock), 3 (parameter error), 4 (already own this lock), 5 (illegal handle). 0 and 4 are success; 1 maps to ErrLockTimeout; the rest are errors.
Requires `GRANT EXECUTE ON DBMS_LOCK TO <user>` — DBMS_LOCK is not granted to schema users by default. The timeout is in whole seconds (DBMS_LOCK's granularity); sub-second waits round up to 1s.
Prerequisite caveat is documented in website/docs/guides/migrations.mdx.
func (*OracleDialect) AlterTableAddColumn ¶
func (o *OracleDialect) AlterTableAddColumn(table, column, dataType string) string
func (*OracleDialect) AlterTableAlterColumn ¶
func (o *OracleDialect) AlterTableAlterColumn(table, column, newDataType string) string
func (*OracleDialect) AlterTableDropColumn ¶
func (o *OracleDialect) AlterTableDropColumn(table, column string) string
func (*OracleDialect) BuildProcedureCall ¶
func (o *OracleDialect) BuildProcedureCall(procedure string, argCount int) string
func (*OracleDialect) BuildRoutineQuery ¶
func (o *OracleDialect) BuildRoutineQuery(routine string, argCount int) string
func (*OracleDialect) CurrentTimestamp ¶
func (o *OracleDialect) CurrentTimestamp() string
func (*OracleDialect) IntrospectSchema ¶ added in v1.0.0
IntrospectSchema reads the Oracle schema from the data dictionary (`USER_TABLES`, `USER_TAB_COLUMNS`, `USER_INDEXES`/`USER_IND_COLUMNS`, `USER_CONSTRAINTS`/`USER_CONS_COLUMNS`). It is scoped to the connected user's own schema — the `USER_*` views never cross schema boundaries.
Oracle-specific handling:
- Identifiers come back UPPERCASE (Oracle folds unquoted names), so every table / column / referenced name is lowercased to match the neutral lowercase form the models and the other dialects use.
- NUMBER reassembly: precision/scale are glued back on (`NUMBER(19)`, `NUMBER(10,2)`); a NULL precision stays bare `NUMBER`. CHAR/VARCHAR2 use CHAR_LENGTH so the displayed length matches the DDL the migrator wrote.
- NOT NULL is stored as a system CHECK constraint (`"COL" IS NOT NULL`); those are filtered out of Checks — they are surfaced via Column.Nullable, not as user CHECK constraints.
- CHECK predicates are read from `SEARCH_CONDITION_VC` (the VARCHAR2 mirror of the LONG `SEARCH_CONDITION`, available on 12.2+), avoiding LONG-column scan issues.
- Oracle has no ON UPDATE referential action; OnUpdate is always "NO ACTION".
func (*OracleDialect) JSONExtract ¶
func (o *OracleDialect) JSONExtract(column, path string) (string, []any, error)
func (*OracleDialect) LastInsertIDQuery ¶
func (o *OracleDialect) LastInsertIDQuery(table, pkColumn string) string
func (*OracleDialect) LimitOffset ¶
func (o *OracleDialect) LimitOffset(limit, offset int) string
func (*OracleDialect) LockSuffix ¶ added in v0.4.0
func (o *OracleDialect) LockSuffix(opts LockOptions) (string, string, error)
Oracle pessimistic locking. SKIP LOCKED supported on 12c+.
FOR UPDATE [NOWAIT|SKIP LOCKED]
Oracle does not have a FOR SHARE; map LockForShare → ErrUnsupportedFeature rather than emitting an unsafe approximation.
func (*OracleDialect) MapColumnType ¶ added in v1.0.0
func (o *OracleDialect) MapColumnType(t string) string
MapColumnType translates a neutral column-type string into Oracle's native form. Oracle has no TEXT type (`ORA-00902: invalid datatype`), so the generic TEXT that other engines accept becomes CLOB — the semantic equivalent for unbounded text. Already-native types pass through unchanged, so this is safe to apply to every column type.
func (*OracleDialect) Placeholder ¶
func (o *OracleDialect) Placeholder(index int) string
func (*OracleDialect) Placeholders ¶
func (o *OracleDialect) Placeholders(n int) []string
func (*OracleDialect) Quote ¶
func (o *OracleDialect) Quote(identifier string) string
func (*OracleDialect) ReleaseSavepointStmt ¶ added in v1.1.0
func (o *OracleDialect) ReleaseSavepointStmt(name string) string
func (*OracleDialect) RenameColumn ¶
func (o *OracleDialect) RenameColumn(table, oldName, newName string) string
func (*OracleDialect) RenameTable ¶
func (o *OracleDialect) RenameTable(oldName, newName string) string
func (*OracleDialect) Returning ¶
func (o *OracleDialect) Returning(columns ...string) string
func (*OracleDialect) RollbackToSavepointStmt ¶ added in v1.1.0
func (o *OracleDialect) RollbackToSavepointStmt(name string) string
func (*OracleDialect) SavepointStmt ¶ added in v1.1.0
func (o *OracleDialect) SavepointStmt(name string) string
func (*OracleDialect) SupportsLastInsertID ¶
func (o *OracleDialect) SupportsLastInsertID() bool
func (*OracleDialect) SupportsReturning ¶
func (o *OracleDialect) SupportsReturning() bool
func (*OracleDialect) SupportsTransactionalDDL ¶
func (o *OracleDialect) SupportsTransactionalDDL() bool
type Page ¶
type Page[T any] struct { Items []T // The items for current page Total int64 // Total count (if available) Page int // Current page number (0-indexed) PageSize int // Items per page TotalPages int64 // Calculated total pages }
Page represents a paginated result set.
type Plan ¶ added in v0.6.0
type Plan struct {
// Ops is the diff between the desired schema (derived from
// models) and the current schema (from [Client.IntrospectSchema]).
// See the [Diff] godoc for ordering guarantees.
Ops []Operation
}
Plan is the result of Client.PlanMigration: the ordered list of [Operation]s that, applied to the database, would bring it into alignment with the Go-side models.
A Plan is inert — it doesn't execute itself. F3-3-execute (the follow-up PR) will add a Plan.Apply method that walks Ops and dispatches each to the per-dialect migrator helpers. The CLI plan command (F3-5) renders the Plan via Plan.String without ever touching SQL.
func (Plan) Hash ¶ added in v0.6.0
Hash returns a deterministic SHA-256 hex digest of the Plan's operation sequence. Used by Client.ApplyPlan's resumable path to detect plan drift between runs: if the same plan_hash carries over from a partially-applied run, the resume can pick up where it left off; if the hash differs, the Plan has changed and the resume is unsafe (e.g. the user added a column to their model between runs).
The digest input is the line-joined `op.String()` output of every op. Op.String() formats are documented as stable in the F3-3-core godoc, so the hash is reproducible across processes and binaries on the same Plan value. A change in any op's content — even cosmetic, like a renamed table — produces a new hash, which is the right safety boundary for resume.
Empty plans hash to the sha256 of the empty string (a constant) so two empty plans compare equal.
func (Plan) IsEmpty ¶ added in v0.6.0
IsEmpty reports whether the Plan would be a no-op when applied. Equivalent to `len(p.Ops) == 0` but more readable in user code.
Use this as the "did anything drift?" check in CI / health endpoints — a non-empty Plan means the Go models and the database schema have diverged.
func (Plan) String ¶ added in v0.6.0
String renders the Plan as a multi-line human-readable report. Each Op contributes one line via its own Operation.String. Empty plans render as "(no changes)".
The format is intentionally minimal so the F3-5 CLI can wrap it without parsing — table or coloured output is the CLI's responsibility, not the Plan's.
type PoolOption ¶ added in v0.3.0
type PoolOption interface {
// contains filtered or unexported methods
}
PoolOption is a configuration option for the database connection pool. These are applied to the *sql.DB before creating the Client.
func WithConnMaxIdleTime ¶ added in v0.3.0
func WithConnMaxIdleTime(d time.Duration) PoolOption
WithConnMaxIdleTime sets the maximum amount of time a connection may be idle.
func WithConnMaxLifetime ¶ added in v0.3.0
func WithConnMaxLifetime(d time.Duration) PoolOption
WithConnMaxLifetime sets the maximum amount of time a connection may be reused.
func WithMaxIdleConns ¶ added in v0.3.0
func WithMaxIdleConns(n int) PoolOption
WithMaxIdleConns sets the maximum number of idle connections in the pool.
func WithMaxOpenConns ¶ added in v0.3.0
func WithMaxOpenConns(n int) PoolOption
WithMaxOpenConns sets the maximum number of open connections to the database.
type PostgresDialect ¶
type PostgresDialect struct {
// contains filtered or unexported fields
}
PostgresDialect implements the PostgreSQL dialect.
func (*PostgresDialect) AcquireMigrationLock ¶ added in v0.6.0
func (d *PostgresDialect) AcquireMigrationLock(ctx context.Context, db DBConnector, name string, timeout time.Duration) (MigrationLock, error)
AcquireMigrationLock uses `pg_advisory_lock(hashtext(name))` on a dedicated connection. Session-level (not transaction-level), so the caller can run multiple statements under the lock without holding a long transaction open. Released by `pg_advisory_unlock` on Release.
Timeout is honoured via `SET lock_timeout` on the connection — PG's native way to bound advisory-lock waits. A timeout violation surfaces as SQLSTATE `55P03` (`lock_not_available`); we translate it to `ErrLockTimeout`.
func (*PostgresDialect) AlterTableAddColumn ¶
func (p *PostgresDialect) AlterTableAddColumn(table, column, dataType string) string
func (*PostgresDialect) AlterTableAlterColumn ¶
func (p *PostgresDialect) AlterTableAlterColumn(table, column, newDataType string) string
func (*PostgresDialect) AlterTableDropColumn ¶
func (p *PostgresDialect) AlterTableDropColumn(table, column string) string
func (*PostgresDialect) BuildProcedureCall ¶
func (p *PostgresDialect) BuildProcedureCall(procedure string, argCount int) string
func (*PostgresDialect) BuildRoutineQuery ¶
func (p *PostgresDialect) BuildRoutineQuery(routine string, argCount int) string
func (*PostgresDialect) CurrentTimestamp ¶
func (p *PostgresDialect) CurrentTimestamp() string
func (*PostgresDialect) IntrospectSchema ¶ added in v0.6.0
IntrospectSchema reads the PG schema by querying `information_schema` (more portable than `pg_catalog` and sufficient for the column-level surface F3-2 needs). The `current_schema()` (typically `public`) scopes the lookup so multi-schema setups don't drag in unrelated tables.
PG caveats handled here:
- The data type returned by `data_type` is the SQL-standard form (`integer`, `bigint`, `character varying`). For native parameter-bearing types (`varchar(255)`, `numeric(10,2)`) we reassemble the precision/scale/length from the adjacent columns so the round-trip vs Go-side schema is comparable.
- The `column_default` is preserved as-is — including PG's `nextval('seq')` wrappers around SERIAL/IDENTITY columns. The diff layer is responsible for recognising those.
func (*PostgresDialect) JSONExtract ¶
func (p *PostgresDialect) JSONExtract(column, path string) (string, []any, error)
func (*PostgresDialect) LastInsertIDQuery ¶
func (p *PostgresDialect) LastInsertIDQuery(table, pkColumn string) string
func (*PostgresDialect) LimitOffset ¶
func (p *PostgresDialect) LimitOffset(limit, offset int) string
func (*PostgresDialect) LockSuffix ¶ added in v0.4.0
func (p *PostgresDialect) LockSuffix(opts LockOptions) (string, string, error)
PostgreSQL pessimistic locking (PG 9.5+ for SKIP LOCKED).
FOR UPDATE [SKIP LOCKED|NOWAIT] FOR SHARE [SKIP LOCKED|NOWAIT]
func (*PostgresDialect) Placeholder ¶
func (p *PostgresDialect) Placeholder(index int) string
func (*PostgresDialect) Placeholders ¶
func (p *PostgresDialect) Placeholders(n int) []string
func (*PostgresDialect) Quote ¶
func (p *PostgresDialect) Quote(identifier string) string
func (*PostgresDialect) RenameColumn ¶
func (p *PostgresDialect) RenameColumn(table, oldName, newName string) string
func (*PostgresDialect) RenameTable ¶
func (p *PostgresDialect) RenameTable(oldName, newName string) string
func (*PostgresDialect) Returning ¶
func (p *PostgresDialect) Returning(columns ...string) string
func (*PostgresDialect) SupportsLastInsertID ¶
func (p *PostgresDialect) SupportsLastInsertID() bool
func (*PostgresDialect) SupportsReturning ¶
func (p *PostgresDialect) SupportsReturning() bool
func (*PostgresDialect) SupportsTransactionalDDL ¶
func (p *PostgresDialect) SupportsTransactionalDDL() bool
type Predicate ¶ added in v0.12.0
type Predicate struct {
// contains filtered or unexported fields
}
Predicate is a single typed WHERE condition produced by a TypedColumn accessor. It is opaque: build it through a TypedColumn method (Eq, Gt, In, …) and pass it to Query.WhereP. Because the only construction path is a generated column handle, the column identifier never originates from caller-supplied input.
type Query ¶
Query represents a type-safe database query builder for model T. All builder methods return a new Query (immutable/clone pattern) for thread-safety. Execution methods are in query_exec.go and query_crud.go
func For ¶
func For[T any](ctx context.Context, provider ClientProvider) *Query[T]
For creates a Query builder for the given model type. This is the primary entry point for type-safe database operations.
Example:
type User struct {
ID int64 `db:"id"`
Name string `db:"name"`
}
user, err := quark.For[User](ctx, client).Find(1)
users, err := quark.For[User](ctx, client).Where("active", "=", true).List()
func ForTx ¶
ForTx creates a Query builder for the given model type bound to a transaction. This is the transactional counterpart of For[T]().
Example:
err := client.Tx(ctx, func(tx *quark.Tx) error {
return quark.ForTx[User](ctx, tx).Create(&user)
})
func (*Query[T]) Apply ¶
Apply applies one or more Scope functions to the query. Scopes are composable, reusable query fragments.
Example:
activeUsers := func(q *quark.Query[User]) *quark.Query[User] {
return q.Where("active", "=", true)
}
users, _ := quark.For[User](ctx, client).Apply(activeUsers).List()
func (*Query[T]) AsSubquery ¶ added in v0.4.0
AsSubquery captures the current Query[T] as a renderable Subquery. The SELECT is rendered eagerly: identifier validation, soft-delete and tenant predicates, JOINs, GROUP BY, HAVING, ORDER BY, LIMIT all run at AsSubquery time.
The captured SQL uses '?' as the bind marker; the outer query swaps it for the dialect's placeholder when the wrapping Expr is rendered. The SELECT cols can be customised via the standard Select() before capture — typical use is `Select("id")` for an `IN (subquery)` shape.
Pessimistic lock options (`ForUpdate` / `ForShare` / `SkipLocked` / `NoWait`) are explicitly rejected on the inner query — MSSQL emits table hints (`WITH (UPDLOCK, ROWLOCK)`) inline in the FROM clause that are not legal inside a subquery context, and PG / MySQL / Oracle's `FOR UPDATE` suffix on a subquery is technically valid but misleading when the outer caller already drives row locking. Acquire locks on the outer query instead.
func (*Query[T]) Create ¶
Create inserts a new record. The entity must have a db tag on fields to be persisted. Returns with the ID set from the database. Create inserts a new record and recursively saves associations.
func (*Query[T]) CreateBatch ¶
CreateBatch inserts multiple records in a single SQL statement using bulk VALUES. Each entity gets its PK populated when the dialect supports RETURNING; otherwise PKs are left at their zero value (callers can re-query if needed).
Example:
users := []*User{{Name: "Alice"}, {Name: "Bob"}}
err := quark.For[User](ctx, client).CreateBatch(users)
func (*Query[T]) Cursor ¶
Cursor returns a Cursor for manual iteration over large result sets. The Cursor must be closed after use (defer cursor.Close()).
Example:
cursor, err := quark.For[User](ctx, client).Where("active", "=", true).Cursor()
if err != nil { log.Fatal(err) }
defer cursor.Close()
for cursor.Next() {
var user User
if err := cursor.Scan(&user); err != nil { break }
process(user)
}
func (*Query[T]) Delete ¶
Delete performs a soft delete by setting deleted_at = NOW(). If the model doesn't have deleted_at field, performs hard delete. Returns the number of rows affected.
func (*Query[T]) DeleteBatch ¶
DeleteBatch deletes multiple records by their primary key values using DELETE … WHERE pk IN (…) statements, chunked to batchChunkSize to stay within every supported dialect's placeholder limit (Oracle: 1000, MSSQL: ~2100, others: larger).
Example:
affected, err := quark.For[User](ctx, client).DeleteBatch([]any{1, 2, 3})
func (*Query[T]) DeleteBy ¶
DeleteBy performs a hard delete with WHERE conditions. Requires Where clause for safety.
func (*Query[T]) Except ¶ added in v0.4.0
Except renders `EXCEPT` (or `MINUS` on Oracle). Not supported on MySQL / MariaDB.
func (*Query[T]) ForShare ¶ added in v0.4.0
ForShare marks the query so the emitted SELECT acquires a shared read lock. Composes with SkipLocked / NoWait. Not supported on SQLite; MSSQL approximates with HOLDLOCK; PG / MySQL 8+ / MariaDB / Oracle support it natively.
func (*Query[T]) ForUpdate ¶ added in v0.4.0
ForUpdate marks the query so the emitted SELECT acquires a row-level FOR UPDATE lock. Composes with SkipLocked / NoWait. Returns the receiver (no error) so it chains naturally; SQL surface failures (unsupported dialect, invalid combination) surface at execution time.
rows, err := quark.For[Order](ctx, client).
Where("status", "=", "pending").
ForUpdate().
Limit(50).
List()
func (*Query[T]) HardDelete ¶
HardDelete permanently deletes the entity by its primary key.
func (*Query[T]) Having ¶
Having adds a HAVING condition (used together with GroupBy).
The column argument is validated as a plain identifier — no parentheses, function calls, or expressions. To filter on aggregates such as COUNT(*) or SUM(col), use HavingAggregate instead.
func (*Query[T]) HavingAggregate ¶ added in v0.4.0
HavingAggregate adds a HAVING condition over an aggregate function.
fn must be one of COUNT, SUM, AVG, MIN, MAX (case-insensitive). column is either a regular column name (validated through SQLGuard) or "*" — only accepted with COUNT, since "SUM(*)" / "AVG(*)" / etc. are not valid SQL. operator goes through the same whitelist Where uses (=, !=, <>, <, <=, >, >=, IN, NOT IN, BETWEEN, IS [NOT] NULL, LIKE, ILIKE).
Example:
groups, err := quark.For[Order](ctx, client).
GroupBy("status").
HavingAggregate("COUNT", "*", ">", 5).
List()
// emitted: ... GROUP BY "status" HAVING COUNT(*) > $1
This closes the historic Having(column, op, value) limitation where the column went through ValidateIdentifier and aggregates therefore could not be expressed without RawQuery. The structured-AST form Having(Func("count", Col("*")), ">", 5) arrives with the full Phase 2 AST; HavingAggregate is the focused, type-safe shortcut for the overwhelmingly common case.
func (*Query[T]) HavingExpr ¶ added in v0.4.0
HavingExpr adds a HAVING condition built from the Expr AST. Same rendering pipeline as WhereExpr; useful for aggregate predicates that need the full composition surface (Func("COUNT", Col("*")) > Lit(5), and so on).
func (*Query[T]) Intersect ¶ added in v0.4.0
Intersect renders `INTERSECT` between the base and the operand. Not supported on MySQL / MariaDB — those return ErrUnsupportedFeature from setOpKeyword at render time.
func (*Query[T]) Iter ¶
Iter executes the query and iterates over results one by one. Uses streaming to handle large datasets without loading all into memory.
Example:
err := quark.For[User](ctx, client).Where("active", "=", true).Iter(func(user User) error {
process(user)
return nil
})
func (*Query[T]) Join ¶
func (q *Query[T]) Join(table string) *JoinBuilder[T]
Join opens an INNER JOIN against `table`. Complete the JOIN with `.On(left, op, right)` (typed binary identifier comparison) or `.OnRaw(onClause)` (free-form, validated through the same identifier grammar as `On`).
The structured form replaces the v0.3.x string-raw `Join(table, on)` signature; see `MIGRATION_v0.4.0.md` for the mechanical rewrite.
func (*Query[T]) LeftJoin ¶
func (q *Query[T]) LeftJoin(table string) *JoinBuilder[T]
LeftJoin opens a LEFT JOIN. See Join for ON-clause grammar.
func (*Query[T]) List ¶
List executes the query and returns all matching rows. If Limit() is not called, uses a safe default (100) to prevent OOM. Use Iter() for unbounded streaming or Paginate() for large datasets.
func (*Query[T]) MustAsSubquery ¶ added in v0.4.0
MustAsSubquery is the panic-on-error variant of AsSubquery for use in expression composition where errors would otherwise have to be threaded through the AST. The error is realistic (invalid identifier, etc.) only when the inner query is malformed; for well-formed inputs it never triggers.
func (*Query[T]) NoWait ¶ added in v0.4.0
NoWait tells the database to fail immediately if any matching row is already locked by another transaction. Combine with ForUpdate / ForShare. Implementation-defined per dialect.
func (*Query[T]) OnlyTrashed ¶ added in v0.3.0
OnlyTrashed returns a query that filters down to soft-deleted rows (deleted_at IS NOT NULL) so callers can list, restore, or hard-delete the trash. A no-op when the model has no deleted_at column.
func (*Query[T]) Or ¶
Or adds an OR condition group. The callback receives a fresh Query to build conditions. All conditions within the callback are grouped with AND and joined to the outer query with OR.
Example:
quark.For[User](ctx, client).
Where("active", "=", true).
Or(func(q *Query[User]) *Query[User] {
return q.Where("role", "=", "admin").Where("role", "=", "superadmin")
}).List()
Generates: WHERE "active" = $1 OR ("role" = $2 AND "role" = $3)
Under the RowLevelSecurityClient tenant strategy the OR group inherits the parent's tenant_id predicate so it cannot escape isolation via SQL operator precedence.
func (*Query[T]) Paginate ¶
Paginate executes the query with pagination. Returns current page, total count, and error.
Example:
page, err := quark.For[User](ctx, client).Paginate(100, 0) // 100 per page, page 0
page, err := quark.For[User](ctx, client).Where("active", "=", true).Paginate(50, 2)
func (*Query[T]) Restore ¶ added in v0.3.0
Restore clears the deleted_at column on the row identified by entity's primary key, "untrashing" it. Returns the number of rows affected.
Restore implicitly scopes to currently-trashed rows (deleted_at IS NOT NULL): a Restore on a row that was never deleted is a 0-row no-op rather than a corrupting NULL-write. Useful as the inverse of Delete in admin flows.
Phase-1 F1-5. Returns ErrInvalidModel when the model has no deleted_at.
func (*Query[T]) RightJoin ¶
func (q *Query[T]) RightJoin(table string) *JoinBuilder[T]
RightJoin opens a RIGHT JOIN. See Join for ON-clause grammar.
func (*Query[T]) SelectExpr ¶ added in v0.4.0
SelectExpr adds an AST projection to the SELECT list, aliased as `alias`. Use it for window functions, scalar subqueries, or any expression the plain `Select(cols...)` API can't model:
q := quark.For[Order](ctx, client).
SelectExpr("rank", quark.Over(quark.Rank(),
quark.NewWindow().
PartitionBy(quark.Col("status")).
OrderBy(quark.Col("amount"), true))).
SelectExpr("running_total", quark.Over(
quark.Func("SUM", quark.Col("amount")),
quark.NewWindow().OrderBy(quark.Col("id"), false)))
The expression is rendered against a `qmark`-emitting dialect at SelectExpr time, so the inner '?' markers are reindexed to the outer dialect's placeholder syntax when buildSelect runs. The args land in the args slice between any CTE args and the WHERE args — matching the SQL-surface order of the SELECT projection.
Composing SelectExpr with the plain Select(cols...) is allowed: the regular columns render first, the AST projections after, comma- separated. If neither is set, the SELECT defaults to '*'.
func (*Query[T]) SkipLocked ¶ added in v0.4.0
SkipLocked tells the database to skip rows that are already locked by another transaction instead of blocking on them. Combine with ForUpdate / ForShare. Implementation-defined per dialect.
func (*Query[T]) Track ¶ added in v0.3.0
func (q *Query[T]) Track() *TrackedQuery[T]
Track returns a TrackedQuery whose Find/First/List yield *Tracked[T] values carrying a column-value snapshot. Call Save on the result to emit an UPDATE that touches only the columns whose values changed since load — the permanent fix for the zero-value trap (P0-4).
Track is opt-in. Existing Find/First/List remain unchanged.
func (*Query[T]) Union ¶ added in v0.4.0
Union appends a UNION (DISTINCT) operand to the query. The combined statement renders flat — `SELECT ... UNION SELECT ...` — without parentheses around the operands, since SQLite's compound-select grammar rejects parenthesised operands. The flat form is the portable shape across all six target dialects.
Identifier validation runs eagerly on the operand so a malformed other-query surfaces at attach time, not at the outer query's exec time. Outer-query `OrderBy` / `Limit` apply to the combined result (the SQL standard binding); the operand cannot have its own ORDER BY / LIMIT (rejected with `ErrUnsupportedFeature`). See attachSetOp for the full operand restriction list.
func (*Query[T]) UnionAll ¶ added in v0.4.0
UnionAll is the multiset variant: `UNION ALL` keeps duplicate rows.
func (*Query[T]) Unscoped ¶
Unscoped ignores soft-delete filters for the query, returning both trashed and non-trashed rows. Equivalent to WithTrashed; kept for backward compatibility.
func (*Query[T]) Update ¶
Update updates the entity by its primary key with partial-update semantics: only fields whose value is non-zero for their type are written.
CAUTION — zero-value trap (P0-4 — pending dirty tracking in Phase 1): because zero values are skipped, calling Update cannot write false to a bool, 0 to an integer, "" to a string, or nil to a pointer/slice/map. To write a zero value explicitly, use UpdateFields or UpdateMap. When Update detects skipped zero-value fields it logs a WARN line so callers notice the silent skip.
Any Where() conditions are merged into the WHERE clause alongside the PK. Returns the number of rows affected. Recursively saves associations.
func (*Query[T]) UpdateBatch ¶
UpdateBatch updates multiple records by their primary keys within a single transaction. Each entity undergoes a partial update: zero-value fields are skipped (same semantics as Update). A transaction is used to guarantee atomicity across all rows.
Example:
err := quark.For[User](ctx, client).UpdateBatch(users)
func (*Query[T]) UpdateFields ¶ added in v0.3.0
UpdateFields updates only the named fields on the entity, bypassing the zero-value filter that Update applies. This is the recommended API when you need to write false / 0 / "" / nil to a column — values that Update would silently skip.
fields are matched against struct field db tags only — the same identifier resolution as Update and Find. Listing a struct field name without a db tag returns ErrInvalidQuery: there is one canonical name per column and we don't accept aliases here, to keep the resolution unambiguous.
The primary key is never overwritten; listing a PK column returns an error. If the client is configured with the RowLevelSecurityClient tenant strategy, the tenant column is injected before the SET clause is built; callers do not need to (and should not) list it explicitly.
Example:
user := User{ID: 42, Active: false}
rows, err := quark.For[User](ctx, client).UpdateFields(&user, "active")
// emitted: UPDATE "users" SET "active" = $1 WHERE "id" = $2 args=[false, 42]
Returns the number of rows affected.
func (*Query[T]) UpdateMap ¶
UpdateMap updates fields using a map (for partial updates without full entity). Requires Where clause for safety. Returns the number of rows affected.
func (*Query[T]) Upsert ¶
Upsert inserts or updates a record depending on whether a conflict occurs on conflictCols. updateCols specifies which columns to update on conflict; if empty, all non-conflict columns are updated.
Example:
quark.For[User](ctx, client).Upsert(&user, []string{"email"}, []string{"name", "updated_at"})
func (*Query[T]) UpsertBatch ¶
UpsertBatch inserts or updates multiple records in a single batch operation. conflictCols defines uniqueness (e.g. primary key or unique index columns). updateCols defines which columns to update on conflict; empty = all non-conflict columns.
Dialect strategies:
- Postgres / SQLite / MySQL / MariaDB: multi-row INSERT … ON CONFLICT / ON DUPLICATE KEY
- MSSQL: single MERGE … USING (VALUES …) AS src(…)
- Oracle: N individual MERGE statements (Oracle IDENTITY restriction prevents bulk MERGE)
Example:
err := quark.For[User](ctx, client).UpsertBatch(users, []string{"email"}, []string{"name"})
func (*Query[T]) WhereBetween ¶
WhereBetween adds a WHERE ... BETWEEN condition.
func (*Query[T]) WhereExpr ¶ added in v0.4.0
WhereExpr adds a WHERE condition built from a composable Expr AST.
The AST is rendered against the active dialect at call time, producing a fragment with '?' bind markers plus the args. Storage and execution reuse the existing raw-fragment slot in condition: buildWhereClause substitutes each '?' for the dialect placeholder at the correct argIndex, so the AST stays dialect-agnostic at construction time and integrates cleanly with WhereJSON, Or, and the rest of the builder.
Errors raised during ToSQL — unknown function names, invalid identifiers, invalid operators, empty IN lists — are stashed on the query and surface at execution time wrapping ErrInvalidQuery.
Example:
q := quark.For[User](ctx, client).WhereExpr(
quark.Or(
quark.Eq(quark.Col("role"), quark.Lit("admin")),
quark.And(
quark.Gt(quark.Col("logins"), quark.Lit(10)),
quark.Eq(quark.Col("verified"), quark.Lit(true)),
),
),
)
func (*Query[T]) WhereJSON ¶
WhereJSON adds a WHERE condition for a JSON field. column is the JSON column name, path is a dotted key path within the JSON object (e.g. "user.name"). The path is always validated by guard.ValidateJSONPath and, on every dialect except Oracle, bound as a parameter rather than interpolated. Oracle's JSON_VALUE rejects a bound path (ORA-40454), so there the validated path is inlined as a literal; the [A-Za-z0-9_.] grammar keeps it injection-safe. See guard.ValidateJSONPath for the accepted grammar.
On invalid path the error is stashed on the query and surfaces at execution time (List, First, etc.), wrapping ErrInvalidJSONPath.
func (*Query[T]) WhereNot ¶
WhereNot adds a WHERE NOT condition with AND logic.
Example:
quark.For[User](ctx, client).WhereNot("active", "=", false).List()
Generates: WHERE NOT ("active" = $1)
func (*Query[T]) WhereP ¶ added in v0.12.0
WhereP appends one or more typed predicates (built from generated column accessors) to the query, combined with AND. It is the compile-time-safe counterpart to Query.Where; the two may be mixed freely on the same query. Returns a clone, like every other builder method.
func (*Query[T]) WhereSubquery ¶
WhereSubquery adds a WHERE column operator (subquery) condition. The subquery is a raw SQL string. Use this only when AllowRawQueries is enabled.
Example:
sub := "SELECT MAX(id) FROM orders WHERE status = 'open'"
quark.For[User](ctx, client).WhereSubquery("id", "IN", sub).List()
func (*Query[T]) With ¶ added in v0.4.0
With attaches a non-recursive CTE to the query. The CTE renders as `WITH <name> AS (<inner>)` before the outer SELECT, and the outer query can reference the CTE by name in JOIN clauses.
Example:
topOrders, _ := quark.For[Order](ctx, client).
Where("amount", ">", 100).
Select("user_id", "amount").
AsSubquery()
users, err := quark.For[User](ctx, client).
With("top_orders", topOrders).
Join("top_orders", "users.id = top_orders.user_id").
Limit(50).
List()
Multiple With calls compose: the entries render comma-separated in the order they were added. If any entry is recursive, the prefix becomes `WITH RECURSIVE ...`.
func (*Query[T]) WithRecursive ¶ added in v0.4.0
WithRecursive is the recursive form. Emits `WITH RECURSIVE` (or just promotes the prefix when at least one of the previously-added entries is recursive). The inner Subquery is responsible for shaping the recursive body — typically a `UNION ALL` between a base case and a step that references the CTE name. quark's typed Subquery surface doesn't yet model UNION (F2-set), so practical recursive use today is limited to engines/cases where the Subquery body can be constructed from a single SELECT — full recursive support is the motivating use case for F2-set.
func (*Query[T]) WithTrashed ¶ added in v0.3.0
WithTrashed returns a query that includes both soft-deleted (trashed) and live rows — the same effect as Unscoped, named for parity with the scope-driven idiom that other ORMs use. Only meaningful when the model carries a deleted_at column.
type QueryEvent ¶
type QueryEvent struct {
SQL string
Args []any
Duration time.Duration
Rows int64
Error error
Table string
Operation string // "SELECT", "INSERT", "UPDATE", "DELETE"
}
QueryEvent represents a executed query.
type QueryFunc ¶
type QueryFunc func(ctx context.Context, exec Executor, sqlStr string, args []any) (*sql.Rows, error)
QueryFunc is the signature for SQL query functions used by middleware.
type QueryObserver ¶
type QueryObserver interface {
ObserveQuery(event QueryEvent)
}
QueryObserver is called after each query execution. Use this for logging, metrics, auditing, etc.
type QueryRowFunc ¶
QueryRowFunc is the signature for SQL single-row query functions used by middleware.
type RelationMeta ¶
type RelationMeta = schema.RelationMeta
RelationMeta is the metadata for a model relation.
type ReplicaStrategy ¶ added in v1.0.0
type ReplicaStrategy int
ReplicaStrategy selects which healthy read replica serves a routed read when more than one is configured (F6-5, ADR-0015). Set it with WithReplicaStrategy; the zero value is ReplicaRoundRobin. Every strategy honours the F6-6 health cooldown — a replica taken out of rotation by [Client.markReplicaDown] is never chosen until its cooldown expires.
const ( // ReplicaRoundRobin spreads reads evenly by advancing an atomic cursor one // slot per read. Default (zero value); the most predictable distribution // under steady concurrency. ReplicaRoundRobin ReplicaStrategy = iota // ReplicaRandom picks a replica at random per read — uniform across all // replicas when they are healthy, and a replica in cooldown is skipped. // Needs no shared cursor, so it avoids the single contended atomic of // round-robin at the cost of a less even short-term distribution. ReplicaRandom // ReplicaLeastConn picks the healthy replica with the fewest in-use pool // connections (sql.DB.Stats().InUse) at selection time. Best when replica // query latencies are uneven, since it steers new reads toward the least // busy pool; it reads each replica's Stats per call (a cheap mutex-guarded // snapshot). ReplicaLeastConn )
type Routine ¶
type Routine[T any] struct { // contains filtered or unexported fields }
Routine is a builder for executing database functions and stored procedures that return results (table-valued functions or scalar functions).
func NewRoutine ¶
func NewRoutine[T any](ctx context.Context, provider ClientProvider, routine string, args ...any) *Routine[T]
NewRoutine creates a new Routine builder for the given procedure/function.
type SQLGuard ¶
SQLGuard re-exports the internal guard.SQLGuard. It provides SQL injection prevention utilities for Quark ORM.
func NewSQLGuard ¶
func NewSQLGuard() *SQLGuard
NewSQLGuard creates a new SQLGuard with default settings.
type SQLiteDialect ¶
type SQLiteDialect struct {
// contains filtered or unexported fields
}
SQLiteDialect implements the SQLite dialect.
func (*SQLiteDialect) AlterTableAddColumn ¶
func (s *SQLiteDialect) AlterTableAddColumn(table, column, dataType string) string
func (*SQLiteDialect) AlterTableAlterColumn ¶
func (s *SQLiteDialect) AlterTableAlterColumn(table, column, newDataType string) string
func (*SQLiteDialect) AlterTableDropColumn ¶
func (s *SQLiteDialect) AlterTableDropColumn(table, column string) string
func (*SQLiteDialect) BuildProcedureCall ¶
func (s *SQLiteDialect) BuildProcedureCall(procedure string, argCount int) string
func (*SQLiteDialect) BuildRoutineQuery ¶
func (s *SQLiteDialect) BuildRoutineQuery(routine string, argCount int) string
func (*SQLiteDialect) CurrentTimestamp ¶
func (s *SQLiteDialect) CurrentTimestamp() string
func (*SQLiteDialect) IntrospectSchema ¶ added in v0.6.0
IntrospectSchema reads the SQLite schema using `sqlite_master` for the table list and `PRAGMA table_info(<table>)` for the column metadata of each table. This avoids parsing the CREATE TABLE DDL, which would be brittle.
SQLite caveats handled here:
- System tables (`sqlite_*`) and quark internal tables (`quark_*`) are filtered out. The diff layer doesn't need to reason about them.
- SQLite's PRAGMA returns columns in declaration order. We preserve that order (Tables is sorted alphabetically; Columns aren't re-sorted within a table).
- The `dflt_value` column from PRAGMA table_info comes back as a literal SQL fragment (`'draft'`, `0`, `CURRENT_TIMESTAMP`); we pass it through unchanged in `Column.Default`.
func (*SQLiteDialect) JSONExtract ¶
func (s *SQLiteDialect) JSONExtract(column, path string) (string, []any, error)
func (*SQLiteDialect) LastInsertIDQuery ¶
func (s *SQLiteDialect) LastInsertIDQuery(table, pkColumn string) string
func (*SQLiteDialect) LimitOffset ¶
func (s *SQLiteDialect) LimitOffset(limit, offset int) string
func (*SQLiteDialect) LockSuffix ¶ added in v0.4.0
func (s *SQLiteDialect) LockSuffix(opts LockOptions) (string, string, error)
SQLite has no row-level pessimistic-lock primitive — locking is transaction-scoped via BEGIN IMMEDIATE / EXCLUSIVE. Return ErrUnsupportedFeature so callers can branch by dialect or fall back.
func (*SQLiteDialect) Placeholder ¶
func (s *SQLiteDialect) Placeholder(index int) string
func (*SQLiteDialect) Placeholders ¶
func (s *SQLiteDialect) Placeholders(n int) []string
func (*SQLiteDialect) Quote ¶
func (s *SQLiteDialect) Quote(identifier string) string
func (*SQLiteDialect) RenameColumn ¶
func (s *SQLiteDialect) RenameColumn(table, oldName, newName string) string
func (*SQLiteDialect) RenameTable ¶
func (s *SQLiteDialect) RenameTable(oldName, newName string) string
func (*SQLiteDialect) Returning ¶
func (s *SQLiteDialect) Returning(columns ...string) string
func (*SQLiteDialect) SupportsLastInsertID ¶
func (s *SQLiteDialect) SupportsLastInsertID() bool
func (*SQLiteDialect) SupportsReturning ¶
func (s *SQLiteDialect) SupportsReturning() bool
func (*SQLiteDialect) SupportsTransactionalDDL ¶
func (s *SQLiteDialect) SupportsTransactionalDDL() bool
type SavepointDialect ¶ added in v1.1.0
type SavepointDialect interface {
SavepointStmt(name string) string
RollbackToSavepointStmt(name string) string
ReleaseSavepointStmt(name string) string
}
SavepointDialect is an optional Dialect extension for engines whose savepoint statements diverge from the ANSI form (SAVEPOINT / ROLLBACK TO SAVEPOINT / RELEASE SAVEPOINT). A Dialect that does not implement it gets the ANSI statements, which are correct for PostgreSQL, MySQL, MariaDB and SQLite. The transaction layer (Tx.Savepoint, Tx.RollbackTo, Tx.ReleaseSavepoint) consults this interface via a type assertion, so adding it to a dialect is non-breaking for existing custom dialects registered through RegisterDialect.
name arrives already validated by guard.ValidateIdentifier; the dialect decides whether to quote it. A ReleaseSavepointStmt returning "" means the engine has no explicit savepoint-release statement (the savepoint is released at COMMIT) — the Tx layer then skips that Exec. Found by the post-v1.0 bug-bash (BB-9, phase F8): SQL Server uses SAVE TRANSACTION / ROLLBACK TRANSACTION and has no release; Oracle has SAVEPOINT and ROLLBACK TO SAVEPOINT but no RELEASE SAVEPOINT.
type Schema ¶ added in v0.6.0
type Schema struct {
Tables []Table
}
Schema is the dialect-neutral representation of a database schema. It's the foundation for F3-3 (schema diff) — the diff comparator takes a Schema derived from the Go models and a Schema returned by IntrospectSchema, and emits the operations needed to align the two.
Tables are sorted by Name for deterministic ordering; the diff comparator relies on this to produce stable plans.
type SchemaIntrospector ¶ added in v0.6.0
type SchemaIntrospector interface {
IntrospectSchema(ctx context.Context, exec Executor) (Schema, error)
}
SchemaIntrospector is the optional Dialect interface for retrieving the current schema from the database. The same pattern as MigrationLocker — kept as a stand-alone interface so custom dialects downstream don't have to grow this method to keep compiling.
IntrospectSchema returns the schema of the database the executor is connected to (the current schema / database / "user space", depending on dialect semantics). It does NOT cross schema or database boundaries.
type Scope ¶
Scope is a reusable query modifier — a function that receives and returns a *Query[T]. Scopes can be composed via Apply().
type ShardFunc ¶ added in v1.0.0
ShardFunc maps a shard-key value to the name of the shard that owns it. It is the partitioning policy and the resharding seam — pluggable: hash-mod (HashShardFunc, the default helper), range, geo, or a lookup table.
func HashShardFunc ¶ added in v1.0.0
HashShardFunc returns a ShardFunc that maps a key to one of shardNames by FNV-1a hash modulo the shard count — a stable, uniform default assignment. The names are copied, so later mutation of the caller's slice does not change routing. With no names it maps everything to "" (GetClient then errors).
type ShardResolver ¶ added in v1.0.0
ShardResolver extracts the shard key from a context, returning "" when none is present. Use DefaultShardResolver with WithShardKey, or supply your own (e.g. reading an existing request value).
type ShardRouter ¶ added in v1.0.0
type ShardRouter struct {
// contains filtered or unexported fields
}
ShardRouter routes each query to the Client of the shard that owns the query's shard key (resolved from context). A query without a shard key in context is an error — there is no implicit cross-shard fan-out.
func NewShardRouter ¶ added in v1.0.0
func NewShardRouter(shards map[string]*Client, resolve ShardResolver, shardFor ShardFunc) (*ShardRouter, error)
NewShardRouter builds a router over a fixed set of shards (name -> Client). resolve extracts the shard key from context; shardFor maps that key to a shard name. It errors if shards is empty or either function is nil — those are setup mistakes worth catching at construction.
func (*ShardRouter) GetClient ¶ added in v1.0.0
func (r *ShardRouter) GetClient(ctx context.Context) (*Client, error)
GetClient implements ClientProvider: it resolves the shard key from ctx, maps it to a shard, and returns that shard's Client. It errors when no shard key is in context (no implicit fan-out) or when the mapped shard is unknown.
func (*ShardRouter) ShardNames ¶ added in v1.0.0
func (r *ShardRouter) ShardNames() []string
ShardNames returns the registered shard names (unspecified order). Useful for migrating/onboarding every shard. The Clients themselves are not exposed — route through For[T] so the shard-key discipline is preserved.
type Subquery ¶ added in v0.4.0
type Subquery struct {
// contains filtered or unexported fields
}
Subquery is a rendered SELECT that can be embedded inside another query through the Expr AST (Sub, Exists, NotExists, InSub, NotInSub).
A subquery is built like any other query — `For[T](...).Where(...)` — and then captured with `AsSubquery()`. The capture eagerly renders the SELECT using the active dialect's identifier quoting but with `?` as the bind marker, so the outer query's `buildWhereClause` can swap each `?` for the dialect's placeholder syntax at the correct arg index when the AST is rendered.
This contract matches the rest of the AST: leaves emit `?`, `substitutePathMarkers` does the placeholder rewrite, args are threaded through `condition.extraArgs`. So a subquery is just another Expr leaf.
type SyncOptions ¶
type SyncOptions struct {
DryRun bool // If true, logs the SQL but doesn't execute it.
NoTransaction bool // If true, doesn't wrap the sync in a transaction.
}
SyncOptions configures the behavior of the Sync operation.
type Table ¶ added in v0.6.0
type Table struct {
Name string
Columns []Column
Indexes []Index
ForeignKeys []ForeignKey
Checks []Check
}
Table represents one table in the schema. The neutral representation stores both the raw dialect-native type strings (`Type`) and (in a later phase) a normalised form for cross-dialect comparison.
type TenantConfig ¶
type TenantConfig struct {
Strategy TenantStrategy
MaxCachedPools int // Maximum number of DB connection pools to keep open (for DatabasePerTenant)
BaseClient *Client // Used for SchemaPerTenant, RowLevelSecurityClient and RowLevelSecurityNative
TenantColumn string // Column name for RowLevelSecurityClient, default is "tenant_id"
// NativeRLSVar is the PostgreSQL session variable name used by
// RowLevelSecurityNative to carry the resolved tenant ID. Each
// query under a Native router is wrapped in a transaction that
// calls `set_config(NativeRLSVar, <tenantID>, true)` before
// executing; the `CREATE POLICY` clauses installed by
// `quark tenant install-rls-policies` (F5-3) reference the same
// variable.
//
// Defaults to "app.tenant_id". Must be a valid PostgreSQL
// configuration parameter name (lowercase, dot-namespaced).
// Ignored when Strategy is not RowLevelSecurityNative.
NativeRLSVar string
}
TenantConfig configures the TenantRouter.
func DefaultTenantConfig ¶
func DefaultTenantConfig() TenantConfig
DefaultTenantConfig provides sensible defaults.
type TenantRouter ¶
type TenantRouter struct {
// contains filtered or unexported fields
}
TenantRouter manages dynamic database connections or queries for different tenants.
func NewTenantRouter ¶
func NewTenantRouter( config TenantConfig, resolver func(ctx context.Context) string, factory func(tenantID string) (*Client, error), ) *TenantRouter
NewTenantRouter creates a new router for multi-tenant database access.
func (*TenantRouter) ActiveTenants ¶
func (r *TenantRouter) ActiveTenants() []string
ActiveTenants returns a list of active tenant connections in the cache.
func (*TenantRouter) GetClient ¶
func (r *TenantRouter) GetClient(ctx context.Context) (*Client, error)
GetClient resolves the tenant ID from the context and returns the corresponding Client. It implements the ClientProvider interface so it can be used with For[T].
func (*TenantRouter) ResolveTenant ¶
func (r *TenantRouter) ResolveTenant(ctx context.Context) (string, error)
ResolveTenant returns the tenant ID for the context.
func (*TenantRouter) Tx ¶ added in v0.9.0
Tx opens a single transaction on the router's BaseClient, calls `set_config('<NativeRLSVar>', <resolvedTenantID>, true)` as the first statement, and invokes fn(tx). On nil return from fn the transaction commits; on error it rolls back. This is the recommended entry point under RowLevelSecurityNative — it avoids the per-query implicit-tx overhead and the connection-hold semantics described on nativeRLSExecutor.
For other strategies (DatabasePerTenant / SchemaPerTenant / RowLevelSecurityClient), Tx delegates to the underlying client's Tx without emitting the variable.
Returns ErrUnsupportedFeature wrapped with the dialect name when the BaseClient is not PostgreSQL under RowLevelSecurityNative.
type TenantStrategy ¶
type TenantStrategy int
TenantStrategy defines how multi-tenancy is handled.
const ( // DatabasePerTenant uses a separate database connection pool per tenant. // This requires an LRU cache to prevent connection exhaustion. DatabasePerTenant TenantStrategy = iota // SchemaPerTenant uses a single database connection pool but prefixes // the table name with the tenant ID (e.g. "tenant_acme.users"). SchemaPerTenant // RowLevelSecurityClient uses a single database connection pool and // injects a "WHERE tenant_id = ?" predicate into every query the // builder constructs. This is **client-side tenant scoping**, not // engine-enforced Row-Level Security: `client.Raw()` and `client.Exec()` // bypass the predicate. See ADR-0012 and `docs/playbooks/tenant.md` for // the limitations. On PostgreSQL, prefer RowLevelSecurityNative for // engine-enforced isolation. On other engines this remains the only // row-level option. RowLevelSecurityClient // RowLevelSecurityNative delegates row-level isolation to the // database engine via PostgreSQL row-level security policies. Each // query is wrapped in a transaction that first calls // `set_config('app.tenant_id', <tenantID>, true)` (i.e., SET LOCAL); // `CREATE POLICY` clauses on each tenant-scoped table reference that // session variable to filter rows. Unlike RowLevelSecurityClient, // `client.Raw()` / `client.Exec()` are still filtered: the policy // runs server-side and returns zero rows when `app.tenant_id` is not // set on the current transaction — there is no client-side bypass. // // A structured warning for Raw/Exec callers under a Native router // context is deferred to a follow-up (TASKS.md F5-2 closure block). // The engine enforcement is the security boundary; the warning would // be a developer-experience cue, not a safety net. // // Native is PostgreSQL-only. Constructing a Query[T] under a Native // router with a non-PostgreSQL dialect returns ErrUnsupportedFeature. // // See ADR-0012 §"Cómo se ejecuta SET LOCAL por query" for the // rationale and the F5-3 CLI for the DDL generator. RowLevelSecurityNative )
type Tracked ¶ added in v0.3.0
type Tracked[T any] struct { // Entity is the loaded value. Mutate fields on it directly; Save will // detect the changes and write only those columns. Entity *T // contains filtered or unexported fields }
Tracked wraps a loaded entity with a snapshot of its column values, so a later Save can emit an UPDATE limited to the fields that actually changed.
Active Record + dirty tracking ligero (Phase 1): the snapshot lives on the wrapper, not in the Client, so there is no shared map to grow or evict. Each Tracked carries the metadata it needs (client, table, dialect, pk, meta) to run Save without the caller threading state.
Tracked is the permanent fix for P0-4: a bool / int / string / pointer can be set to its zero value and Save will write it because the diff is taken against the snapshot, not against "is this field non-zero?".
func (*Tracked[T]) Changed ¶ added in v0.3.0
Changed reports the names (db tags) of columns whose value differs between Entity now and the snapshot taken at load time. Useful for tests and observability; Save uses the same comparison internally.
func (*Tracked[T]) Save ¶ added in v0.3.0
Save updates the row identified by Entity's primary key, writing only the columns whose value differs from the load-time snapshot. If nothing changed, Save returns (0, nil) without touching the database.
Returns the number of rows affected. Refreshes the internal snapshot on success so subsequent Save calls diff against the new state.
type TrackedQuery ¶ added in v0.3.0
type TrackedQuery[T any] struct { // contains filtered or unexported fields }
TrackedQuery is the lightweight wrapper Query[T].Track() returns. It re-issues Find/First/List on the underlying query and wraps each loaded entity with a snapshot for later dirty-tracked Save.
func (*TrackedQuery[T]) Find ¶ added in v0.3.0
func (tq *TrackedQuery[T]) Find(id any) (*Tracked[T], error)
Find loads a single entity by primary key and wraps it in a Tracked.
func (*TrackedQuery[T]) First ¶ added in v0.3.0
func (tq *TrackedQuery[T]) First() (*Tracked[T], error)
First returns the first matching entity wrapped in a Tracked.
func (*TrackedQuery[T]) List ¶ added in v0.3.0
func (tq *TrackedQuery[T]) List() ([]*Tracked[T], error)
List loads all matching entities wrapped in Tracked values.
type Tx ¶
type Tx struct {
// contains filtered or unexported fields
}
Tx wraps *sql.Tx and provides transactional query execution. It shares dialect, guard, observers, and limits from the parent Client.
func TxFromContext ¶ added in v0.9.0
TxFromContext returns the *Tx the context is currently scoped to, or nil when the context was not enriched by a ForTx call. Note that this includes the Client.Tx callback body itself: that callback receives the *Tx as a parameter, and the bare ctx it captures is NOT enriched until it flows through ForTx[T]. The helper is therefore meant for lifecycle hooks (which only get a context), not for the Tx callback body (which already has the tx).
The primary use is inside a lifecycle hook: the hook interfaces (BeforeCreateHook, AfterUpdateHook, …) receive only a context, so a hook that needs to register a commit/rollback side-effect reaches the transaction through this helper:
func (o *Order) AfterCreate(ctx context.Context) error {
if tx := quark.TxFromContext(ctx); tx != nil {
tx.OnCommit(func(ctx context.Context) error {
return bus.Publish(ctx, OrderCreated{ID: o.ID})
})
}
return nil
}
Returning nil is the normal case for non-transactional CRUD; the caller must nil-check before use.
func (*Tx) Commit ¶
Commit commits the transaction.
When commit succeeds, any model After* hooks queued during the transaction lifetime are drained in FIFO order. A hook returning an error is logged via the Client's slog logger (event: `quark.hook.after_post_commit_error`) but does NOT block the remaining hooks or fail the Commit — once the database confirmed the commit, no application-level handler can undo it (ADR-0013 Regla 2). Commit failures surface the underlying database error and the queued hooks are discarded.
The context each After* hook receives is the context the originating Query[T] captured at construction time (the ctx passed to ForTx). If the caller installed a deadline on that context via context.WithTimeout inside the Client.Tx callback, the hook may observe an already-expired ctx after Commit returns. Hooks that need a fresh context can derive one from context.Background inside their implementation.
func (*Tx) OnCommit ¶ added in v0.9.0
OnCommit registers a callback to run after the transaction commits successfully. Callbacks fire in FIFO registration order, after the model After* hooks, with the transaction's context. A callback returning an error is logged via the Client's slog logger (event `quark.hook.on_commit_error`) but does NOT block the remaining callbacks — once the database has confirmed the commit, no application-level handler can undo it (ADR-0013 Regla 3).
If the transaction rolls back instead, registered OnCommit callbacks are discarded without firing.
Registering an OnCommit callback from inside another OnCommit callback (i.e. during the drain) is a no-op for the current commit: the queue was already lifted before the drain began, so the newly-registered fn will not fire. This prevents unbounded re-entrancy.
Example:
err := client.Tx(ctx, func(tx *quark.Tx) error {
if err := quark.ForTx[Order](ctx, tx).Create(o); err != nil {
return err
}
tx.OnCommit(func(ctx context.Context) error {
return bus.Publish(ctx, OrderCreated{ID: o.ID})
})
return nil
})
func (*Tx) OnRollback ¶ added in v0.9.0
OnRollback registers a callback to run after the transaction rolls back. Callbacks fire in FIFO registration order with the transaction's context. A callback returning an error is logged via the Client's slog logger (event `quark.hook.on_rollback_error`) but does NOT block the remaining callbacks.
If the transaction commits instead, registered OnRollback callbacks are discarded without firing.
Example:
tx.OnRollback(func(ctx context.Context) error {
log.Warn("order create rolled back", "id", o.ID)
return nil
})
func (*Tx) ReleaseSavepoint ¶
ReleaseSavepoint releases the named savepoint. Hooks queued since it was set stay on the transaction queues — released work is part of the surrounding transaction and its side-effects fire with it.
func (*Tx) Rollback ¶
Rollback aborts the transaction.
All model After* hooks and OnCommit callbacks queued during the transaction lifetime are discarded without firing — rolled-back work never triggers the side-effects that would have followed it (ADR-0013 Regla 2). OnRollback callbacks DO fire, in FIFO order, after the underlying rollback is attempted, so callers can react to the abort (release a reservation, emit a "cancelled" event, etc.). An OnRollback callback returning an error is logged but does not block the rest of the chain.
func (*Tx) RollbackTo ¶
RollbackTo rolls back to the named savepoint. Beyond undoing the SQL written since the savepoint, it discards the After*/OnCommit/ OnRollback hooks queued in that window so the rolled-back work does not trigger its side-effects on the eventual commit (ADR-0013 Regla 2). Reactions to the partial rollback flow through the error the nested scope returns, not through OnRollback (which fires only on a whole-transaction rollback).
func (*Tx) Savepoint ¶
Savepoint creates a savepoint with the given name. Hooks queued by CRUD run between this call and a matching Tx.RollbackTo are unwound by that rollback (see Tx.RollbackTo).
type TypeMapper ¶ added in v0.3.0
type TypeMapper = migrate.TypeMapper
TypeMapper produces a dialect-specific SQL type for a Go type. The caller supplies the dialect name (lower-case: "postgres", "mysql", "mariadb", "sqlite", "mssql", "oracle") and the sizing hints from the field's tag.
type TypeOptions ¶ added in v0.3.0
type TypeOptions = migrate.TypeOptions
TypeOptions carries the SQL-type sizing hints parsed from a struct's db tag — `size=N`, `precision=N`, `scale=N` — plus a flag indicating whether the column is the primary key. A zero value for any field means "use the dialect default for the Go type".
type TypedBinder ¶ added in v0.11.0
TypedBinder returns the column names and bind arguments for entity — a non-nil pointer to the model value (*T) — for the given mode, without reflection. Implemented by generated code and registered via RegisterTypedBinder. Not intended for hand use.
type TypedColumn ¶ added in v0.12.0
type TypedColumn[T any] struct { // contains filtered or unexported fields }
TypedColumn is a typed handle to one model column, parameterised by the Go type of the underlying field. Generated code constructs it via NewTypedColumn; its methods produce [Predicate]s whose value argument is typed T, so passing a value of the wrong type for the column is a compile error.
func NewTypedColumn ¶ added in v0.12.0
func NewTypedColumn[T any](name string) TypedColumn[T]
NewTypedColumn builds a typed column handle for the given SQL column name. It is intended to be called only from generated code, where name is the model's `db` tag column.
func (TypedColumn[T]) Between ¶ added in v0.12.0
func (c TypedColumn[T]) Between(lo, hi T) Predicate
Between builds `column BETWEEN lo AND hi`.
func (TypedColumn[T]) Eq ¶ added in v0.12.0
func (c TypedColumn[T]) Eq(v T) Predicate
Eq builds `column = value`.
func (TypedColumn[T]) Gt ¶ added in v0.12.0
func (c TypedColumn[T]) Gt(v T) Predicate
Gt builds `column > value`.
func (TypedColumn[T]) Gte ¶ added in v0.12.0
func (c TypedColumn[T]) Gte(v T) Predicate
Gte builds `column >= value`.
func (TypedColumn[T]) In ¶ added in v0.12.0
func (c TypedColumn[T]) In(values ...T) Predicate
In builds `column IN (values...)`. Pass at least one value: with none it lowers to the same empty-IN condition as the string WhereIn — which SQLite treats as matching nothing but PostgreSQL, Oracle, and SQL Server reject as invalid SQL. (WhereP is a faithful lowering of the string API, so it does not paper over that engine difference.)
func (TypedColumn[T]) IsNotNull ¶ added in v0.12.0
func (c TypedColumn[T]) IsNotNull() Predicate
IsNotNull builds `column IS NOT NULL`.
func (TypedColumn[T]) IsNull ¶ added in v0.12.0
func (c TypedColumn[T]) IsNull() Predicate
IsNull builds `column IS NULL`.
func (TypedColumn[T]) Lt ¶ added in v0.12.0
func (c TypedColumn[T]) Lt(v T) Predicate
Lt builds `column < value`.
func (TypedColumn[T]) Lte ¶ added in v0.12.0
func (c TypedColumn[T]) Lte(v T) Predicate
Lte builds `column <= value`.
func (TypedColumn[T]) Name ¶ added in v0.12.0
func (c TypedColumn[T]) Name() string
Name returns the SQL column name the handle refers to.
func (TypedColumn[T]) Neq ¶ added in v0.12.0
func (c TypedColumn[T]) Neq(v T) Predicate
Neq builds `column != value`.
func (TypedColumn[T]) NotIn ¶ added in v0.12.0
func (c TypedColumn[T]) NotIn(values ...T) Predicate
NotIn builds `column NOT IN (values...)`. Pass at least one value — see In for the empty-list caveat.
type TypedScanner ¶ added in v0.11.0
TypedScanner scans the current row of rows into dest — a non-nil pointer to the model value (*T) — without reflection. It is implemented by generated code and registered via RegisterTypedScanner. Not intended for hand use.
type TypedStringColumn ¶ added in v0.12.0
type TypedStringColumn struct {
TypedColumn[string]
}
TypedStringColumn is a TypedColumn[string] that additionally offers the text-only operators LIKE / NOT LIKE. The generator uses it for string-typed fields so pattern matching is offered only where it is meaningful, while every TypedColumn[string] method (Eq, In, …) remains available via embedding.
func NewTypedStringColumn ¶ added in v0.12.0
func NewTypedStringColumn(name string) TypedStringColumn
NewTypedStringColumn builds a typed string-column handle. Intended to be called only from generated code.
func (TypedStringColumn) Like ¶ added in v0.12.0
func (c TypedStringColumn) Like(pattern string) Predicate
Like builds `column LIKE pattern`.
func (TypedStringColumn) NotLike ¶ added in v0.12.0
func (c TypedStringColumn) NotLike(pattern string) Predicate
NotLike builds `column NOT LIKE pattern`.
type Window ¶ added in v0.4.0
type Window struct {
// contains filtered or unexported fields
}
Window models the `OVER (...)` clause for a window-function expression. Build with NewWindow() and chain PartitionBy / OrderBy. The chain is immutable: each method returns a fresh copy so a Window definition can be reused across multiple Over() calls without aliasing.
func NewWindow ¶ added in v0.4.0
func NewWindow() *Window
NewWindow returns an empty Window. An empty Window renders as `OVER ()` — sometimes legitimate (e.g. `COUNT(*) OVER ()` for a running grand total), so it's not an error.
func (*Window) OrderBy ¶ added in v0.4.0
OrderBy adds an order entry. Set desc=true for descending; the second argument is the conventional bool toggle to keep the API tight (no "ASC"/"DESC" stringly-typed argument).
func (*Window) PartitionBy ¶ added in v0.4.0
PartitionBy adds one or more partition expressions. Identifiers go through SQLGuard at render time — pass `Col("status")`, not the raw column string.
Source Files
¶
- array.go
- audit.go
- cache.go
- cache_invalidation.go
- cache_stampede.go
- client.go
- client_registry.go
- codegen_registry.go
- cte.go
- cursor.go
- db_errors.go
- dialect.go
- dialect_introspection.go
- dialect_lock.go
- dialect_migration_lock.go
- dirty_track.go
- errors.go
- events.go
- expr.go
- hooks.go
- hooks_find.go
- json_field.go
- locking.go
- migrate_backfill.go
- migrate_diff.go
- migrate_execute.go
- migrate_plan.go
- migrate_state.go
- migration_lock.go
- migrator.go
- model.go
- nullable.go
- optimistic_locking.go
- option.go
- page.go
- pg_listener.go
- preload_loaders.go
- preload_tree.go
- query_builder.go
- query_crud.go
- query_exec.go
- replicas.go
- rls_native.go
- routine_builder.go
- schema.go
- security.go
- setop.go
- shard_router.go
- slow_query_log.go
- soft_delete.go
- subquery.go
- sync.go
- tenant_router.go
- timezone.go
- tx.go
- type_mapper.go
- typed_columns.go
- validator.go
- window.go
Directories
¶
| Path | Synopsis |
|---|---|
|
cache
|
|
|
cmd
|
|
|
quark
command
Command quark is the Quark ORM CLI: model scaffolding, migrations, seeders, multi-tenancy helpers, and code generation.
|
Command quark is the Quark ORM CLI: model scaffolding, migrations, seeders, multi-tenancy helpers, and code generation. |
|
quark/internal/codegen
Package codegen implements `quark gen`: it parses a user's package with go/packages + go/types (not reflection, so the tool can be `go install`ed and driven from //go:generate) and emits, per package, a quark_gen.go that registers typed implementations with the runtime registry in package quark.
|
Package codegen implements `quark gen`: it parses a user's package with go/packages + go/types (not reflection, so the tool can be `go install`ed and driven from //go:generate) and emits, per package, a quark_gen.go that registers typed implementations with the runtime registry in package quark. |
|
quark/internal/codegen/sample
Package sample holds a representative model used by the codegen tests: the conformance test loads it via go/packages (AST) and via reflection, and the golden test regenerates its quark_gen.go and compares.
|
Package sample holds a representative model used by the codegen tests: the conformance test loads it via go/packages (AST) and via reflection, and the golden test regenerates its quark_gen.go and compares. |
|
examples
|
|
|
migrations
command
Command migrations is a minimal example of using the `quarkmigrate` package to wire a plan/verify/apply CLI workflow for a Quark-managed schema.
|
Command migrations is a minimal example of using the `quarkmigrate` package to wire a plan/verify/apply CLI workflow for a Quark-managed schema. |
|
mssql
command
|
|
|
mysql
command
|
|
|
oracle
command
|
|
|
postgres
command
|
|
|
sharding
command
Sharding example (F6-7, ADR-0016).
|
Sharding example (F6-7, ADR-0016). |
|
sqlite
command
|
|
|
superapp/cli
Package cli es el área de cobertura del BINARIO `cmd/quark` dentro del arnés de aceptación (superapp S9).
|
Package cli es el área de cobertura del BINARIO `cmd/quark` dentro del arnés de aceptación (superapp S9). |
|
superapp/cmd/gen-apisurface
command
Command gen-apisurface genera apisurface.json: el DENOMINADOR del gate de cobertura del superapp — todo símbolo exportado de la superficie pública de Quark (el paquete raíz + los subpaquetes públicos).
|
Command gen-apisurface genera apisurface.json: el DENOMINADOR del gate de cobertura del superapp — todo símbolo exportado de la superficie pública de Quark (el paquete raíz + los subpaquetes públicos). |
|
superapp/cmd/workload
command
Command workload ejerce el dominio de la superapp a alto volumen y emite un informe ejecutivo, métricas y el log de la aplicación, para contrastar cómo corre Quark con datos relacionados, consultas, transacciones y caché.
|
Command workload ejerce el dominio de la superapp a alto volumen y emite un informe ejecutivo, métricas y el log de la aplicación, para contrastar cómo corre Quark con datos relacionados, consultas, transacciones y caché. |
|
superapp/control
Package control implementa la maquinaria de cobertura, paridad y gating de la superapp de aceptación: el manifiesto de superficie pública, el reconciliador invocado-vs-manifiesto, la matriz de capacidad por motor y el reporte/gate.
|
Package control implementa la maquinaria de cobertura, paridad y gating de la superapp de aceptación: el manifiesto de superficie pública, el reconciliador invocado-vs-manifiesto, la matriz de capacidad por motor y el reporte/gate. |
|
superapp/domain
Package domain define el modelo de la superapp.
|
Package domain define el modelo de la superapp. |
|
superapp/engine
Package engine arranca y teardownea los 6 motores para el arnés del superapp, y verifica que no haya fugas (goroutines, conexiones del pool) tras cerrar.
|
Package engine arranca y teardownea los 6 motores para el arnés del superapp, y verifica que no haya fugas (goroutines, conexiones del pool) tras cerrar. |
|
superapp/exercise
Package exercise ejerce la superficie pública de Quark contra cada motor, marcando los símbolos que invoca (para el gate de cobertura del manifiesto) y asertando el resultado funcional.
|
Package exercise ejerce la superficie pública de Quark contra cada motor, marcando los símbolos que invoca (para el gate de cobertura del manifiesto) y asertando el resultado funcional. |
|
superapp/recorder
Package recorder instrumenta un Client de Quark para la superapp de aceptación: observa cada statement SQL y lo atribuye al símbolo de la API pública que lo originó, alimentando dos mecanismos del arnés:
|
Package recorder instrumenta un Client de Quark para la superapp de aceptación: observa cada statement SQL y lo atribuye al símbolo de la API pública que lo originó, alimentando dos mecanismos del arnés: |
|
superapp/workload
Package workload ejerce el dominio de la superapp a ALTO VOLUMEN —datos relacionados, consultas, transacciones y caché— y recolecta métricas para un informe ejecutivo.
|
Package workload ejerce el dominio de la superapp a ALTO VOLUMEN —datos relacionados, consultas, transacciones y caché— y recolecta métricas para un informe ejecutivo. |
|
tenant-rls-native
command
Example: quark tenant install-rls-policies — minimal embedding of the quarktenant library into a user-owned binary that can be run from CI / Makefile to install PostgreSQL row-level security policies for every model registered on the quark.Client.
|
Example: quark tenant install-rls-policies — minimal embedding of the quarktenant library into a user-owned binary that can be run from CI / Makefile to install PostgreSQL row-level security policies for every model registered on the quark.Client. |
|
internal
|
|
|
guard
Package guard provides SQL injection prevention utilities for Quark ORM.
|
Package guard provides SQL injection prevention utilities for Quark ORM. |
|
migrate
Package migrate provides internal utilities for database schema migrations.
|
Package migrate provides internal utilities for database schema migrations. |
|
schema
Package schema provides struct reflection and model metadata caching for Quark ORM.
|
Package schema provides struct reflection and model metadata caching for Quark ORM. |
|
Package otel provides a Quark Middleware that emits OpenTelemetry tracing spans and metrics for every database operation.
|
Package otel provides a Quark Middleware that emits OpenTelemetry tracing spans and metrics for every database operation. |
|
Package quarkmigrate is the thin CLI wrapper that turns a quark.Client + a set of Go model values into a plan/verify/apply workflow.
|
Package quarkmigrate is the thin CLI wrapper that turns a quark.Client + a set of Go model values into a plan/verify/apply workflow. |
|
Package quarktenant is the thin CLI wrapper that turns a quark.Client + a set of registered Go models into a tenant row-level-security install workflow.
|
Package quarktenant is the thin CLI wrapper that turns a quark.Client + a set of registered Go models into a tenant row-level-security install workflow. |