Phastos database layer provides a SQL query builder with read/write separation, connection pooling, soft delete, transactions, prepared statement caching, and automatic query construction via reflection-based struct mapping. Built on jmoiron/sqlx.
database.Connect() initialises both master (read-write) and follower (read-only) connections using environment variables:
| Variable | Description | Default |
|---|---|---|
DATABASE_ENGINE |
mysql, nrmysql, postgres, nrpostgres |
— (required) |
DATABASE_CONN_STRING_MASTER |
DSN for master/write DB | — (required) |
DATABASE_CONN_STRING_FOLLOWER |
DSN for follower/read DB | — (required) |
DATABASE_MAX_OPEN_CONN |
Max open connections per pool | 10 |
DATABASE_MAX_IDLE_CONN |
Max idle connections per pool | 2 |
DATABASE_CONN_MAX_LIFETIME |
Max seconds before a connection is recycled | 300 (5 min) |
DATABASE_CONN_MAX_IDLE_TIME |
Max seconds a connection stays idle | 45 |
NR-prefixed engines (nrmysql, nrpostgres) use New Relic instrumented drivers for automatic query tracing.
import "github.com/kodekoding/phastos/v2/go/database"
db, err := database.Connect()
if err != nil {
log.Fatal(err)
}type ISQL interface {
Master
Follower
GetTransaction() Transactions
Read(ctx context.Context, opts *QueryOpts, additionalParams ...interface{}) error
Write(ctx context.Context, opts *QueryOpts, isSoftDelete ...bool) (*CUDResponse, error)
CachedRebind(query string) string
}type QueryOpts struct {
BaseQuery string
Conditions func(ctx context.Context)
Result interface{}
IsList bool
SelectRequest *TableRequest
CUDRequest *CUDConstructData
Trx *sqlx.Tx
LockingType string
UseMaster bool
// ... internal fields
}type TableRequest struct {
Keyword string // search keyword
SearchColsStr string // comma-separated column names for keyword search
Page int // page number (1-based)
Limit int // page size
OrderBy string // ORDER BY clause
GroupBy string // GROUP BY clause
CreatedStart string // date range start (YYYY-MM-DD)
CreatedEnd string // date range end (YYYY-MM-DD)
CustomDateColFilter string // custom date column name (default: created_at)
InitiateWhere []string // raw WHERE conditions
InitiateWhereValues []interface{} // values for InitiateWhere placeholders
IncludeDeleted bool // include soft-deleted rows
MainTableAlias string // alias prefix for column qualifiers
IsDeleted string // "1" to filter only deleted rows
}type CUDConstructData struct {
Cols []string
Values []interface{}
ColsInsert string
BulkValues string
BulkQuery string
Action string
TableName string
}type CUDResponse struct {
Status bool `json:"status"`
RowsAffected int64 `json:"rows_affected"`
LastInsertID int64 `json:"last_insert_id"`
Message string `json:"message,omitempty"`
}type SelectResponse struct {
Data interface{} `json:"data"`
*ResponseMetaData
}
type ResponseMetaData struct {
RequestParam interface{} `json:"request_param"`
TotalData int64 `json:"total_data"`
TotalFiltered int64 `json:"total_filtered"`
}db.Read(ctx, &database.QueryOpts{...}) returns results into Result by reference. The method:
- Calls
Conditions(ctx)if set (for dynamic query parts before execution) - Generates add-on clauses (WHERE, GROUP BY, ORDER BY, LIMIT) from
SelectRequest - Rebind-s
?placeholders to the engine-appropriate format - Executes via prepared statement cache (LRU, max 500 entries) or falls back to direct DB
type User struct {
Id int64 `json:"id" db:"id"`
Name string `json:"name" db:"name"`
}
var user User
err := db.Read(ctx, &database.QueryOpts{
BaseQuery: "SELECT id, name FROM users",
SelectRequest: &database.TableRequest{
InitiateWhere: []string{"id = ?"},
InitiateWhereValues: []interface{}{42},
},
Result: &user,
})
// user.Id == 42, user.Name == "Alice"var users []User
err := db.Read(ctx, &database.QueryOpts{
BaseQuery: "SELECT id, name FROM users",
SelectRequest: &database.TableRequest{
Page: 1,
Limit: 20,
OrderBy: "created_at DESC",
},
Result: &users,
IsList: true,
})
// LIMIT/OFFSET generated automatically (engine-aware: postgres uses LIMIT ? OFFSET ?, mysql uses LIMIT ?,?)var users []User
req := &database.TableRequest{
Keyword: "alice",
SearchColsStr: "name,email",
Page: 1,
Limit: 10,
}
err := db.Read(ctx, &database.QueryOpts{
BaseQuery: "SELECT id, name, email FROM users",
SelectRequest: req,
Result: &users,
IsList: true,
})
// Generates: WHERE (name LIKE ? OR email LIKE ?) with LIKE params as %alice%req := &database.TableRequest{
CreatedStart: "2025-01-01",
CreatedEnd: "2025-12-31",
Page: 1,
Limit: 10,
}
// Generates: WHERE created_at >= ? AND created_at <= ?
// MySQL uses STR_TO_DATE / DATE_FORMAT for comparisonreq := &database.TableRequest{}
req.SetWhereCondition("status = ?", "active")
req.SetWhereCondition("deleted_at IS NULL")
// Generates: WHERE status = ? AND deleted_at IS NULLvar user User
err := db.Read(ctx, &database.QueryOpts{
BaseQuery: "SELECT id, balance FROM users WHERE id = ?",
Result: &user,
Trx: trx,
LockingType: database.LockUpdate, // " FOR UPDATE"
})var user User
err := db.Read(ctx, &database.QueryOpts{
BaseQuery: "SELECT id, name FROM users WHERE id = ?",
Result: &user,
UseMaster: true,
SelectRequest: &database.TableRequest{
InitiateWhere: []string{"id = ?"},
InitiateWhereValues: []interface{}{42},
},
})// Manual count + list pattern:
totalData, totalFiltered, err := commonRepo.Count(ctx, req, "users")
req := &database.TableRequest{
Page: 1,
Limit: 10,
OrderBy: "created_at DESC",
}
var users []User
err := db.Read(ctx, &database.QueryOpts{
BaseQuery: "SELECT id, name FROM users",
SelectRequest: req,
Result: &users,
IsList: true,
})
response := database.GetSelectResponse()
response.Data = users
response.ResponseMetaData = &database.ResponseMetaData{
TotalData: int64(totalData),
TotalFiltered: int64(totalFiltered),
}Use pool helpers to reduce heap allocations in hot paths:
qOpts := database.GetQueryOpts()
defer database.PutQueryOpts(qOpts)
req := database.GetTableRequest()
defer database.PutTableRequest(req)
resp := database.GetSelectResponse()
defer database.PutSelectResponse(resp)db.Write(ctx, opts) generates queries from CUDConstructData and executes them on the master DB.
| Constant | Value | Description |
|---|---|---|
database.ActionInsert |
"insert" |
Single row INSERT |
database.ActionBulkInsert |
"bulk_insert" |
Multi-row INSERT |
database.ActionUpdate |
"update" |
UPDATE with condition |
database.ActionUpdateById |
"update_by_id" |
UPDATE WHERE id = ? |
database.ActionUpsert |
"upsert" |
INSERT ... ON DUPLICATE KEY UPDATE |
database.ActionDelete |
"delete" |
Soft/hard delete with condition |
database.ActionDeleteById |
"delete_by_id" |
Soft/hard delete WHERE id = ? |
database.ActionBulkUpdate |
"bulk_update" |
UPDATE with JOIN on bulk values |
result, err := db.Write(ctx, &database.QueryOpts{
CUDRequest: &database.CUDConstructData{
Action: database.ActionInsert,
TableName: "users",
Cols: []string{"name", "email"},
Values: []interface{}{"Alice", "alice@example.com"},
},
})
// result.LastInsertID = 1
// result.RowsAffected = 1
// result.Status = truePostgreSQL appends RETURNING id so LastInsertID is populated without an extra query.
type User struct {
Id int64 `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Email string `json:"email" db:"email"`
}
user := User{Name: "Alice", Email: "alice@example.com"}
cols, vals := helper.ConstructColNameAndValue(ctx, &user)
result, err := db.Write(ctx, &database.QueryOpts{
CUDRequest: &database.CUDConstructData{
Action: database.ActionInsert,
TableName: "users",
Cols: cols,
Values: vals,
},
Result: &user, // populated with generated values (MySQL: LastInsertId)
})result, err := db.Write(ctx, &database.QueryOpts{
CUDRequest: &database.CUDConstructData{
Action: database.ActionUpdateById,
TableName: "users",
Cols: []string{"name = ?", "email = ?"},
Values: []interface{}{"Alice Updated", "new@example.com", 42},
},
})The last value in Values is the ID for the WHERE id = ? clause.
result, err := db.Write(ctx, &database.QueryOpts{
CUDRequest: &database.CUDConstructData{
Action: database.ActionUpdate,
TableName: "users",
Cols: []string{"status = ?"},
Values: []interface{}{"inactive"},
},
SelectRequest: &database.TableRequest{
InitiateWhere: []string{"email = ?"},
InitiateWhereValues: []interface{}{"old@example.com"},
},
})
// Generates: UPDATE users SET status = ? WHERE email = ?Soft delete is enabled by default — sets deleted_at = now() instead of removing rows:
result, err := db.Write(ctx, &database.QueryOpts{
CUDRequest: &database.CUDConstructData{
Action: database.ActionDeleteById,
TableName: "users",
Values: []interface{}{42},
},
}, true) // second arg = isSoftDelete
// Generates: UPDATE users SET deleted_at = now() WHERE id = ?For hard delete, pass false:
result, err := db.Write(ctx, &database.QueryOpts{
CUDRequest: &database.CUDConstructData{
Action: database.ActionDeleteById,
TableName: "users",
Values: []interface{}{42},
},
}, false)
// Generates: DELETE FROM users WHERE id = ?result, err := db.Write(ctx, &database.QueryOpts{
CUDRequest: &database.CUDConstructData{
Action: database.ActionDelete,
TableName: "users",
},
SelectRequest: &database.TableRequest{
InitiateWhere: []string{"status = ?"},
InitiateWhereValues: []interface{}{"inactive"},
},
}, true)
// Generates: UPDATE users SET deleted_at = now() WHERE status = ?users := []User{
{Name: "Alice", Email: "alice@x.com"},
{Name: "Bob", Email: "bob@x.com"},
}
cudData, err := helper.ConstructColNameAndValueBulk(ctx, &users)
cudData.Action = database.ActionBulkInsert
cudData.TableName = "users"
result, err := db.Write(ctx, &database.QueryOpts{
CUDRequest: cudData,
})
// Generates: INSERT INTO users (name, email) VALUES (?,?),(?,?)cudData := helper.ConstructColNameAndValueForUpdate(ctx, &user)
cudData.Action = database.ActionUpsert
cudData.TableName = "users"
cudData.ColsInsert = "name, email"
cudData.Values = append(cudData.Values, cudData.Values...) // dup values for ON DUPLICATE KEY UPDATE
// Generates: INSERT INTO users (name, email) VALUES (?,?) ON DUPLICATE KEY UPDATE name = ?, email = ?By default, Read-generated queries exclude soft-deleted rows (deleted_at IS NULL for Postgres, deleted_at IS NULL OR deleted_at = '0000-00-00...' for MySQL). Include them:
req := &database.TableRequest{
IncludeDeleted: true,
NotContainsDeletedCol: true, // skip the deleted_at filter entirely
}trx, err := db.Begin() // returns *sqlx.Tx
if err != nil {
return err
}
defer db.Finish(trx, &err) // Commit on nil, Rollback on error
// Use trx in QueryOpts
var user User
err = db.Read(ctx, &database.QueryOpts{
BaseQuery: "SELECT id, balance FROM users WHERE id = ?",
SelectRequest: &database.TableRequest{
InitiateWhere: []string{"id = ?"},
InitiateWhereValues: []interface{}{42},
},
Result: &user,
Trx: trx,
LockingType: database.LockUpdate,
})
if err != nil {
return err
}
result, err := db.Write(ctx, &database.QueryOpts{
CUDRequest: &database.CUDConstructData{
Action: database.ActionUpdateById,
TableName: "users",
Cols: []string{"balance = ?"},
Values: []interface{}{user.Balance - 100, 42},
},
Trx: trx,
})
if err != nil {
return err
}// GetTransaction returns a Transactions interface:
trx, err := db.GetTransaction().Begin()
defer db.GetTransaction().Finish(trx, &err)func TransferBalance(ctx context.Context, db database.ISQL, from, to int64, amount float64) (err error) {
trx, err := db.Begin()
if err != nil {
return err
}
defer db.Finish(trx, &err)
// ... read + write operations using trx ...
return nil
}database/action package provides pre-built CRUD implementations:
import "github.com/kodekoding/phastos/v2/go/database/action"
base := action.NewBase(db, "users") // table: users, softDelete: true
// or:
base := action.NewBase(db, "users", false) // hard delete// Get list with pagination, search, ordering
opts := &database.QueryOpts{
BaseQuery: "SELECT id, name, email FROM users",
SelectRequest: &database.TableRequest{Page: 1, Limit: 10},
Result: &users,
IsList: true,
}
err := base.GetList(ctx, opts)
// Get single row by ID
var user User
err := base.GetDetailById(ctx, &user, 42)
// Get single row with custom query
opts := &database.QueryOpts{
BaseQuery: "SELECT id, name FROM users",
SelectRequest: &database.TableRequest{
InitiateWhere: []string{"email = ?"},
InitiateWhereValues: []interface{}{"alice@example.com"},
},
Result: &user,
}
err := base.GetDetail(ctx, opts)// Insert (struct fields mapped automatically via reflection)
user := User{Name: "Alice", Email: "alice@example.com"}
result, err := base.Insert(ctx, &user)
// Update by ID
user.Name = "Alice Updated"
result, err := base.UpdateById(ctx, &user, 42)
// Update with custom condition
result, err := base.Update(ctx, &user, map[string]interface{}{
"email = ?": "old@example.com",
})
// Upsert (condition for duplicate check, vals for ON DUPLICATE KEY UPDATE)
result, err := base.Upsert(ctx, &user, map[string]interface{}{
"email": "alice@example.com",
})
// Delete by ID (soft delete by default)
result, err := base.DeleteById(ctx, 42)
// Delete with condition
result, err := base.Delete(ctx, map[string]interface{}{
"status = ?": "inactive",
})
// Bulk Insert
users := []User{{Name: "A"}, {Name: "B"}}
result, err := base.BulkInsert(ctx, &users)
// Bulk Update
result, err := base.BulkUpdate(ctx, &users, map[string][]interface{}{
"id": {1, 2},
})
// With transaction
trx, _ := db.Begin()
defer db.Finish(trx, &err)
result, err := base.Insert(ctx, &user, trx)type UsecaseCRUD interface {
GetList(ctx context.Context, requestData interface{}) (*database.SelectResponse, error)
GetDetailById(ctx context.Context, id interface{}) (*database.SelectResponse, error)
Insert(ctx context.Context, data interface{}) (*database.CUDResponse, error)
Update(ctx context.Context, data interface{}) (*database.CUDResponse, error)
Delete(ctx context.Context, id interface{}) (*database.CUDResponse, error)
}type RepoCRUD interface {
ReadRepo // GetList, GetDetail, GetDetailById, Count
WriteRepo // Insert, BulkInsert, BulkUpdate, Update, Upsert, UpdateById, Delete, DeleteById
}The internal sendNilResponse function (go/database/sql.go:1064) maps database errors to HTTP status codes:
no rows in result set→ returnsnil, nil(empty result)- PostgreSQL unique violation (
code 23505) → HTTP 409 Conflict - PostgreSQL check violation (
code 23514) → HTTP 422 Unprocessable Entity - Other errors → HTTP 500
err := db.Read(ctx, opts)
// err nil + result nil → no rows found
// err *custerr with Code 409/422 → constraint violation
// err *custerr with Code 500 → internal errorresult, err := db.Write(ctx, opts)
if result != nil {
queryMap := result.GetGeneratedQuery() // map[string][]interface{} — map[<query>]<params>
}Set DATABASE_SLOW_QUERY_WARNING=true and DATABASE_SLOW_QUERY_THRESHOLD (seconds, default 1) to receive platform notifications (slack, etc.) for slow queries.
export DATABASE_SLOW_QUERY_WARNING=true
export DATABASE_SLOW_QUERY_THRESHOLD=2.0database.MySQLEngine // "mysql"
database.NRMySQLEngine // "nrmysql" (New Relic instrumented)
database.PostgresEngine // "postgres"
database.NRPostgresEngine // "nrpostgres" (New Relic instrumented)Engine-aware behavior:
- Placeholder rebinding (
?→$1,$2for PG) RETURNING idappended for PG INSERT/UPDATEFOR SHAREvsLOCK IN SHARE MODEfor lockingLIMIT ? OFFSET ?(PG) vsLIMIT ?,?(MySQL) for pagination- Date format functions differ (
STR_TO_DATE/DATE_FORMATfor MySQL)
package main
import (
"context"
"log"
"github.com/kodekoding/phastos/v2/go/database"
"github.com/kodekoding/phastos/v2/go/database/action"
)
type Product struct {
Id int64 `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Price int64 `json:"price" db:"price"`
}
func main() {
db, err := database.Connect()
if err != nil {
log.Fatal(err)
}
base := action.NewBase(db, "products")
ctx := context.Background()
// Insert
product := Product{Name: "Widget", Price: 9900}
result, err := base.Insert(ctx, &product)
if err != nil {
log.Fatal(err)
}
log.Printf("Inserted: id=%d, affected=%d", result.LastInsertID, result.RowsAffected)
// List with search
var products []Product
searchReq := &database.TableRequest{
Keyword: "Widget",
SearchColsStr: "name",
Page: 1,
Limit: 10,
}
err = db.Read(ctx, &database.QueryOpts{
BaseQuery: "SELECT id, name, price FROM products",
SelectRequest: searchReq,
Result: &products,
IsList: true,
})
if err != nil {
log.Fatal(err)
}
log.Printf("Found %d products", len(products))
}Object pools are available for heap-allocation reduction in hot request paths:
qOpts := database.GetQueryOpts()
defer database.PutQueryOpts(qOpts)
req := database.GetTableRequest()
defer database.PutTableRequest(req)
resp := database.GetSelectResponse()
defer database.PutSelectResponse(resp)
cudResp := database.GetCUDResponse()
defer database.PutCUDResponse(cudResp)
cudData := database.GetCUDConstructData()
defer database.PutCUDConstructData(cudData)- Prepared statement cache — LRU bounded (500 entries each for reads and writes). Evicted on capacity; close is deferred to connection recycling.
- Rebind cache — bounded sync.Map (2000 entries), caches
?→$Nrebinding per query string. - DeleteById query cache —
sync.Mapper(tableName, isSoftDelete), stores pre-computed rebound queries.