Skip to content

Commit 4af697c

Browse files
fix: Standardize Error Handling in Postgres Package (#32)
* Refine error handling across Postgres methods by standardizing GORM error propagation Updated all Postgres methods and QueryBuilder implementations to return GORM errors directly for consistency and performance. Introduced optional `TranslateError` utility for standardized error conversion when needed. Enhanced documentation and integration tests to demonstrate direct GORM error handling, translation patterns, and helper functions for consistency in error handling workflows. * update docs
1 parent 0b645e4 commit 4af697c

9 files changed

Lines changed: 340 additions & 147 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ DOCKER_HOST=unix://$HOME/.colima/default/docker.sock TESTCONTAINERS_RYUK_DISABLE
7070

7171
# Go Packages Documentation
7272

73-
Generated on Tue Dec 16 18:01:28 CET 2025
73+
Generated on Thu Dec 18 12:16:13 CET 2025
7474

7575
## Packages
7676
- [tracer](docs/v1/tracer.md)

docs/v1/postgres.md

Lines changed: 162 additions & 92 deletions
Large diffs are not rendered by default.

v1/postgres/basic_ops.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import (
1212
// - dest: Pointer to a slice where the results will be stored
1313
// - conditions: Optional query conditions (follows GORM conventions)
1414
//
15-
// Return an error if the query fails or nil on success.
15+
// Returns a GORM error if the query fails or nil on success.
16+
// Use TranslateError() to convert to standardized error types if needed.
1617
//
1718
// Example:
1819
//
@@ -33,12 +34,16 @@ func (p *Postgres) Find(ctx context.Context, dest interface{}, conditions ...int
3334
// - dest: Pointer to a struct where the result will be stored
3435
// - conditions: Optional query conditions (follows GORM conventions)
3536
//
36-
// Return ErrRecordNotFound if no matching record exists, or another error if the query fails.
37+
// Returns gorm.ErrRecordNotFound if no matching record exists, or another GORM error if the query fails.
38+
// Use TranslateError() to convert to standardized error types if needed.
3739
//
3840
// Example:
3941
//
4042
// var user User
4143
// err := db.First(ctx, &user, "email = ?", "user@example.com")
44+
// if errors.Is(err, gorm.ErrRecordNotFound) {
45+
// // Handle not found
46+
// }
4247
func (p *Postgres) First(ctx context.Context, dest interface{}, conditions ...interface{}) error {
4348
p.mu.RLock()
4449
defer p.mu.RUnlock()
@@ -54,7 +59,8 @@ func (p *Postgres) First(ctx context.Context, dest interface{}, conditions ...in
5459
// - ctx: Context for the database operation
5560
// - value: The struct or slice of structs to be created
5661
//
57-
// Returns an error if the creation fails or nil on success.
62+
// Returns a GORM error if the creation fails or nil on success.
63+
// Use TranslateError() to convert to standardized error types if needed.
5864
//
5965
// Example:
6066
//
@@ -75,7 +81,8 @@ func (p *Postgres) Create(ctx context.Context, value interface{}) error {
7581
// - ctx: Context for the database operation
7682
// - value: The struct to be saved
7783
//
78-
// Returns an error if the operation fails or nil on success.
84+
// Returns a GORM error if the operation fails or nil on success.
85+
// Use TranslateError() to convert to standardized error types if needed.
7986
//
8087
// Example:
8188
//
@@ -99,7 +106,7 @@ func (p *Postgres) Save(ctx context.Context, value interface{}) error {
99106
//
100107
// Returns:
101108
// - int64: Number of rows affected by the update operation
102-
// - error: Error if the update fails, nil on success
109+
// - error: GORM error if the update fails, nil on success
103110
//
104111
// Note: The current implementation has a bug where it executes the query twice.
105112
// This should be fixed to execute only once and return both values properly.
@@ -136,7 +143,7 @@ func (p *Postgres) Update(ctx context.Context, model interface{}, attrs interfac
136143
//
137144
// Returns:
138145
// - int64: Number of rows affected by the update operation
139-
// - error: Error if the update fails, nil on success
146+
// - error: GORM error if the update fails, nil on success
140147
//
141148
// Example:
142149
//
@@ -165,7 +172,7 @@ func (p *Postgres) UpdateColumn(ctx context.Context, model interface{}, columnNa
165172
//
166173
// Returns:
167174
// - int64: Number of rows affected by the update operation
168-
// - error: Error if the update fails, nil on success
175+
// - error: GORM error if the update fails, nil on success
169176
//
170177
// Example:
171178
//
@@ -197,7 +204,7 @@ func (p *Postgres) UpdateColumns(ctx context.Context, model interface{}, columnV
197204
//
198205
// Returns:
199206
// - int64: Number of rows affected by the delete operation
200-
// - error: Error if the deletion fails, nil on success
207+
// - error: GORM error if the deletion fails, nil on success
201208
//
202209
// Example:
203210
//
@@ -231,7 +238,7 @@ func (p *Postgres) Delete(ctx context.Context, value interface{}, conditions ...
231238
//
232239
// Returns:
233240
// - int64: Number of rows affected by the SQL execution
234-
// - error: Error if the execution fails, nil on success
241+
// - error: GORM error if the execution fails, nil on success
235242
//
236243
// Example:
237244
//
@@ -259,7 +266,8 @@ func (p *Postgres) Exec(ctx context.Context, sql string, values ...interface{})
259266
// - count: Pointer to an int64 where the count will be stored
260267
// - conditions: Query conditions to filter the records to count
261268
//
262-
// Returns an error if the query fails or nil on success.
269+
// Returns a GORM error if the query fails or nil on success.
270+
// Use TranslateError() to convert to standardized error types if needed.
263271
//
264272
// Example:
265273
//
@@ -284,7 +292,7 @@ func (p *Postgres) Count(ctx context.Context, model interface{}, count *int64, c
284292
//
285293
// Returns:
286294
// - int64: Number of rows affected by the update operation
287-
// - error: Error if the update fails, nil on success
295+
// - error: GORM error if the update fails, nil on success
288296
//
289297
// Example:
290298
//

v1/postgres/doc.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,48 @@
9292
// )
9393
// app.Run()
9494
//
95+
// Error Handling:
96+
//
97+
// All methods in this package return GORM errors directly. This design provides:
98+
// - Consistency: All methods behave the same way
99+
// - Flexibility: Consumers can use errors.Is() with GORM error types
100+
// - Performance: No translation overhead unless needed
101+
// - Transparency: Preserves the full error chain from GORM
102+
//
103+
// Basic error handling with GORM errors:
104+
//
105+
// var user User
106+
// err := db.First(ctx, &user, "email = ?", "user@example.com")
107+
// if errors.Is(err, gorm.ErrRecordNotFound) {
108+
// // Handle not found
109+
// }
110+
//
111+
// err = db.Query(ctx).Where("email = ?", "user@example.com").First(&user)
112+
// if errors.Is(err, gorm.ErrRecordNotFound) {
113+
// // Handle not found
114+
// }
115+
//
116+
// For standardized error types, use TranslateError():
117+
//
118+
// err := db.First(ctx, &user, conditions)
119+
// if err != nil {
120+
// err = db.TranslateError(err)
121+
// if errors.Is(err, postgres.ErrRecordNotFound) {
122+
// // Handle not found with standardized error
123+
// }
124+
// }
125+
//
126+
// Recommended pattern - create a helper function for common error checks:
127+
//
128+
// func isRecordNotFound(err error) bool {
129+
// return errors.Is(err, gorm.ErrRecordNotFound)
130+
// }
131+
//
132+
// // Use consistently throughout your codebase
133+
// if isRecordNotFound(err) {
134+
// // Handle not found
135+
// }
136+
//
95137
// Performance Considerations:
96138
//
97139
// - Connection pooling is automatically handled to optimize performance

v1/postgres/integration_test.go

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@ package postgres
22

33
import (
44
"context"
5+
"database/sql"
6+
"errors"
57
"fmt"
68
"net"
79
"os"
810
"testing"
911
"time"
1012

1113
"github.com/docker/docker/api/types/container"
12-
"github.com/jackc/pgx/v5/pgconn"
13-
14-
"database/sql"
15-
1614
"github.com/docker/go-connections/nat"
15+
"github.com/jackc/pgx/v5/pgconn"
1716
_ "github.com/lib/pq"
1817
"github.com/stretchr/testify/assert"
1918
"github.com/stretchr/testify/require"
@@ -393,32 +392,78 @@ func TestPostgresWithFXModule(t *testing.T) {
393392
assert.Equal(t, 200, items[1].Value)
394393
})
395394

396-
// Test error translation
397-
t.Run("ErrorTranslation", func(t *testing.T) {
395+
// Test error handling - GORM errors and translation
396+
t.Run("ErrorHandling", func(t *testing.T) {
398397
ctx := context.Background()
399398

400-
// Test record didn't find an error
401-
var user TestUser
402-
err := postgres.First(ctx, &user, "name = ?", "NonExistentUser")
403-
translatedErr := postgres.TranslateError(err)
404-
assert.ErrorIs(t, translatedErr, ErrRecordNotFound)
399+
// Test 1: Direct GORM error checking (recommended pattern)
400+
t.Run("DirectGORMErrorChecking", func(t *testing.T) {
401+
var user TestUser
402+
err := postgres.First(ctx, &user, "name = ?", "NonExistentUser")
403+
assert.Error(t, err)
404+
assert.ErrorIs(t, err, gorm.ErrRecordNotFound, "Should return gorm.ErrRecordNotFound directly")
405+
406+
// Same pattern works with QueryBuilder
407+
err = postgres.Query(ctx).Where("name = ?", "NonExistentUser").First(&user)
408+
assert.Error(t, err)
409+
assert.ErrorIs(t, err, gorm.ErrRecordNotFound, "QueryBuilder should also return gorm.ErrRecordNotFound")
410+
})
405411

406-
// Create a table with a unique constraint
407-
_, err = postgres.Exec(ctx, `
408-
CREATE TABLE IF NOT EXISTS unique_test (
409-
id SERIAL PRIMARY KEY,
410-
email TEXT UNIQUE NOT NULL
411-
)
412-
`)
413-
assert.NoError(t, err)
412+
// Test 2: Helper function pattern (recommended for consistency)
413+
t.Run("HelperFunctionPattern", func(t *testing.T) {
414+
// Define helper function (would typically be in your repository layer)
415+
isRecordNotFound := func(err error) bool {
416+
return errors.Is(err, gorm.ErrRecordNotFound)
417+
}
414418

415-
// Create a record
416-
_, err = postgres.Exec(ctx, `INSERT INTO unique_test (email) VALUES ('test@example.com')`)
417-
assert.NoError(t, err)
419+
var user TestUser
420+
err := postgres.First(ctx, &user, "name = ?", "NonExistentUser")
421+
assert.True(t, isRecordNotFound(err), "Helper function should identify record not found")
422+
423+
// Works consistently with QueryBuilder too
424+
err = postgres.Query(ctx).Where("name = ?", "NonExistentUser").First(&user)
425+
assert.True(t, isRecordNotFound(err), "Helper function works with QueryBuilder")
426+
})
427+
428+
// Test 3: Error translation (optional, when standardized errors are needed)
429+
t.Run("ErrorTranslation", func(t *testing.T) {
430+
var user TestUser
431+
err := postgres.First(ctx, &user, "name = ?", "NonExistentUser")
432+
translatedErr := postgres.TranslateError(err)
433+
assert.ErrorIs(t, translatedErr, ErrRecordNotFound, "TranslateError should convert to ErrRecordNotFound")
434+
435+
// Translation works with any GORM error
436+
err = postgres.Query(ctx).Where("name = ?", "NonExistentUser").First(&user)
437+
translatedErr = postgres.TranslateError(err)
438+
assert.ErrorIs(t, translatedErr, ErrRecordNotFound, "TranslateError works with QueryBuilder errors")
439+
})
418440

419-
// Try to create a duplicate (will fail due to unique constraint)
420-
_, err = postgres.Exec(ctx, `INSERT INTO unique_test (email) VALUES ('test@example.com')`)
421-
assert.Error(t, err)
441+
// Test 4: Constraint violation errors
442+
t.Run("ConstraintViolations", func(t *testing.T) {
443+
// Create a table with a unique constraint
444+
_, err := postgres.Exec(ctx, `
445+
CREATE TABLE IF NOT EXISTS unique_test (
446+
id SERIAL PRIMARY KEY,
447+
email TEXT UNIQUE NOT NULL
448+
)
449+
`)
450+
assert.NoError(t, err)
451+
452+
// Create a record
453+
_, err = postgres.Exec(ctx, `INSERT INTO unique_test (email) VALUES ('test@example.com')`)
454+
assert.NoError(t, err)
455+
456+
// Try to create a duplicate (will fail due to unique constraint)
457+
_, err = postgres.Exec(ctx, `INSERT INTO unique_test (email) VALUES ('test@example.com')`)
458+
assert.Error(t, err)
459+
460+
// Check with GORM error
461+
assert.ErrorIs(t, err, gorm.ErrDuplicatedKey, "Should return gorm.ErrDuplicatedKey for unique violation")
462+
463+
// Or translate to standardized error
464+
translatedErr := postgres.TranslateError(err)
465+
assert.ErrorIs(t, translatedErr, ErrDuplicateKey, "Should translate to ErrDuplicateKey")
466+
})
422467
})
423468

424469
// Stop the application
@@ -2715,7 +2760,7 @@ func TestQueryBuilder_ToSubquery(t *testing.T) {
27152760

27162761
require.NoError(t, err)
27172762
assert.Len(t, results, 2) // Charlie and Dave
2718-
2763+
27192764
names := make([]string, len(results))
27202765
for i, u := range results {
27212766
names[i] = u.Name

v1/postgres/migrations.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ type MigrationHistoryRecord struct {
9292
// Parameters:
9393
// - models: The GORM models to auto-migrate
9494
//
95-
// Returns an error if any part of the migration process fails.
95+
// Returns a GORM error if any part of the migration process fails.
9696
//
9797
// This method is useful during development or for simple applications,
9898
// but for production systems, explicit migrations are recommended.
@@ -143,7 +143,8 @@ func (p *Postgres) ensureMigrationHistoryTable() error {
143143
// - ctx: Context for database operations
144144
// - migrationsDir: Directory containing the migration SQL files
145145
//
146-
// Returns an error if any migration fails or if there are issues accessing the migrations.
146+
// Returns a wrapped error if any migration fails or if there are issues accessing the migrations.
147+
// The error wraps the underlying GORM error with additional context.
147148
//
148149
// Example:
149150
//
@@ -241,7 +242,8 @@ func (p *Postgres) MigrateUp(ctx context.Context, migrationsDir string) error {
241242
// - ctx: Context for database operations
242243
// - migrationsDir: Directory containing the migration SQL files
243244
//
244-
// Returns an error if the rollback fails or if the down migration can't be found.
245+
// Returns a wrapped error if the rollback fails or if the down migration can't be found.
246+
// The error wraps the underlying GORM error with additional context.
245247
//
246248
// Example:
247249
//
@@ -372,7 +374,8 @@ func (p *Postgres) loadMigrations(dir string, direction MigrationDirection) ([]M
372374
// - migrationsDir: Directory containing the migration SQL files
373375
//
374376
// Returns a slice of maps with status information for each migration,
375-
// or an error if the status cannot be determined.
377+
// or a wrapped error if the status cannot be determined.
378+
// The error wraps the underlying GORM error with additional context.
376379
//
377380
// Example:
378381
//
@@ -440,7 +443,7 @@ func (p *Postgres) GetMigrationStatus(ctx context.Context, migrationsDir string)
440443
// - name: Descriptive name for the migration
441444
// - migrationType: Whether this is a schema or data migration
442445
//
443-
// Returns the base filename of the created migration or an error if creation fails.
446+
// Returns the base filename of the created migration or a wrapped error if creation fails.
444447
//
445448
// Example:
446449
//

0 commit comments

Comments
 (0)