Recently I have a case where after collecting rows using pgx.CollectRows, I needed to iterate over the result and filter out nil values which are basically the values that did not pass my custom filter after scan.
func (p *PostgresEventStore) Load(ctx context.Context, aggregateID uuid.UUID, predicate domain.EventPredicate) ([]domain.Event, error) {
// omitted for brevity
rows, err := p.pool.Query(ctx, stmt, args...)
if err != nil {
return nil, err
}
pgEvents, err := pgx.CollectRows[domain.Event](rows, func(row pgx.CollectableRow) (domain.Event, error) {
var e PgEvent
if scanErr := row.Scan(//....); scanErr != nil {
return nil, err
}
// If it doesn't pass the predicate, we return nil
if predicate != nil && !predicate(e) {
return nil, nil
}
return e, nil
})
if err != nil {
return nil, err
}
// SECOND PASS: filter out the nil items
var events []domain.Event
for _, e := range pgEvents {
if e != nil {
events = append(events, e)
}
}
return events, nil
}
I believe it will be good to have a new function, for example pgx.CollectRowsWithFilter that extends pgx.CollectRows by also accepting a filter func func(T)bool meaning we’d handle everything in one pass. The above snippet becomes
func (p *PostgresEventStore) Load(ctx context.Context, aggregateID uuid.UUID, predicate domain.EventPredicate) ([]domain.Event, error) {
// omitted for brevity
rows, err := p.pool.Query(ctx, stmt, args...)
if err != nil {
return nil, fmt.Errorf("query events: %w", err)
}
pgEvents, err := pgx.CollectRowsWithFilter[domain.Event](
rows,
// Step 1: How to collect/scan each row
func(row pgx.CollectableRow) (domain.Event, error) {
var e PgEvent
if scanErr := row.Scan(//...); scanErr != nil {
return nil, err
}
return e, nil
},
// Step 2: Decide whether to keep the scanned event
func(evt domain.Event) bool {return predicate(evt)},
)
if err != nil {
return nil, err
}
// No second pass needed
return pgEvents, nil
}
maybe we can have something like this
func AppendRowsWithFilter[T any, S ~[]T](slice S, rows Rows, fn RowToFunc[T], filter func(T)bool) (S, error){
defer rows.Close()
for rows.Next() {
value, err := fn(rows)
if err != nil {
return nil, err
}
if filter == nil{
slice = append(slice,value)
continue
}
if filter(value) {
slice = append(slice, value)
}
}
if err := rows.Err(); err != nil {
return nil, err
}
return slice, nil
}
func CollectRowsWithFilter[T any](rows Rows, fn RowToFunc[T], filter func(T)bool) ([]T, error){
return AppendRowsWithFilter([]T{}, rows, fn, filter)
}
skipping items as soon as they fail a filter pass can improve efficiency by avoiding storing and then re-iterating the unwanted rows.
Recently I have a case where after collecting rows using
pgx.CollectRows, I needed to iterate over the result and filter out nil values which are basically the values that did not pass my custom filter after scan.I believe it will be good to have a new function, for example
pgx.CollectRowsWithFilterthat extendspgx.CollectRowsby also accepting a filter funcfunc(T)boolmeaning we’d handle everything in one pass. The above snippet becomesmaybe we can have something like this
skipping items as soon as they fail a filter pass can improve efficiency by avoiding storing and then re-iterating the unwanted rows.