mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 15:41:41 +00:00
6.1 KiB
6.1 KiB
PostgreSQL Implementation Patterns
Consistent Patterns Across All Repositories
1. Package Structure
package postgres
import (
"context"
"database/sql"
"fmt"
"github.com/google/uuid"
"github.com/lib/pq"
)
2. Repository Constructor Pattern
type CertificateRepository struct {
db *sql.DB
}
func NewCertificateRepository(db *sql.DB) *CertificateRepository {
return &CertificateRepository{db: db}
}
3. UUID Generation Pattern
if cert.ID == "" {
cert.ID = uuid.New().String()
}
4. Parameterized Queries Pattern
All queries use $1, $2, $3... placeholders:
err := r.db.QueryRowContext(ctx, `
SELECT id, name FROM table WHERE id = $1
`, id).Scan(&result.ID, &result.Name)
5. Context Propagation Pattern
// QueryContext for SELECT
rows, err := r.db.QueryContext(ctx, query, args...)
// QueryRowContext for single row
row := r.db.QueryRowContext(ctx, query, args...)
// ExecContext for INSERT/UPDATE/DELETE
result, err := r.db.ExecContext(ctx, query, args...)
6. NULL Handling Pattern
// For nullable types, use sql.Null*
var agent.LastHeartbeatAt *time.Time
// Scan handles NULL automatically
err := row.Scan(&agent.LastHeartbeatAt)
7. Array Handling Pattern (pq)
import "github.com/lib/pq"
// Storing arrays
pq.Array(cert.SANs) // Converts []string to PostgreSQL array
// Scanning arrays
var sans pq.StringArray
row.Scan(&sans)
cert.SANs = []string(sans)
8. JSON Handling Pattern
import "encoding/json"
// For JSONB config columns (stored as json.RawMessage)
issuer.Config // type: json.RawMessage
// For tags (stored as JSON string)
tagsJSON, err := json.Marshal(cert.Tags)
row.Scan(&tagsJSON)
json.Unmarshal(tagsJSON, &cert.Tags)
9. Pagination Pattern
// Set defaults
if filter.Page < 1 {
filter.Page = 1
}
if filter.PerPage == 0 || filter.PerPage > 500 {
filter.PerPage = 50
}
// Calculate offset
offset := (filter.Page - 1) * filter.PerPage
// Add to query
query += fmt.Sprintf("LIMIT $%d OFFSET $%d", argCount, argCount+1)
args = append(args, filter.PerPage, offset)
10. Dynamic WHERE Clause Pattern
var whereConditions []string
var args []interface{}
argCount := 1
if filter.Status != "" {
whereConditions = append(whereConditions, fmt.Sprintf("status = $%d", argCount))
args = append(args, filter.Status)
argCount++
}
whereClause := ""
if len(whereConditions) > 0 {
whereClause = "WHERE " + strings.Join(whereConditions, " AND ")
}
11. Row Count Verification Pattern
result, err := r.db.ExecContext(ctx, query, args...)
if err != nil {
return fmt.Errorf("failed to update: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rows == 0 {
return fmt.Errorf("entity not found")
}
12. Not Found Error Pattern
row := r.db.QueryRowContext(ctx, query, args...)
entity, err := scanEntity(row)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("entity not found")
}
return nil, fmt.Errorf("failed to query entity: %w", err)
}
13. Scanner Helper Pattern (for reusable scanning)
func scanEntity(scanner interface {
Scan(...interface{}) error
}) (*domain.Entity, error) {
var e domain.Entity
err := scanner.Scan(&e.ID, &e.Name, ...)
if err != nil {
return nil, fmt.Errorf("failed to scan entity: %w", err)
}
return &e, nil
}
// Used in both single row and multiple rows contexts
row := r.db.QueryRowContext(ctx, query)
entity, err := scanEntity(row)
for rows.Next() {
entity, err := scanEntity(rows)
}
14. List Query Pattern
// Get total count first
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM table %s", whereClause)
var total int
r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total)
// Then get paginated results
rows, err := r.db.QueryContext(ctx, paginatedQuery, args...)
defer rows.Close()
var results []*domain.Entity
for rows.Next() {
entity, err := scanEntity(rows)
results = append(results, entity)
}
15. Error Wrapping Pattern
// All errors wrapped with context
if err != nil {
return fmt.Errorf("failed to create entity: %w", err)
}
16. RETURNING Clause Pattern (for retrieving generated IDs)
err := r.db.QueryRowContext(ctx, `
INSERT INTO table (col1, col2)
VALUES ($1, $2)
RETURNING id
`, val1, val2).Scan(&entity.ID)
17. Join Table Pattern (for many-to-many)
// ListByCertificate uses certificate_target_mappings join table
rows, err := r.db.QueryContext(ctx, `
SELECT dt.id, dt.name, dt.type, dt.agent_id, dt.config, dt.enabled, dt.created_at, dt.updated_at
FROM deployment_targets dt
INNER JOIN certificate_target_mappings ctm ON dt.id = ctm.target_id
WHERE ctm.certificate_id = $1
ORDER BY dt.created_at DESC
`, certID)
Type-Specific Patterns
Certificate with Arrays and JSON
// In certificate.go
var sans pq.StringArray
var tagsJSON []byte
err := scanner.Scan(&cert.ID, &cert.Name, &cert.CommonName, &sans, ...)
if err != nil {
return nil, fmt.Errorf("failed to scan: %w", err)
}
cert.SANs = []string(sans)
json.Unmarshal(tagsJSON, &cert.Tags)
Agent with Nullable Timestamp
// In agent.go
var agent domain.Agent
err := scanner.Scan(&agent.ID, &agent.Name, &agent.Hostname, &agent.Status,
&agent.LastHeartbeatAt, &agent.RegisteredAt, &agent.APIKeyHash)
// LastHeartbeatAt can be nil, automatically handled by sql.NullTime
Job with Nullable String
// In job.go
var job domain.Job
var lastError *string
err := scanner.Scan(&job.ID, ..., &lastError, ...)
// lastError can be nil for successful jobs
job.LastError = lastError
Testing Considerations
These implementations expect:
- PostgreSQL database with proper schema
- Tables created with matching column names and types
- Foreign key relationships established
- Proper indexes on frequently queried columns
For testing, consider:
- Using
testcontainers-gofor PostgreSQL in Docker - Running migrations before test suite
- Using transactions with rollback for test isolation