diff --git a/internal/db/read_writer.go b/internal/db/read_writer.go index 30b28d8109..ee9531fe1d 100644 --- a/internal/db/read_writer.go +++ b/internal/db/read_writer.go @@ -70,6 +70,11 @@ type Reader interface { // ScanRows will scan sql rows into the interface provided ScanRows(ctx context.Context, rows *sql.Rows, result any) error + + // Now returns the current transaction timestamp. Now will return the same + // timestamp whenever it is called within a transaction. In other words, calling + // Now at the start and at the end of a transaction will return the same value. + Now(ctx context.Context) (time.Time, error) } // Writer interface defines create, update and retryable transaction handlers @@ -533,6 +538,27 @@ func (rw *Db) SearchWhere(ctx context.Context, resources any, where string, args return nil } +// Now returns the current transaction timestamp. Now will return the same +// timestamp whenever it is called within a transaction. In other words, calling +// Now at the start and at the end of a transaction will return the same value. +func (rw *Db) Now(ctx context.Context) (time.Time, error) { + const op = "db.(*Db).Now" + // The Postgres docs define the different pre-defined time variables available: + // https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-CURRENT. + // The value produced by this function is equivalent to current_timestamp. + rows, err := rw.Query(ctx, "select now()", nil) + if err != nil { + return time.Time{}, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query current timestamp")) + } + var now time.Time + for rows.Next() { + if err := rw.ScanRows(ctx, rows, &now); err != nil { + return time.Time{}, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query current timestamp")) + } + } + return now, nil +} + func isNil(i any) bool { if i == nil { return true diff --git a/internal/db/read_writer_test.go b/internal/db/read_writer_test.go index 10b0020e25..93406551d7 100644 --- a/internal/db/read_writer_test.go +++ b/internal/db/read_writer_test.go @@ -3193,3 +3193,37 @@ func TestDb_oplogMsgsForItems(t *testing.T) { // }) // } // } + +func TestDb_Now(t *testing.T) { + t.Parallel() + assert, require := assert.New(t), require.New(t) + conn, _ := TestSetup(t, "postgres") + rw := New(conn) + ctx := context.Background() + now, err := rw.Now(ctx) + require.NoError(err) + + now2, err := rw.Now(ctx) + require.NoError(err) + + // Calling Now() outside a transaction should result in different timestamps. + // Using time.Equal instead of assert.Equal since it's more consistently + // accurate for time.Time structs. + assert.False(now.Equal(now2)) + + _, err = rw.DoTx(ctx, StdRetryCnt, ExpBackoff{}, func(r Reader, _ Writer) error { + now3, err := r.Now(ctx) + require.NoError(err) + now4, err := r.Now(ctx) + require.NoError(err) + + // Calling Now() inside a transaction should result in the same timestamp + assert.True(now3.Equal(now4)) + // The timestamps within the transaction should differ from those created outside. + assert.False(now3.Equal(now)) + assert.False(now3.Equal(now2)) + + return nil + }) + require.NoError(err) +}