Help us shape the future of Go! Take the 2026 Go Developer Survey

sql

package module
v1.0.0-beta.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package sql connects a Dygo app to MySQL or Postgres. It stays close to database/sql and plain SQL; it is not an ORM.

Import the module for your database once, in the root module:

import "github.com/dygoframework/dygo/sql/postgres" // or sql/mysql

var AppModule = &dygo.Module{
	Imports: []*dygo.Module{postgres.Module(), users.Module},
}

The module is global and provides *sql.DB to every provider:

func NewRepo(db *sql.DB) *Repo { return &Repo{db: db} }

At boot it reads Config from the environment (DATABASE_URL, DB_MAX_OPEN, ...), opens the pool and pings the database, so a wrong URL or password stops the app before it listens. The password never appears in errors or logs. On shutdown the pool is closed after in-flight requests finish, and GET /health/ready reports the database as "db".

Driver errors become *Error values with DB.MapError: a duplicate key matches ErrDuplicate with errors.Is and answers 409, a missing row matches ErrNotFound and answers 404, with no driver import in your controllers.

Queries are written with ? on both databases, and results are scanned into structs by db tag (or the snake_case field name):

var u User
err := db.Get(ctx, &u, "SELECT id, email FROM users WHERE id = ?", id)
var list []User
err = db.Select(ctx, &list, "SELECT * FROM users WHERE id IN (?)", sql.In(ids))
_, err = db.Exec(ctx, "UPDATE users SET active = ? WHERE id = ?", false, id)

Transactions travel in ctx: every query that gets fn's ctx joins it, and a nested Tx is a savepoint. Use Detach(ctx) for goroutines started inside.

err := db.Tx(ctx, func(ctx context.Context) error {
	if err := orders.Create(ctx, o); err != nil {
		return err // rolls back
	}
	return stock.Take(ctx, o.ProductID, o.Qty)
})

For CRUD without writing SQL, map a struct with db tags and provide a repository in the feature module:

type User struct {
	ID        int64     `db:"id,pk,auto"`
	Email     string    `db:"email"`
	CreatedAt time.Time `db:"created_at,created"`
	UpdatedAt time.Time `db:"updated_at,updated"`
}

Providers: []dygo.Provider{sql.ForFeature[User]("users"), dygo.Provide(NewService)}

func NewService(users *sql.Repository[User]) *Service

u, err := users.Find(ctx, id) // ErrNotFound answers 404
page, err := users.Page(ctx, 1, 20, sql.Where("active = ?", true), sql.OrderBy(in.Sort))
err = users.Create(ctx, &u)   // sets ID, CreatedAt, UpdatedAt
err = users.Patch(ctx, id, sql.Set{"email": in.Email})

DATABASE_URL=memory:// keeps the data in memory instead, for the start of a project and for tests: repositories work the same, so moving to a real database is changing the URL. See MemoryURL.

For a second database, give it a prefix and a name:

postgres.Module(sql.Prefix("REPORTS_"), sql.Named("reports"))

and inject it with a field tagged name:"reports" in a dygo.In struct, or put a repository on it with sql.ForFeature[T](table, sql.Using("reports")).

This package is v0: its API can still change before v1. See docs/phase-8-database.md for the plan.

Index

Constants

View Source
const (
	LevelDefault         = stdsql.LevelDefault
	LevelReadCommitted   = stdsql.LevelReadCommitted
	LevelRepeatableRead  = stdsql.LevelRepeatableRead
	LevelSerializable    = stdsql.LevelSerializable
	LevelReadUncommitted = stdsql.LevelReadUncommitted
)

Isolation levels for TxOptions.

View Source
const MemoryURL = "memory://"

MemoryURL is the DATABASE_URL that keeps the data in memory instead of a database, the default in an app made by dygo new. The data is lost when the app stops. Change DATABASE_URL to a Postgres or MySQL URL to store it in that database; the code stays the same.

In memory, Repository keeps a table per name, and DB.Tx runs its function without a transaction. Raw SQL (DB.Get, DB.Exec, ...) and Where clauses need a real database and fail with ErrMemory.

Variables

View Source
var (
	// ErrNotFound means no row matched. It answers 404.
	ErrNotFound = errors.New("not found")
	// ErrDuplicate means a unique constraint was violated. It answers 409.
	ErrDuplicate = errors.New("duplicate")
	// ErrForeignKey means a foreign key was violated. It answers 409.
	ErrForeignKey = errors.New("foreign key violation")
	// ErrConstraint means a not null or check constraint was violated. It
	// answers 422.
	ErrConstraint = errors.New("constraint violation")
	// ErrRetryable means the transaction hit a serialization failure or a
	// deadlock and may succeed if run again. It answers 500.
	ErrRetryable = errors.New("retryable")
)

Errors that mapped errors match with errors.Is.

View Source
var ErrMemory = errors.New("sql: DATABASE_URL is memory://, which runs no SQL; set a Postgres or MySQL URL in .env")

ErrMemory is the error of SQL run on a MemoryURL database.

View Source
var ErrPlaceholders = errors.New("sql: placeholders do not match arguments")

ErrPlaceholders means a query's ? placeholders do not match its arguments.

View Source
var ErrTxDone = errors.New("sql: transaction already finished")

ErrTxDone means a query ran with the context of a transaction that has already committed or rolled back, usually from a goroutine or async listener started inside DB.Tx. Pass Detach(ctx) to such work.

View Source
var ErrUnknownColumn = errors.New("unknown column")

ErrUnknownColumn means OrderBy or Patch named a column that T does not map. It answers 400.

Functions

func Detach

func Detach(ctx context.Context) context.Context

Detach returns ctx without its transactions, keeping its other values and its cancellation. Use it for work that may outlive the transaction, such as a goroutine started inside DB.Tx, so its queries run on the pool instead of failing with ErrTxDone.

func ForFeature

func ForFeature[T any](table string, opts ...FeatureOption) dygo.Provider

ForFeature provides *sql.Repository[T] for table. Put it in the feature module's providers:

Providers: []dygo.Provider{sql.ForFeature[User]("users"), dygo.Provide(NewService)}

func NewService(users *sql.Repository[User]) *Service

func In

func In(slice any) any

In expands a slice into a comma separated list of placeholders, for WHERE id IN (?):

db.Select(ctx, &users, "SELECT * FROM users WHERE id IN (?)", sql.In(ids))

An empty slice is an error, because IN () is not valid SQL.

func InTx

func InTx(ctx context.Context) bool

InTx reports whether ctx carries a transaction of any pool.

func IsMemory

func IsMemory(url string) bool

IsMemory reports whether url is MemoryURL.

func MapError

func MapError(d Dialect, err error) error

MapError is DB.MapError for a dialect.

func MemoryModule

func MemoryModule() *dygo.Module

MemoryModule is Module with an empty in-memory database whatever .env says, for tests of a feature module:

dygotest.New(&dygo.Module{Name: "test", Imports: []*dygo.Module{sql.MemoryModule(), users.Module}})

func Module

func Module(d Dialect, opts ...Option) *dygo.Module

Module returns a global module that provides *DB (or Token(name) with Named) to the whole app. The pool is opened and pinged at boot, closed after the HTTP drain on shutdown, and added to GET /health/ready as "db" (or the name). The sql/postgres and sql/mysql packages wrap it with their dialect:

Imports: []*dygo.Module{postgres.Module()}

Module(nil), which an app made by dygo new imports, has no driver: it opens DATABASE_URL=memory:// (MemoryURL, also the default when the variable is not set) and stops the boot with a hint to run dygo db init for any other URL.

func Token

func Token(name string) dygo.NamedToken[*DB]

Token is the named token Named provides the pool under.

Types

type Clause

type Clause func(*clauses)

Clause narrows or orders the rows of Repository.List, FindBy, Count, Exists and Page.

func Limit

func Limit(n int) Clause

Limit returns at most n rows.

func Offset

func Offset(n int) Clause

Offset skips the first n rows.

func OrderBy

func OrderBy(cols ...string) Clause

OrderBy sorts by columns, each "name" or "name ASC" / "name DESC" (several may be given in one string, separated by commas). Names must be columns of T, so a sort parameter from the request is safe to pass; an unknown one is ErrUnknownColumn (400).

func Where

func Where(cond string, args ...any) Clause

Where adds a condition, written in SQL with ? placeholders. Several Where clauses are joined with AND. Pass user input only as arguments, never by building cond with string concatenation:

sql.Where("email = ? AND deleted_at IS NULL", email)

type Config

type Config struct {
	// URL is the connection URL: postgres://user:pass@host:5432/db or
	// mysql://user:pass@host:3306/db. A driver native DSN works too.
	URL string `env:"DATABASE_URL" required:"true"`
	// MaxOpen limits open connections (in use plus idle).
	MaxOpen int `env:"DB_MAX_OPEN" default:"25"`
	// MaxIdle limits idle connections kept in the pool.
	MaxIdle int `env:"DB_MAX_IDLE" default:"25"`
	// ConnMaxLifetime closes connections older than this. Keep it below the
	// server's own idle timeout (MySQL wait_timeout is 8 hours by default).
	ConnMaxLifetime time.Duration `env:"DB_CONN_MAX_LIFETIME" default:"30m"`
	// ConnMaxIdleTime closes connections idle for longer than this.
	ConnMaxIdleTime time.Duration `env:"DB_CONN_MAX_IDLE_TIME" default:"5m"`
	// ConnectTimeout bounds the ping at boot.
	ConnectTimeout time.Duration `env:"DB_CONNECT_TIMEOUT" default:"5s"`
}

Config is the connection and pool configuration. Module loads it from the environment (and .env files) with the config package, so every variable below can be set there; Prefix namespaces them for a second database.

type DB

type DB struct {
	// contains filtered or unexported fields
}

DB is a connection pool for one database. It is safe for concurrent use. Use DB.SQL for the underlying *database/sql.DB.

func Memory

func Memory() *DB

Memory returns an empty in-memory database, for tests:

users, _ := sql.NewRepository[User](sql.Memory(), "users")

func Open

func Open(ctx context.Context, d Dialect, cfg Config) (*DB, error)

Open connects using cfg: it opens the pool, applies the pool settings and pings the database within cfg.ConnectTimeout, so a wrong URL or password is reported at boot. The error never contains the password.

func (*DB) Close

func (db *DB) Close() error

Close closes the pool. Module calls it on shutdown; call it yourself only for a DB made with Open.

func (*DB) Dialect

func (db *DB) Dialect() Dialect

Dialect returns the database dialect.

func (*DB) Exec

func (db *DB) Exec(ctx context.Context, query string, args ...any) (Result, error)

Exec runs a statement that returns no rows, such as INSERT, UPDATE or DELETE. Write placeholders as ?. Inside DB.Tx it runs in the transaction.

func (*DB) Get

func (db *DB) Get(ctx context.Context, dest any, query string, args ...any) error

Get runs a query and scans its first row into dest, a pointer to a struct (columns matched to db tags, or to the snake_case field name) or to a single value (int64, string, time.Time, any sql.Scanner) for a one column query. No row is ErrNotFound, which answers 404.

var u User
err := db.Get(ctx, &u, "SELECT id, email FROM users WHERE id = ?", id)

func (*DB) IsMemory

func (db *DB) IsMemory() bool

IsMemory reports whether the data is kept in memory (MemoryURL).

func (*DB) MapError

func (db *DB) MapError(err error) error

MapError turns a driver error into an *Error when the dialect recognises it, and returns any other error unchanged. sql.ErrNoRows becomes ErrNotFound. Query methods call it for you; call it yourself on errors from DB.SQL.

func (*DB) Name

func (db *DB) Name() string

Name is the dialect name, or the name given with Named.

func (*DB) OnApplicationShutdown

func (db *DB) OnApplicationShutdown(context.Context, string) error

OnApplicationShutdown closes the pool. It runs after the HTTP server has drained in-flight requests, so no handler loses its connection mid-query.

func (*DB) PingContext

func (db *DB) PingContext(ctx context.Context) error

PingContext checks that the database answers. The health indicator uses it.

func (*DB) Query

func (db *DB) Query(ctx context.Context, query string, args ...any) (*Rows, error)

Query runs a query and returns its rows, for streaming large results. Close the rows when done. Most code wants DB.Get or DB.Select instead.

func (*DB) SQL

func (db *DB) SQL() *stdsql.DB

SQL returns the underlying pool, for libraries that take a *database/sql.DB (sqlc, migration tools, GORM). It is nil for a MemoryURL database.

func (*DB) Select

func (db *DB) Select(ctx context.Context, dest any, query string, args ...any) error

Select runs a query and scans every row into dest, a pointer to a slice of structs, of pointers to structs, or of single values. The slice is reset first; no rows leaves it empty, not nil-error.

var users []User
err := db.Select(ctx, &users, "SELECT * FROM users WHERE active = ? ORDER BY id", true)

func (*DB) Stats

func (db *DB) Stats() stdsql.DBStats

Stats returns the pool statistics.

func (*DB) Transactional

func (db *DB) Transactional(opts ...TxOptions) dygo.RouteOption

Transactional is a route option that runs the whole handler, including interceptors after it and the pipes, in one transaction. The handler's ctx carries the transaction; an error or panic rolls it back.

r.Post("/orders", dygo.Handle(c.Create), c.db.Transactional())

func (*DB) Tx

func (db *DB) Tx(ctx context.Context, fn func(ctx context.Context) error) error

Tx runs fn in a transaction: it commits when fn returns nil and rolls back when fn returns an error or panics (the panic goes on after the rollback). Every query on this pool that gets fn's ctx, directly or through a repository, runs in the transaction:

err := db.Tx(ctx, func(ctx context.Context) error {
	if _, err := db.Exec(ctx, "UPDATE stock SET qty = qty - ? WHERE id = ?", n, id); err != nil {
		return err
	}
	_, err := db.Exec(ctx, "INSERT INTO orders (product_id, qty) VALUES (?, ?)", id, n)
	return err
})

Called inside another Tx on the same pool, it becomes a savepoint: an error rolls back only fn's work, and the outer transaction goes on.

func (*DB) TxWith

func (db *DB) TxWith(ctx context.Context, opts TxOptions, fn func(ctx context.Context) error) error

TxWith is DB.Tx with options. Inside an existing transaction the options are ignored, since a savepoint has the outer transaction's settings.

type Dialect

type Dialect interface {
	// Name is the database name used in logs and errors, such as "postgres".
	Name() string
	// DriverName is the database/sql driver to open, such as "pgx".
	DriverName() string
	// DSN turns Config.URL into the driver's connection string, and reports
	// a malformed URL before anything is dialed.
	DSN(url string) (string, error)
	// Classify reports what kind of failure a driver error is, and the
	// constraint involved when the database names one.
	Classify(err error) (kind Kind, constraint string)
	// Placeholder is the n-th (from 1) bind parameter: "?" on MySQL, "$1" on
	// Postgres. Queries are always written with "?" and rewritten with it.
	Placeholder(n int) string
	// BackslashEscapes reports whether a backslash escapes a quote inside a
	// plain '...' string (MySQL), so the rewriter can find where it ends.
	BackslashEscapes() bool
	// Quote quotes an identifier, such as a table or column name: "name" on
	// Postgres, `name` on MySQL. A dotted name (schema.table) is quoted part
	// by part.
	Quote(ident string) string
	// Returning reports whether INSERT ... RETURNING gives back the columns
	// the database set (Postgres). Without it, [Repository.Create] reads the
	// one auto increment column from LastInsertId (MySQL).
	Returning() bool
}

Dialect adapts the module to one database. The sql/postgres and sql/mysql packages provide one each. The interface may still grow before v1.

type Error

type Error struct {
	// Kind is ErrNotFound, ErrDuplicate, ErrForeignKey, ErrConstraint or
	// ErrRetryable.
	Kind error
	// Constraint is the constraint or key the database named, if any.
	Constraint string
	// Err is the driver's error.
	Err error
}

Error is a database error the module recognised. errors.Is matches its kind (ErrDuplicate, ...) and errors.As still reaches the driver's error (*pgconn.PgError, *mysql.MySQLError). The constraint name is logged but never sent to the client.

func (*Error) Error

func (e *Error) Error() string

func (*Error) HTTPError

func (e *Error) HTTPError() *errs.HTTPError

HTTPError makes Dygo's error writer answer with a fitting status instead of 500: 404 for ErrNotFound, 409 for ErrDuplicate and ErrForeignKey, 422 for ErrConstraint. A retryable error stays a 500.

func (*Error) Unwrap

func (e *Error) Unwrap() []error

Unwrap returns the kind and the driver error.

type FeatureOption

type FeatureOption func(*featureOptions)

FeatureOption configures ForFeature.

func Using

func Using(name string) FeatureOption

Using picks the pool provided with sql.Named(name) instead of the default *sql.DB.

type IsolationLevel

type IsolationLevel = stdsql.IsolationLevel

IsolationLevel is a transaction isolation level.

type Kind

type Kind int

Kind is the class of a database error, as reported by Dialect.Classify.

const (
	// KindOther is any error the dialect does not recognise.
	KindOther Kind = iota
	// KindDuplicate is a unique or primary key violation.
	KindDuplicate
	// KindForeignKey is a foreign key violation.
	KindForeignKey
	// KindConstraint is a not null or check constraint violation.
	KindConstraint
	// KindRetryable is a serialization failure or deadlock: running the
	// transaction again may succeed.
	KindRetryable
)

The kinds of database errors the module recognises.

type Null

type Null[T any] = stdsql.Null[T]

Null is any value that may be NULL.

type NullBool

type NullBool = stdsql.NullBool

NullBool is a bool that may be NULL.

type NullFloat64

type NullFloat64 = stdsql.NullFloat64

NullFloat64 is a float64 that may be NULL.

type NullInt32

type NullInt32 = stdsql.NullInt32

NullInt32 is an int32 that may be NULL.

type NullInt64

type NullInt64 = stdsql.NullInt64

NullInt64 is an int64 that may be NULL.

type NullString

type NullString = stdsql.NullString

NullString is a string that may be NULL.

type NullTime

type NullTime = stdsql.NullTime

NullTime is a time.Time that may be NULL.

type Option

type Option func(*moduleOptions)

Option configures Module.

func Files

func Files(paths ...string) Option

Files sets the .env files to read, earlier ones winning. The default is ".env.local" then ".env", the files the app generated by dygo new reads, so the app and the pool see the same DATABASE_URL; missing files are skipped.

func Named

func Named(name string) Option

Named provides the pool under the named token Token(name) instead of *sql.DB, so an app can have several databases. Inject it with a field tagged name:"name" in a dygo.In struct, or with dygo.GetNamed.

func NoHealth

func NoHealth() Option

NoHealth leaves out the readiness indicator.

func Prefix

func Prefix(p string) Option

Prefix puts p in front of every variable name: with Prefix("REPORTS_") the URL is read from REPORTS_DATABASE_URL and the pool size from REPORTS_DB_MAX_OPEN.

func WithConfig

func WithConfig(cfg Config) Option

WithConfig uses cfg instead of reading the environment.

type Page

type Page[T any] struct {
	Items []T   `json:"items"`
	Total int64 `json:"total"`
	Page  int   `json:"page"`
	Size  int   `json:"size"`
}

Page is one page of a listing, as Repository.Page returns it.

func (Page[T]) Pages

func (p Page[T]) Pages() int

Pages is the number of pages for Total rows.

type Repository

type Repository[T any] struct {
	// contains filtered or unexported fields
}

Repository gives CRUD methods for one table, mapped to the struct T by its db tags. Get one per entity with ForFeature in the feature module and inject *sql.Repository[T].

Tag options after the column name:

ID        int64     `db:"id,pk,auto"`         // primary key, set by the database
CreatedAt time.Time `db:"created_at,created"` // set by Create when zero
UpdatedAt time.Time `db:"updated_at,updated"` // set by Create, Update and Patch
Note      string    `db:"-"`                  // not a column

T needs exactly one pk column. auto columns are left out of INSERT and read back after it: with RETURNING on Postgres, from LastInsertId on MySQL (which allows one integer auto column). Every method runs in the transaction in ctx, if any.

The SQL for each method is built once, when the repository is created. Anything the repository cannot express is a custom query on the same struct through Repository.DB.

func NewRepository

func NewRepository[T any](db *DB, table string) (*Repository[T], error)

NewRepository builds the repository for T on table. ForFeature calls it at boot, so a tag error stops the app before it listens.

func (*Repository[T]) Count

func (r *Repository[T]) Count(ctx context.Context, clauses ...Clause) (int64, error)

Count returns the number of rows matching the Where clauses. OrderBy, Limit and Offset are ignored.

func (*Repository[T]) Create

func (r *Repository[T]) Create(ctx context.Context, v *T) error

Create inserts v and sets its auto columns from the database, and its created and updated columns to now. Values in auto columns are not written. A unique violation is ErrDuplicate (409).

func (*Repository[T]) CreateMany

func (r *Repository[T]) CreateMany(ctx context.Context, items []T) error

CreateMany inserts items with multi row INSERTs, sets created and updated columns in place, and on Postgres sets the auto columns too. (On MySQL, read them back with a query if you need them: LastInsertId gives only the first.) A large slice is split into several statements, run in one transaction so they all commit or none do.

func (*Repository[T]) DB

func (r *Repository[T]) DB() *DB

DB returns the pool, for custom queries on the same struct:

var out []User
err := users.DB().Select(ctx, &out, "SELECT u.* FROM users u JOIN teams t ON ...")

func (*Repository[T]) Delete

func (r *Repository[T]) Delete(ctx context.Context, id any) error

Delete deletes the row with primary key id. No such row is ErrNotFound.

func (*Repository[T]) Exists

func (r *Repository[T]) Exists(ctx context.Context, clauses ...Clause) (bool, error)

Exists reports whether a row matches the Where clauses.

func (*Repository[T]) Find

func (r *Repository[T]) Find(ctx context.Context, id any) (T, error)

Find returns the row with primary key id, or ErrNotFound (404).

func (*Repository[T]) FindBy

func (r *Repository[T]) FindBy(ctx context.Context, clauses ...Clause) (T, error)

FindBy returns the first row matching the clauses, or ErrNotFound:

u, err := users.FindBy(ctx, sql.Where("email = ?", email))

func (*Repository[T]) List

func (r *Repository[T]) List(ctx context.Context, clauses ...Clause) ([]T, error)

List returns the rows matching the clauses, in their order, never nil:

users, err := repo.List(ctx, sql.Where("active = ?", true), sql.OrderBy("created_at DESC"), sql.Limit(20))

func (*Repository[T]) Page

func (r *Repository[T]) Page(ctx context.Context, page, size int, clauses ...Clause) (Page[T], error)

Page returns page number page (from 1) of size rows matching the clauses, with the total count. A page below 1 is page 1. Limit and Offset clauses are replaced by the page's own.

func (*Repository[T]) Patch

func (r *Repository[T]) Patch(ctx context.Context, id any, set Set) error

Patch writes only the columns in set to the row with primary key id, and sets the updated columns to now unless set names them. Column names are checked against T's tags, so a name from user input cannot inject SQL; an unknown one is ErrUnknownColumn (400). No such row is ErrNotFound.

err := users.Patch(ctx, id, sql.Set{"name": in.Name})

func (*Repository[T]) Table

func (r *Repository[T]) Table() string

Table returns the table name.

func (*Repository[T]) Update

func (r *Repository[T]) Update(ctx context.Context, v *T) error

Update writes every column of v except the primary key, auto and created columns, to the row with v's primary key, and sets its updated columns to now. No such row is ErrNotFound (404).

type Result

type Result = stdsql.Result

Result is the result of DB.Exec.

type Rows

type Rows = stdsql.Rows

Rows is the result of DB.Query. Close it when done.

type Set

type Set map[string]any

Set lists the columns Repository.Patch writes, by column name.

type TxOptions

type TxOptions struct {
	// Isolation is the isolation level; LevelDefault uses the database's.
	Isolation IsolationLevel
	// ReadOnly starts a read only transaction.
	ReadOnly bool
	// Retries reruns the whole function up to this many times when the
	// transaction fails with [ErrRetryable] (a serialization failure or a
	// deadlock). fn must be safe to run again.
	Retries int
}

TxOptions configure DB.TxWith.

Directories

Path Synopsis
internal
suite
Package suite holds the integration tests every dialect must pass against a real database, so MySQL and Postgres behave the same.
Package suite holds the integration tests every dialect must pass against a real database, so MySQL and Postgres behave the same.
Package migrate runs SQL migrations for the Dygo sql module, on Postgres and MySQL, with no other dependency.
Package migrate runs SQL migrations for the Dygo sql module, on Postgres and MySQL, with no other dependency.
Package mysql is the MySQL dialect for the Dygo sql module, on go-sql-driver/mysql:
Package mysql is the MySQL dialect for the Dygo sql module, on go-sql-driver/mysql:
Package postgres is the Postgres dialect for the Dygo sql module, on the pgx driver:
Package postgres is the Postgres dialect for the Dygo sql module, on the pgx driver:

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL