High-performance, production-ready entgo.io/ent dialect.Driver implementation natively backed by jackc/pgx/v5.
entpgx completely eliminates database/sql overhead. Connection pooling, query execution, row scanning, and transaction management stream directly through pgxpool.Pool and pgx.Tx using PostgreSQL's native wire protocol.
- Zero
database/sqlOverhead: Bypasses the Go standard library SQL wrapper. Executes queries and scans rows directly viapgx/v5. - Full
pgxpool.PoolControl: Customize TLS settings, tracers/logging, pool sizes (MinConns/MaxConns), health checks, and custompgtypeOID codecs. - Leak-Safe Session Variables / Row-Level Security (RLS): Safely pass dynamic session context (e.g.
app.tenant_id) down to PostgreSQL queries. Automatically reset on pool release and scoped viaSET LOCALduring transactions. - Native Transaction Features: Supports nested transactions via PostgreSQL
SAVEPOINTs and provides an escape hatch (tx.Raw()) to execute nativepgxoperations (such asCopyFrom,LISTEN/NOTIFY, and Large Objects) within ent transactions. - Seamless Ent Parity: Full support for
entCRUD operations, error classification (ent.IsNotFound,ent.IsConstraintError), hooks, privacy rules, and third-party extensions likeariga/entcache.
go get github.com/incroy/entpgxpackage main
import (
"context"
"log"
"github.com/incroy/entpgx"
"yourproject/ent" // generated ent client
)
func main() {
ctx := context.Background()
// Open driver with PostgreSQL connection string
drv, err := entpgx.Open(ctx, "postgres://postgres:pass@localhost:5432/dbname?sslmode=disable")
if err != nil {
log.Fatalf("failed opening connection: %v", err)
}
defer drv.Close()
// Wrap as ent client
client := ent.NewClient(ent.Driver(drv))
defer client.Close()
// Use client normally with ent
users, err := client.User.Query().All(ctx)
if err != nil {
log.Fatalf("failed querying users: %v", err)
}
log.Printf("found %d users", len(users))
}If you need advanced pool features (e.g., OpenTelemetry tracing, custom connection limits, TLS, custom type codecs), parse the config manually and pass the pool to entpgx.NewDriver:
package main
import (
"context"
"log"
"github.com/incroy/entpgx"
"github.com/jackc/pgx/v5/pgxpool"
"yourproject/ent"
)
func main() {
ctx := context.Background()
cfg, err := pgxpool.ParseConfig("postgres://postgres:pass@localhost:5432/dbname")
if err != nil {
log.Fatalf("failed to parse config: %v", err)
}
// Customize pgxpool configuration
cfg.MinConns = 5
cfg.MaxConns = 50
pool, err := pgxpool.NewWithConfig(ctx, cfg)
if err != nil {
log.Fatalf("failed to create pool: %v", err)
}
defer pool.Close()
drv := entpgx.NewDriver(pool)
client := ent.NewClient(ent.Driver(drv))
defer client.Close()
}Attach custom session variables (GUCs) to the context using entpgx.WithVar or entpgx.WithIntVar. These can be read by PostgreSQL RLS policies via current_setting('app.tenant_id'):
// Attach tenant ID to context
ctx = entpgx.WithVar(ctx, "app.tenant_id", "tenant_12345")
// Every query executed with this context will automatically set "app.tenant_id"
users, err := client.User.Query().All(ctx)Note
- Pool Queries: Automatically pins a connection for the query execution, runs
set_config($1, $2, false), and issues aRESETbefore releasing the connection back to the pool. - Transactions: Uses PostgreSQL
SET LOCAL(set_config($1, $2, true)). PostgreSQL automatically revertsSET LOCALatCOMMITorROLLBACK, eliminating cross-request variable leaks.
Access the underlying pgx.Tx inside an ent transaction to perform high-performance bulk operations (CopyFrom), LISTEN/NOTIFY, or Large Object operations within the exact same transaction boundaries:
err := entpgx.RunInTx(ctx, drv, func(ctx context.Context, tx *entpgx.Tx) error {
// 1. Perform ent CRUD operations
user, err := client.User.Create().SetName("Alice").Save(ctx)
if err != nil {
return err
}
// 2. Access raw pgx.Tx for bulk CopyFrom insertion
rawTx := tx.Raw()
_, err = rawTx.CopyFrom(
ctx,
pgx.Identifier{"audit_logs"},
[]string{"user_id", "action"},
pgx.CopyFromRows([][]any{
{user.ID, "user_created"},
}),
)
return err
})If your code initiates a transaction using client.Tx(ctx), you can capture the underlying *entpgx.Tx handle using WithTxCapture:
box := &entpgx.TxBox{}
ctx = entpgx.WithTxCapture(ctx, box)
txClient, err := client.Tx(ctx)
if err != nil {
return err
}
// box.Tx now holds the *entpgx.Tx handle
rawPgxTx := box.Tx.Raw()Ent's built-in auto-migration tool (client.Schema.Create) relies on an internal unchecked type assertion in entgo.io/ent/dialect/sql/schema/atlas.go that expects a concrete *database/sql.Rows. Because entpgx does not construct database/sql.Rows, running client.Schema.Create will cause a panic.
Recommended Solutions:
- Migration CLI (Recommended for Production): Run schema migrations using standard migration tools such as the Atlas CLI, golang-migrate, or goose against your database DSN.
- Migration-Only Client: If you prefer using
client.Schema.Create(), initialize a secondary migration client backed bydatabase/sqlviapgx/v5/stdlib:import ( "entgo.io/ent/dialect/sql" _ "github.com/jackc/pgx/v5/stdlib" ) // Open migration-only client db, _ := sql.Open("pgx", dsn) migrationClient := ent.NewClient(ent.Driver(db)) _ = migrationClient.Schema.Create(ctx)
*sql.ColumnType does not expose an exported constructor outside Go's database/sql package. As a result, ColumnTypes() returns an unsupported error on entpgx.
This does not affect standard ent CRUD operations or entity queries. Standard generated ent entities and GroupBy(...).Aggregate(...) queries function normally.
go test -v ./...Set ENTPGX_TEST_DSN (or POSTGRES_DSN / DATABASE_URL) to run full integration tests against a PostgreSQL instance:
ENTPGX_TEST_DSN="postgres://postgres:postgres@localhost:5432/testdb?sslmode=disable" go test -v ./...entpgx is licensed under the Apache License 2.0.