Version: 1.0
License: MIT
Go Version: 1.18+
- Overview
- Installation
- Quick Start
- Configuration
- Core Concepts
- DataStore Operations
- Transactions
- Batch Operations
- Output Formats
- Security Best Practices
- Advanced Usage
- Troubleshooting
- API Reference
goquery is a lightweight Go library that simplifies database operations through a fluent, type-safe API. It provides a unified interface for multiple database backends while maintaining performance and safety.
- ✅ Fluent API - Chainable, readable query building
- ✅ Multi-Database Support - PostgreSQL (pgx), SQLite, Oracle, DuckDB
- ✅ Type-Safe Mapping - Automatic struct-to-row mapping via tags
- ✅ Transaction Support - Automatic rollback on panic, commit on success
- ✅ Batch Operations - High-performance bulk inserts (pgx)
- ✅ Multiple Output Formats - Structs, JSON, CSV
- ✅ Connection Pooling - Configurable pool settings
- ✅ SQL Generation - Auto-generate INSERT/SELECT from structs
- ✅ Security First - Parameterized queries prevent SQL injection
┌─────────────────┐
│ Your Code │
└────────┬────────┘
│
▼
┌─────────────────┐
│ DataStore │ ← Unified interface
└────────┬────────┘
│
┌────┴────┐
▼ ▼
┌───────┐ ┌───────┐
│ PgxDb│ │SqlxDb │ ← Driver implementations
└───┬───┘ └───┬───┘
│ │
▼ ▼
┌───────┐ ┌───────┐
│ pgx │ │ sqlx │ ← Underlying libraries
└───────┘ └───────┘
go get github.com/usace/goquerygoquery uses these excellent libraries:
- pgx (v4) - High-performance PostgreSQL driver
- sqlx - Extensions to database/sql
- scany - Struct scanning for SQL rows
- go-strcase - String case conversion for JSON
package main
import (
"log"
"github.com/usace/goquery"
)
func main() {
// Create configuration
config := goquery.RdbmsConfig{
Dbuser: "postgres",
Dbpass: "password",
Dbhost: "localhost",
Dbport: "5432",
Dbname: "mydb",
DbDriver: "pgx",
DbStore: "pgx",
}
// Connect to database
store, err := goquery.NewRdbmsDataStore(&config)
if err != nil {
log.Fatal(err)
}
// Use the store...
}type User struct {
ID int `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
}
// Query into struct slice
var users []User
err := store.Select("SELECT id, name, email FROM users").
Dest(&users).
Fetch()
if err != nil {
log.Fatal(err)
}
for _, user := range users {
fmt.Printf("%s (%s)\n", user.Name, user.Email)
}var user User
err := store.Select("SELECT id, name, email FROM users WHERE id = $1").
Params(42).
Dest(&user).
Fetch()type RdbmsConfig struct {
// Connection Settings
Dbuser string // Database username
Dbpass string // Database password
Dbhost string // Database host (e.g., "localhost")
Dbport string // Database port (e.g., "5432")
Dbname string // Database name
DbDriver string // Driver: "pgx", "sqlite", "godror"
DbStore string // Store type: "pgx" or "sqlx"
// Advanced Settings
ExternalLib string // Path to external libs (Oracle)
OnInit string // Initialization SQL (Oracle)
DbDriverSettings string // Additional driver parameters
// Connection Pool Settings
PoolMaxConns int // Maximum pool connections
PoolMinConns int // Minimum pool connections
PoolMaxConnLifetime string // Max connection lifetime (e.g., "1h")
PoolMaxConnIdle string // Max connection idle time (e.g., "30m")
}config := goquery.RdbmsConfigFromEnv()
store, err := goquery.NewRdbmsDataStore(config)Supported environment variables:
| Variable | Description | Default |
|---|---|---|
DBUSER |
Database username | (none) |
DBPASS |
Database password | (none) |
DBHOST |
Database host | (none) |
DBPORT |
Database port | 5432 |
DBNAME |
Database name | (none) |
DBDRIVER |
Database driver | (none) |
DBSTORE |
Store type (pgx/sqlx) | (none) |
DBDRIVER_PARAMS |
Additional parameters | (none) |
POOLMAXCONNS |
Max connections | (driver default) |
POOLMINCONNS |
Min connections | (driver default) |
POOLMAXCONNLIFETIME |
Max lifetime | (none) |
POOLMAXCONNIDLE |
Max idle time | (none) |
The DbDriverSettings field allows you to pass additional parameters to the database driver:
config := goquery.RdbmsConfig{
// ... other settings ...
DbDriverSettings: "sslmode=verify-full sslrootcert=/path/to/ca.crt",
}Available SSL modes:
disable- No SSL (insecure, development only)require- SSL required, no certificate verification (default)verify-ca- Verify server certificate against CAverify-full- Verify certificate and hostname (recommended for production)
config.DbDriverSettings = "sslmode=require application_name=myapp connect_timeout=10"Common parameters:
application_name- Application name in logsconnect_timeout- Connection timeout in secondsstatement_timeout- Query timeout in millisecondssearch_path- Default schema search path
config := goquery.RdbmsConfig{
DbDriver: "godror",
DbStore: "sqlx",
ExternalLib: "/usr/lib/oracle/instantclient",
OnInit: "ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'",
DbDriverSettings: "poolMinSessions=4 poolMaxSessions=100",
}config := goquery.RdbmsConfig{
Dbname: "/path/to/database.db",
DbDriver: "sqlite",
DbStore: "sqlx",
DbDriverSettings: "_journal_mode=WAL&_timeout=5000",
}Duration strings use Go's time.ParseDuration format: "300ms", "1.5h", "2h45m".
Example configurations:
// High-traffic web application
config := goquery.RdbmsConfig{
PoolMaxConns: 100,
PoolMinConns: 10,
PoolMaxConnLifetime: "1h",
PoolMaxConnIdle: "10m",
}
// Background worker
config := goquery.RdbmsConfig{
PoolMaxConns: 10,
PoolMinConns: 2,
PoolMaxConnLifetime: "30m",
PoolMaxConnIdle: "5m",
}
// Development
config := goquery.RdbmsConfig{
PoolMaxConns: 5,
PoolMinConns: 1,
PoolMaxConnLifetime: "5m",
PoolMaxConnIdle: "1m",
}The DataStore is your main entry point. All database operations go through it.
type DataStore interface {
// Query Operations
Select(stmt ...string) *FluentSelect
FetchRows(tx *Tx, input QueryInput) (Rows, error)
// Insert Operations
Insert(ds DataSet) *FluentInsert
InsertRecs(tx *Tx, input InsertInput) error
// Execute Operations
Exec(tx *Tx, stmt string, params ...interface{}) error
Execr(tx *Tx, stmt string, params ...interface{}) (ExecResult, error)
MustExec(tx *Tx, stmt string, params ...interface{})
MustExecr(tx *Tx, stmt string, params ...interface{}) ExecResult
// Transaction Operations
NewTransaction() (Tx, error)
Transaction(tf TransactionFunction) error
}DataSets organize your data structures and associated SQL statements:
// 1. Define your struct with db tags
type Product struct {
ID int32 `db:"id" dbid:"SEQUENCE" idsequence:"products_id_seq"`
Name string `db:"name"`
Price float64 `db:"price"`
Description *string `db:"description"` // Nullable field
}
// 2. Create a TableDataSet
var productsDS = goquery.TableDataSet{
Name: "products",
Schema: "public", // Optional schema
Statements: goquery.Statements{
"get-all": "SELECT * FROM products ORDER BY name",
"get-by-id": "SELECT * FROM products WHERE id = $1",
"search": "SELECT * FROM products WHERE name ILIKE $1",
"get-expensive": "SELECT * FROM products WHERE price > $1",
},
TableFields: Product{}, // Used for auto-generating INSERT statements
}
// 3. Use the dataset
var products []Product
err := store.Select().
DataSet(&productsDS).
StatementKey("get-all").
Dest(&products).
Fetch()goquery uses struct tags to map between Go structs and database columns:
type User struct {
ID int32 `db:"id" dbid:"SEQUENCE" idsequence:"users_id_seq"`
Username string `db:"username"`
Email string `db:"email"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt *time.Time `db:"updated_at"` // Nullable
Internal string `db:"-"` // Ignored by goquery
}Tag reference:
| Tag | Description | Example |
|---|---|---|
db:"column_name" |
Maps field to column | db:"user_id" |
db:"-" |
Ignores field | db:"-" |
dbid:"SEQUENCE" |
Auto-increment ID | dbid:"SEQUENCE" |
dbid:"AUTOINCREMENT" |
Auto-increment (SQLite) | dbid:"AUTOINCREMENT" |
idsequence:"seq_name" |
Sequence name (Postgres) | idsequence:"users_id_seq" |
// Query all rows
var users []User
err := store.Select("SELECT * FROM users").
Dest(&users).
Fetch()
// Query single row
var user User
err := store.Select("SELECT * FROM users WHERE id = $1").
Params(123).
Dest(&user).
Fetch()var users []User
err := store.Select().
DataSet(&usersDS).
StatementKey("get-all").
Dest(&users).
Fetch()// Apply() for internal query building (column names, table names)
var user User
err := store.Select("SELECT * FROM %s WHERE %s = $1").
Apply("users", "id").
Params(123).
Dest(&user).
Fetch()
Apply() uses fmt.Sprintf() for SQL templating. NEVER pass user input to Apply(). Use Params() for all user data.
// ✅ CORRECT - Apply() for internal values, Params() for user input
tableName := "users" // Internal constant
userID := getUserInput()
err := store.Select("SELECT * FROM %s WHERE id = $1").
Apply(tableName).
Params(userID).
Dest(&user).
Fetch()
// ❌ WRONG - SQL Injection vulnerability!
userTable := getUserInput()
err := store.Select("SELECT * FROM %s").
Apply(userTable). // NEVER DO THIS!
Dest(&results).
Fetch()// Add WHERE, ORDER BY, LIMIT dynamically
var users []User
err := store.Select("SELECT * FROM users").
Suffix("WHERE active = true ORDER BY created_at DESC LIMIT 10").
Dest(&users).
Fetch()
// With parameters
err := store.Select("SELECT * FROM users").
Suffix("WHERE active = $1 AND role = $2").
Params(true, "admin").
Dest(&users).
Fetch()err := store.Select("SELECT * FROM users WHERE id = $1").
Params(123).
LogSql(true). // Logs the final SQL statement
Dest(&user).
Fetch()rows, err := store.Select("SELECT * FROM users").FetchRows()
if err != nil {
log.Fatal(err)
}
defer rows.Close() // Always close!
for rows.Next() {
var user User
if err := rows.ScanStruct(&user); err != nil {
log.Fatal(err)
}
fmt.Printf("User: %s\n", user.Username)
}// Automatic resource management
var user User
err := store.Select("SELECT * FROM users").
ForEachRow(func(row goquery.Rows) error {
if err := row.ScanStruct(&user); err != nil {
return err
}
fmt.Printf("User: %s\n", user.Username)
return nil
}).
Fetch()user := User{
Username: "john_doe",
Email: "[email protected]",
}
// Auto-generates INSERT statement from struct tags
err := store.Insert(&usersDS).
Records(&user).
Execute()users := []User{
{Username: "alice", Email: "[email protected]"},
{Username: "bob", Email: "[email protected]"},
{Username: "charlie", Email: "[email protected]"},
}
err := store.Insert(&usersDS).
Records(&users).
Execute()Batch inserts are more efficient for large datasets (pgx only):
users := make([]User, 10000)
// ... populate users ...
err := store.Insert(&usersDS).
Records(&users).
Batch(true).
BatchSize(100). // Send 100 records per batch
Execute()Batch vs Bulk:
- Bulk Insert - Uses multiple transactions, one per record
- Batch Insert - Uses pgx batching, single network roundtrip per batch (much faster)
// Get generated ID
var newID int
err := store.Select().
DataSet(&usersDS).
StatementKey("insert-with-return").
Params("john_doe", "[email protected]").
Dest(&newID).
Fetch()
// Statement defined in DataSet:
// "insert-with-return": "INSERT INTO users (username, email) VALUES ($1, $2) RETURNING id"Use Exec() for operations that don't return data:
// Update
err := store.Exec(
goquery.NoTx,
"UPDATE users SET email = $1 WHERE id = $2",
"[email protected]",
123,
)
// Delete
err := store.Exec(
goquery.NoTx,
"DELETE FROM users WHERE id = $1",
123,
)
// Get rows affected
result, err := store.Execr(
goquery.NoTx,
"DELETE FROM users WHERE active = false",
)
if err == nil {
fmt.Printf("Deleted %d rows\n", result.RowsAffected())
}// Delete and get IDs of deleted rows
var deletedIDs []int
err := store.Select(`
DELETE FROM users
WHERE active = false
RETURNING id
`).Dest(&deletedIDs).Fetch()
fmt.Printf("Deleted user IDs: %v\n", deletedIDs)The Transaction() method handles commit/rollback automatically:
err := store.Transaction(func(tx goquery.Tx) {
// All operations within this function are part of the transaction
// Insert a user
store.Insert(&usersDS).
Records(&user).
Tx(&tx).
PanicOnErr(true). // Panic on error to trigger rollback
Execute()
// Update related data
store.MustExec(&tx,
"UPDATE accounts SET balance = balance + $1 WHERE user_id = $2",
100.00, user.ID,
)
// If any operation panics, transaction is automatically rolled back
// If function completes without panic, transaction is automatically committed
})
if err != nil {
log.Printf("Transaction failed: %v\n", err)
}Transaction behavior:
- ✅ Automatic commit if function completes successfully
- ✅ Automatic rollback if function panics
- ✅ Panic is caught and converted to error return value
- ✅ Errors are logged during rollback/commit failures
For more control:
tx, err := store.NewTransaction()
if err != nil {
log.Fatal(err)
}
// Perform operations
err = store.Exec(&tx, "UPDATE users SET active = false WHERE id = $1", 123)
if err != nil {
tx.Rollback()
log.Fatal(err)
}
// Commit
err = tx.Commit()
if err != nil {
log.Fatal(err)
}Use PanicOnErr() or MustExec() to automatically rollback on error:
err := store.Transaction(func(tx goquery.Tx) {
// Option 1: PanicOnErr with fluent API
store.Insert(&usersDS).
Records(&user).
Tx(&tx).
PanicOnErr(true).
Execute()
// Option 2: MustExec (panics on error)
store.MustExec(&tx, "UPDATE accounts SET balance = $1 WHERE user_id = $2", 0, user.ID)
// Option 3: Manual panic
result, err := store.Execr(&tx, "DELETE FROM temp_data")
if err != nil {
panic(err) // Triggers rollback
}
})Batch operations dramatically improve performance for bulk inserts by reducing network round trips.
- ✅ Inserting 100+ records
- ✅ High-throughput data pipelines
- ✅ ETL operations
- ✅ Importing large datasets
// Generate test data
users := make([]User, 10000)
for i := range users {
users[i] = User{
Username: fmt.Sprintf("user_%d", i),
Email: fmt.Sprintf("user_%[email protected]", i),
}
}
// Batch insert with error handling
err := store.Insert(&usersDS).
Records(&users).
Batch(true).
BatchSize(500). // 500 records per batch
Execute()
if err != nil {
log.Fatalf("Batch insert failed: %v", err)
}Choose batch size based on your data and network:
| Batch Size | Use Case | Notes |
|---|---|---|
| 50-100 | Small records, slow network | Reduces packet size |
| 100-500 | Typical web applications | Good default |
| 500-1000 | Large records, fast network | Maximum throughput |
| 1000+ | Very large datasets, LAN | Diminishing returns |
Example with 10,000 records:
| Method | Time | Network Trips |
|---|---|---|
| Individual inserts | ~45s | 10,000 |
| Bulk insert (multi-tx) | ~25s | 10,000 |
| Batch insert (size 100) | ~2s | 100 |
| Batch insert (size 500) | ~1s | 20 |
Batch operations validate each statement in the batch:
err := store.Insert(&usersDS).
Records(&users).
Batch(true).
BatchSize(100).
Execute()
if err != nil {
// Error message includes which record failed
// Example: "batch insert failed at record 342: duplicate key value"
log.Printf("Batch failed: %v\n", err)
}Important: If any record in a batch fails, the entire batch is rolled back (not the entire operation).
var buf bytes.Buffer
writer := bufio.NewWriter(&buf)
err := store.Select("SELECT * FROM users").
OutputJson(writer).
Fetch()
if err != nil {
log.Fatal(err)
}
writer.Flush()
jsonBytes := buf.Bytes()err := store.Select("SELECT * FROM users").
OutputJson(writer).
IsJsonArray(true). // Wrap results in []
Fetch()
// Output: [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]err := store.Select("SELECT * FROM users WHERE id = $1").
Params(1).
OutputJson(writer).
IsJsonArray(false). // No array wrapper
Fetch()
// Output: {"id":1,"name":"Alice"}err := store.Select("SELECT * FROM users").
OutputJson(writer).
CamelCase(true). // Convert column names to camelCase
OmitNull(true). // Omit null fields
DateFormat("2006-01-02"). // Custom date format
Fetch()
// Output: {"userId":1,"userName":"Alice","createdAt":"2024-01-15"}// Deprecated but still available
jsonBytes, err := store.Select("SELECT * FROM users LIMIT 10").
FetchJSON()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(jsonBytes))csv, err := store.Select("SELECT id, name, email FROM users").
FetchCSV()
if err != nil {
log.Fatal(err)
}
fmt.Println(csv)
// Output:
// "id","name","email"
// 1,"Alice","[email protected]"
// 2,"Bob","[email protected]"CSV Options:
csv, err := store.Select("SELECT * FROM users").
CamelCase(true). // Column headers in camelCase
DateFormat("2006-01-02"). // Date formatting
FetchCSV()func usersHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
err := store.Select("SELECT * FROM users").
OutputJson(w). // Write directly to response
IsJsonArray(true).
CamelCase(true).
Fetch()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}✅ CORRECT:
userID := getUserInput()
err := store.Select("SELECT * FROM users WHERE id = $1").
Params(userID). // Safe - uses parameter binding
Dest(&user).
Fetch()❌ WRONG:
userID := getUserInput()
sql := fmt.Sprintf("SELECT * FROM users WHERE id = %s", userID) // SQL INJECTION!
err := store.Select(sql).Dest(&user).Fetch()✅ CORRECT:
// Internal constants only
tableName := "users"
columnName := "id"
userID := getUserInput()
err := store.Select("SELECT * FROM %s WHERE %s = $1").
Apply(tableName, columnName). // Internal values
Params(userID). // User input
Dest(&user).
Fetch()❌ WRONG:
// NEVER pass user input to Apply()
tableName := getUserInput() // User could input: "users; DROP TABLE users--"
err := store.Select("SELECT * FROM %s").
Apply(tableName). // SQL INJECTION!
Dest(&results).
Fetch()Development:
config := goquery.RdbmsConfig{
// ... other settings ...
DbDriverSettings: "sslmode=disable", // OK for local development
}Production (Recommended):
config := goquery.RdbmsConfig{
// ... other settings ...
DbDriverSettings: "sslmode=verify-full sslrootcert=/path/to/ca-cert.pem",
}Available SSL modes (PostgreSQL):
| Mode | Encryption | Certificate Check | Use Case |
|---|---|---|---|
disable |
❌ No | ❌ No | Local dev only |
require |
✅ Yes | ❌ No | Default, basic security |
verify-ca |
✅ Yes | ✅ CA only | Verify server identity |
verify-full |
✅ Yes | ✅ CA + hostname | Production (recommended) |
❌ DON'T hardcode credentials:
config := goquery.RdbmsConfig{
Dbuser: "admin",
Dbpass: "password123", // NEVER hardcode passwords!
}✅ DO use environment variables:
config := goquery.RdbmsConfigFromEnv()
// Or:
config := goquery.RdbmsConfig{
Dbuser: os.Getenv("DB_USER"),
Dbpass: os.Getenv("DB_PASS"),
// ...
}✅ BETTER - Use secret managers:
// Example with AWS Secrets Manager
secret := getSecretFromAWS("prod/db/credentials")
config := goquery.RdbmsConfig{
Dbuser: secret["username"],
Dbpass: secret["password"],
// ...
}Connection strings contain passwords in plaintext. Never log them:
// ❌ WRONG
log.Printf("Connecting with config: %+v", config) // Logs password!
// ✅ CORRECT
log.Printf("Connecting to %s@%s:%s/%s", config.Dbuser, config.Dbhost, config.Dbport, config.Dbname)Always validate user input before using it in queries:
func getUser(idStr string) (*User, error) {
// Validate input
id, err := strconv.Atoi(idStr)
if err != nil {
return nil, fmt.Errorf("invalid user ID: %w", err)
}
if id <= 0 {
return nil, errors.New("user ID must be positive")
}
// Safe to use in query
var user User
err = store.Select("SELECT * FROM users WHERE id = $1").
Params(id).
Dest(&user).
Fetch()
return &user, err
}Use database users with minimal required permissions:
-- Create application user with limited permissions
CREATE USER app_user WITH PASSWORD 'secure_password';
-- Grant only what's needed
GRANT SELECT, INSERT, UPDATE ON users TO app_user;
GRANT SELECT ON products TO app_user;
-- Don't grant DELETE or DROP permissions unless necessaryrows, err := store.Select("SELECT id, name, email FROM users").FetchRows()
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int
var name, email string
err := rows.Scan(&id, &name, &email)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%d: %s <%s>\n", id, name, email)
}rows, err := store.Select("SELECT * FROM users").FetchRows()
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// Get column names
columns, err := rows.Columns()
if err != nil {
log.Fatal(err)
}
fmt.Println("Columns:", columns)
// Get column types
types, err := rows.ColumnTypes()
if err != nil {
log.Fatal(err)
}
for i, col := range columns {
fmt.Printf("Column %s has type %v\n", col, types[i])
}Use pointer types for nullable columns:
type User struct {
ID int32 `db:"id"`
Name string `db:"name"`
Email *string `db:"email"` // Nullable
PhoneNumber *string `db:"phone_number"` // Nullable
LastLogin *time.Time `db:"last_login"` // Nullable
}
// Inserting with NULL values
email := "[email protected]"
user := User{
Name: "John Doe",
Email: &email, // Has value
PhoneNumber: nil, // NULL
LastLogin: nil, // NULL
}
err := store.Insert(&usersDS).Records(&user).Execute()// Connect to multiple databases
pgConfig := goquery.RdbmsConfig{
Dbhost: "postgres-host",
DbDriver: "pgx",
DbStore: "pgx",
// ...
}
pgStore, err := goquery.NewRdbmsDataStore(&pgConfig)
sqliteConfig := goquery.RdbmsConfig{
Dbname: "/path/to/local.db",
DbDriver: "sqlite",
DbStore: "sqlx",
}
sqliteStore, err := goquery.NewRdbmsDataStore(&sqliteConfig)
// Use them independently
var pgUsers []User
pgStore.Select("SELECT * FROM users").Dest(&pgUsers).Fetch()
var sqliteCache []CacheEntry
sqliteStore.Select("SELECT * FROM cache").Dest(&sqliteCache).Fetch()// TableDataSet with schema
var productsDS = goquery.TableDataSet{
Name: "products",
Schema: "sales", // Queries will use "sales.products"
Statements: goquery.Statements{
"get-all": "SELECT * FROM sales.products",
},
TableFields: Product{},
}// See what SQL will be generated
stmt, err := store.(*goquery.RdbmsDataStore).RdbmsDb().InsertStmt(&usersDS)
if err != nil {
log.Fatal(err)
}
fmt.Println("Generated INSERT:", stmt)
// Output: INSERT INTO users (id, name, email) VALUES (nextval('users_id_seq'), $1, $2)var user User
err := store.Select("SELECT * FROM users WHERE id = $1").
Params(999).
Dest(&user).
Fetch()
// err will be "no rows in result set" if no user with id=999Solution: Check if record exists or handle the error:
if err != nil {
if strings.Contains(err.Error(), "no rows") {
return nil, fmt.Errorf("user not found")
}
return nil, err
}Unable to connect to pgx datastore: connection refused
Checklist:
- ✅ Is the database running? (
pg_ctl statusorsystemctl status postgresql) - ✅ Is the host correct? (use
localhostor127.0.0.1for local) - ✅ Is the port correct? (default PostgreSQL is
5432) - ✅ Firewall blocking the connection?
- ✅ Check
pg_hba.conffor PostgreSQL access rules
Unsupported store type: pgxx
Solution: Check DbStore value - must be exactly "pgx" or "sqlx":
config := goquery.RdbmsConfig{
DbStore: "pgx", // Not "pgxx" or "PGX"
}Solution: Ensure all required fields are set:
config := goquery.RdbmsConfig{
Dbuser: "postgres",
Dbpass: "password",
Dbhost: "localhost",
Dbport: "5432",
Dbname: "mydb", // Required!
DbDriver: "pgx",
DbStore: "pgx",
}FATAL: sorry, too many clients already
Solutions:
- Reduce pool size:
config.PoolMaxConns = 10 // Lower value- Increase database max connections (PostgreSQL):
ALTER SYSTEM SET max_connections = 200;
SELECT pg_reload_conf();- Ensure connections are closed:
rows, err := store.Select("...").FetchRows()
defer rows.Close() // Always close!sql: Scan error: converting NULL to string is unsupported
Solution: Use pointer types for nullable columns:
type User struct {
Name string `db:"name"` // NOT NULL column
Email *string `db:"email"` // Nullable column
}Enable SQL logging to see what queries are executed:
err := store.Select("SELECT * FROM users WHERE active = $1").
Params(true).
LogSql(true). // Prints SQL to console
Dest(&users).
Fetch()import (
"time"
)
start := time.Now()
err := store.Select("SELECT * FROM large_table").Dest(&results).Fetch()
duration := time.Since(start)
log.Printf("Query took %v", duration)| Method | Description | Example |
|---|---|---|
DataSet(ds DataSet) |
Use a DataSet | .DataSet(&usersDS) |
StatementKey(key string) |
Use named statement from DataSet | .StatementKey("get-all") |
Params(params ...interface{}) |
Bind parameters | .Params(123, "john") |
Apply(vals ...interface{}) |
Apply formatting (internal only) | .Apply("users", "id") |
Suffix(suffix string) |
Append to query | .Suffix("LIMIT 10") |
Dest(dest interface{}) |
Set destination | .Dest(&users) |
Tx(tx *Tx) |
Use transaction | .Tx(&tx) |
LogSql(log bool) |
Log SQL statement | .LogSql(true) |
PanicOnErr(panic bool) |
Panic on error | .PanicOnErr(true) |
OutputJson(w io.Writer) |
Output as JSON | .OutputJson(writer) |
OutputCsv(w io.Writer) |
Output as CSV | .OutputCsv(writer) |
IsJsonArray(array bool) |
Wrap JSON in array | .IsJsonArray(true) |
CamelCase(camel bool) |
Convert to camelCase | .CamelCase(true) |
OmitNull(omit bool) |
Omit null fields | .OmitNull(true) |
DateFormat(format string) |
Custom date format | .DateFormat("2006-01-02") |
ForEachRow(fn RowFunction) |
Iterate rows | .ForEachRow(func(r Rows) error {...}) |
Fetch() |
Execute query | .Fetch() |
FetchRows() |
Get row cursor | .FetchRows() |
| Method | Description | Example |
|---|---|---|
Records(recs interface{}) |
Set records to insert | .Records(&user) or .Records(&users) |
Tx(tx *Tx) |
Use transaction | .Tx(&tx) |
Batch(batch bool) |
Use batch mode | .Batch(true) |
BatchSize(size int) |
Set batch size | .BatchSize(500) |
PanicOnErr(panic bool) |
Panic on error | .PanicOnErr(true) |
Execute() |
Execute insert | .Execute() |
| Method | Description |
|---|---|
Commit() |
Commit transaction |
Rollback() |
Rollback transaction |
PgxTx() |
Get underlying pgx transaction |
SqlXTx() |
Get underlying sqlx transaction |
SqlTx() |
Get underlying sql.Tx |
| Method | Description |
|---|---|
Next() |
Advance to next row |
Scan(...interface{}) |
Scan into variables |
ScanStruct(interface{}) |
Scan into struct |
Columns() |
Get column names |
ColumnTypes() |
Get column types |
Close() |
Close cursor |
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"github.com/usace/goquery"
"github.com/gorilla/mux"
)
type User struct {
ID int32 `db:"id" dbid:"SEQUENCE" idsequence:"users_id_seq"`
Username string `db:"username"`
Email string `db:"email"`
Active bool `db:"active"`
}
var usersDS = goquery.TableDataSet{
Name: "users",
Statements: goquery.Statements{
"get-all": "SELECT * FROM users ORDER BY username",
"get-by-id": "SELECT * FROM users WHERE id = $1",
"get-active": "SELECT * FROM users WHERE active = true",
},
TableFields: User{},
}
var store goquery.DataStore
func main() {
// Initialize database
config := goquery.RdbmsConfigFromEnv()
var err error
store, err = goquery.NewRdbmsDataStore(config)
if err != nil {
log.Fatal(err)
}
// Setup routes
r := mux.NewRouter()
r.HandleFunc("/users", listUsers).Methods("GET")
r.HandleFunc("/users/{id}", getUser).Methods("GET")
r.HandleFunc("/users", createUser).Methods("POST")
r.HandleFunc("/users/{id}", updateUser).Methods("PUT")
r.HandleFunc("/users/{id}", deleteUser).Methods("DELETE")
// Start server
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", r))
}
func listUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
err := store.Select().
DataSet(&usersDS).
StatementKey("get-all").
OutputJson(w).
IsJsonArray(true).
CamelCase(true).
Fetch()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func getUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
err = store.Select().
DataSet(&usersDS).
StatementKey("get-by-id").
Params(id).
OutputJson(w).
CamelCase(true).
Fetch()
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
}
}
func createUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err := store.Insert(&usersDS).Records(&user).Execute()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func updateUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
user.ID = int32(id)
err = store.Exec(
goquery.NoTx,
"UPDATE users SET username = $1, email = $2, active = $3 WHERE id = $4",
user.Username, user.Email, user.Active, user.ID,
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
func deleteUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
err = store.Exec(
goquery.NoTx,
"DELETE FROM users WHERE id = $1",
id,
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}package main
import (
"encoding/csv"
"log"
"os"
"strconv"
"github.com/usace/goquery"
)
type Record struct {
ID int32 `db:"id" dbid:"AUTOINCREMENT"`
Name string `db:"name"`
Value float64 `db:"value"`
}
var recordsDS = goquery.TableDataSet{
Name: "records",
TableFields: Record{},
}
func main() {
// Connect to database
config := goquery.RdbmsConfigFromEnv()
store, err := goquery.NewRdbmsDataStore(config)
if err != nil {
log.Fatal(err)
}
// Read CSV file
file, err := os.Open("data.csv")
if err != nil {
log.Fatal(err)
}
defer file.Close()
reader := csv.NewReader(file)
rows, err := reader.ReadAll()
if err != nil {
log.Fatal(err)
}
// Parse records
records := make([]Record, 0, len(rows)-1)
for i, row := range rows {
if i == 0 {
continue // Skip header
}
value, err := strconv.ParseFloat(row[2], 64)
if err != nil {
log.Printf("Skipping row %d: %v", i, err)
continue
}
records = append(records, Record{
Name: row[1],
Value: value,
})
}
log.Printf("Importing %d records...", len(records))
// Batch insert
err = store.Insert(&recordsDS).
Records(&records).
Batch(true).
BatchSize(500).
Execute()
if err != nil {
log.Fatal(err)
}
log.Println("Import complete!")
}// Before (database/sql)
rows, err := db.Query("SELECT id, name FROM users WHERE active = $1", true)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
var users []User
for rows.Next() {
var user User
err := rows.Scan(&user.ID, &user.Name)
if err != nil {
log.Fatal(err)
}
users = append(users, user)
}
// After (goquery)
var users []User
err := store.Select("SELECT id, name FROM users WHERE active = $1").
Params(true).
Dest(&users).
Fetch()// Before (GORM)
var users []User
db.Where("active = ?", true).Find(&users)
// After (goquery)
var users []User
store.Select("SELECT * FROM users WHERE active = $1").
Params(true).
Dest(&users).
Fetch()We welcome contributions! Please:
- Fork the repository
- Create a feature branch
- Write tests for new functionality
- Ensure all tests pass
- Submit a pull request
MIT License - see LICENSE file for details
- Issues: https://github.com/usace/goquery/issues
- Documentation: https://github.com/usace/goquery
- Email: [email protected]
Version: 1.0
Last Updated: 2024
Maintained by: U.S. Army Corps of Engineers