@@ -29,8 +29,14 @@ type hasSetDeletedAt interface {
2929 SetDeletedAt (time.Time )
3030}
3131
32- // Save inserts or updates a record. Auto-detects insert vs update by ID.
33- func (t * Table [T , PT , IDT ]) Save (ctx context.Context , record PT ) error {
32+ // Save inserts or updates given records. Auto-detects insert vs update by ID.
33+ func (t * Table [T , PT , IDT ]) Save (ctx context.Context , records ... PT ) error {
34+ if len (records ) != 1 { // Handles 0 records too.
35+ return t .saveAll (ctx , records )
36+ }
37+
38+ record := records [0 ]
39+
3440 if err := record .Validate (); err != nil {
3541 return err //nolint:wrapcheck
3642 }
@@ -59,8 +65,9 @@ func (t *Table[T, PT, IDT]) Save(ctx context.Context, record PT) error {
5965 return nil
6066}
6167
62- // SaveAll saves multiple records sequentially.
63- func (t * Table [T , PT , IDT ]) SaveAll (ctx context.Context , records []PT ) error {
68+ // saveAll saves multiple records sequentially.
69+ // TODO: This can be likely optimized to use a batch insert.
70+ func (t * Table [T , PT , IDT ]) saveAll (ctx context.Context , records []PT ) error {
6471 for i := range records {
6572 if err := t .Save (ctx , records [i ]); err != nil {
6673 return err
@@ -70,56 +77,56 @@ func (t *Table[T, PT, IDT]) SaveAll(ctx context.Context, records []PT) error {
7077 return nil
7178}
7279
73- // GetOne returns the first record matching the condition.
74- func (t * Table [T , PT , IDT ]) GetOne (ctx context.Context , cond sq.Sqlizer , orderBy []string ) (PT , error ) {
80+ // Get returns the first record matching the condition.
81+ func (t * Table [T , PT , IDT ]) Get (ctx context.Context , where sq.Sqlizer , orderBy []string ) (PT , error ) {
7582 if len (orderBy ) == 0 {
7683 orderBy = []string {t .IDColumn }
7784 }
7885
79- dest := new (T )
86+ record := new (T )
8087
8188 q := t .SQL .
8289 Select ("*" ).
8390 From (t .Name ).
84- Where (cond ).
91+ Where (where ).
8592 Limit (1 ).
8693 OrderBy (orderBy ... )
8794
88- if err := t .Query .GetOne (ctx , q , dest ); err != nil {
95+ if err := t .Query .GetOne (ctx , q , record ); err != nil {
8996 return nil , err
9097 }
9198
92- return dest , nil
99+ return record , nil
93100}
94101
95- // GetAll returns all records matching the condition.
96- func (t * Table [T , PT , IDT ]) GetAll (ctx context.Context , cond sq.Sqlizer , orderBy []string ) ([]PT , error ) {
102+ // List returns all records matching the condition.
103+ func (t * Table [T , PT , IDT ]) List (ctx context.Context , where sq.Sqlizer , orderBy []string ) ([]PT , error ) {
97104 if len (orderBy ) == 0 {
98105 orderBy = []string {t .IDColumn }
99106 }
100107
101108 q := t .SQL .
102109 Select ("*" ).
103110 From (t .Name ).
104- Where (cond ).
111+ Where (where ).
105112 OrderBy (orderBy ... )
106113
107- var dest []PT
108- if err := t .Query .GetAll (ctx , q , & dest ); err != nil {
114+ var records []PT
115+ if err := t .Query .GetAll (ctx , q , & records ); err != nil {
109116 return nil , err
110117 }
111118
112- return dest , nil
119+ return records , nil
113120}
114121
115122// GetByID returns a record by its ID.
116123func (t * Table [T , PT , IDT ]) GetByID (ctx context.Context , id IDT ) (PT , error ) {
117- return t .GetOne (ctx , sq.Eq {t .IDColumn : id }, []string {t .IDColumn })
124+ return t .Get (ctx , sq.Eq {t .IDColumn : id }, []string {t .IDColumn })
118125}
119126
120127// GetByIDs returns records by their IDs.
121128func (t * Table [T , PT , IDT ]) GetByIDs (ctx context.Context , ids []IDT ) ([]PT , error ) {
122- return t .GetAll (ctx , sq.Eq {t .IDColumn : ids }, nil )
129+ return t .List (ctx , sq.Eq {t .IDColumn : ids }, nil )
123130}
124131
125132// Count returns the number of matching records.
@@ -173,31 +180,31 @@ func (t *Table[T, PT, IDT]) WithTx(tx pgx.Tx) *Table[T, PT, IDT] {
173180 }
174181}
175182
176- // LockForUpdate locks and processes records using PostgreSQL's FOR UPDATE SKIP LOCKED pattern
183+ // LockForUpdates locks and processes records using PostgreSQL's FOR UPDATE SKIP LOCKED pattern
177184// for safe concurrent processing. Each record is processed exactly once across multiple workers.
178185// Records are automatically updated after updateFn() completes. Keep updateFn() fast to avoid
179186// holding the transaction. For long-running work, update status to "processing" and return early,
180187// then process asynchronously. Use defer LockOneForUpdate() to update status to "completed" or "failed".
181- func (t * Table [T , PT , IDT ]) LockForUpdate (ctx context.Context , cond sq.Sqlizer , orderBy []string , limit uint64 , updateFn func (records []PT )) error {
188+ func (t * Table [T , PT , IDT ]) LockForUpdates (ctx context.Context , where sq.Sqlizer , orderBy []string , limit uint64 , updateFn func (records []PT )) error {
182189 // Check if we're already in a transaction
183190 if t .DB .Query .Tx != nil {
184- return t .lockForUpdateWithTx (ctx , t .DB .Query .Tx , cond , orderBy , limit , updateFn )
191+ return t .lockForUpdatesWithTx (ctx , t .DB .Query .Tx , where , orderBy , limit , updateFn )
185192 }
186193
187194 return pgx .BeginFunc (ctx , t .DB .Conn , func (pgTx pgx.Tx ) error {
188- return t .lockForUpdateWithTx (ctx , pgTx , cond , orderBy , limit , updateFn )
195+ return t .lockForUpdatesWithTx (ctx , pgTx , where , orderBy , limit , updateFn )
189196 })
190197}
191198
192- func (t * Table [T , PT , IDT ]) lockForUpdateWithTx (ctx context.Context , pgTx pgx.Tx , cond sq.Sqlizer , orderBy []string , limit uint64 , updateFn func (records []PT )) error {
199+ func (t * Table [T , PT , IDT ]) lockForUpdatesWithTx (ctx context.Context , pgTx pgx.Tx , where sq.Sqlizer , orderBy []string , limit uint64 , updateFn func (records []PT )) error {
193200 if len (orderBy ) == 0 {
194201 orderBy = []string {t .IDColumn }
195202 }
196203
197204 q := t .SQL .
198205 Select ("*" ).
199206 From (t .Name ).
200- Where (cond ).
207+ Where (where ).
201208 OrderBy (orderBy ... ).
202209 Limit (limit ).
203210 Suffix ("FOR UPDATE SKIP LOCKED" )
@@ -221,17 +228,17 @@ func (t *Table[T, PT, IDT]) lockForUpdateWithTx(ctx context.Context, pgTx pgx.Tx
221228 return nil
222229}
223230
224- // LockOneForUpdate locks and processes one record using PostgreSQL's FOR UPDATE SKIP LOCKED pattern
231+ // LockForUpdate locks and processes one record using PostgreSQL's FOR UPDATE SKIP LOCKED pattern
225232// for safe concurrent processing. The record is processed exactly once across multiple workers.
226233// The record is automatically updated after updateFn() completes. Keep updateFn() fast to avoid
227234// holding the transaction. For long-running work, update status to "processing" and return early,
228- // then process asynchronously. Use defer LockOneForUpdate () to update status to "completed" or "failed".
235+ // then process asynchronously. Use defer LockForUpdate () to update status to "completed" or "failed".
229236//
230237// Returns ErrNoRows if no matching records are available for locking.
231- func (t * Table [T , PT , IDT ]) LockOneForUpdate (ctx context.Context , cond sq.Sqlizer , orderBy []string , updateFn func (record PT )) error {
238+ func (t * Table [T , PT , IDT ]) LockForUpdate (ctx context.Context , where sq.Sqlizer , orderBy []string , updateFn func (record PT )) error {
232239 var noRows bool
233240
234- err := t .LockForUpdate (ctx , cond , orderBy , 1 , func (records []PT ) {
241+ err := t .LockForUpdates (ctx , where , orderBy , 1 , func (records []PT ) {
235242 if len (records ) > 0 {
236243 updateFn (records [0 ])
237244 } else {
0 commit comments