feat: use postgres

This commit is contained in:
Ilia Denisov
2026-04-26 20:34:39 +02:00
committed by GitHub
parent 48b0056b49
commit fe829285a6
365 changed files with 29223 additions and 24049 deletions
@@ -0,0 +1,248 @@
package notificationstore
import (
"context"
"database/sql"
"fmt"
"time"
"galaxy/notification/internal/api/intentstream"
pgtable "galaxy/notification/internal/adapters/postgres/jet/notification/table"
"galaxy/notification/internal/service/acceptintent"
pg "github.com/go-jet/jet/v2/postgres"
)
// routeSelectColumns is the canonical SELECT list for the routes table,
// matching scanRoute's column order.
var routeSelectColumns = pg.ColumnList{
pgtable.Routes.NotificationID,
pgtable.Routes.RouteID,
pgtable.Routes.Channel,
pgtable.Routes.RecipientRef,
pgtable.Routes.Status,
pgtable.Routes.AttemptCount,
pgtable.Routes.MaxAttempts,
pgtable.Routes.NextAttemptAt,
pgtable.Routes.ResolvedEmail,
pgtable.Routes.ResolvedLocale,
pgtable.Routes.LastErrorClassification,
pgtable.Routes.LastErrorMessage,
pgtable.Routes.LastErrorAt,
pgtable.Routes.CreatedAt,
pgtable.Routes.UpdatedAt,
pgtable.Routes.PublishedAt,
pgtable.Routes.DeadLetteredAt,
pgtable.Routes.SkippedAt,
}
// scanRoute scans one routes row from rs.
func scanRoute(rs rowScanner) (acceptintent.NotificationRoute, error) {
var (
notificationID string
routeID string
channel string
recipientRef string
status string
attemptCount int
maxAttempts int
nextAttemptAt sql.NullTime
resolvedEmail string
resolvedLocale string
lastErrorClassification string
lastErrorMessage string
lastErrorAt sql.NullTime
createdAt time.Time
updatedAt time.Time
publishedAt sql.NullTime
deadLetteredAt sql.NullTime
skippedAt sql.NullTime
)
if err := rs.Scan(
&notificationID,
&routeID,
&channel,
&recipientRef,
&status,
&attemptCount,
&maxAttempts,
&nextAttemptAt,
&resolvedEmail,
&resolvedLocale,
&lastErrorClassification,
&lastErrorMessage,
&lastErrorAt,
&createdAt,
&updatedAt,
&publishedAt,
&deadLetteredAt,
&skippedAt,
); err != nil {
return acceptintent.NotificationRoute{}, err
}
return acceptintent.NotificationRoute{
NotificationID: notificationID,
RouteID: routeID,
Channel: intentstream.Channel(channel),
RecipientRef: recipientRef,
Status: acceptintent.RouteStatus(status),
AttemptCount: attemptCount,
MaxAttempts: maxAttempts,
NextAttemptAt: timeFromNullable(nextAttemptAt),
ResolvedEmail: resolvedEmail,
ResolvedLocale: resolvedLocale,
LastErrorClassification: lastErrorClassification,
LastErrorMessage: lastErrorMessage,
LastErrorAt: timeFromNullable(lastErrorAt),
CreatedAt: createdAt.UTC(),
UpdatedAt: updatedAt.UTC(),
PublishedAt: timeFromNullable(publishedAt),
DeadLetteredAt: timeFromNullable(deadLetteredAt),
SkippedAt: timeFromNullable(skippedAt),
}, nil
}
// insertRoute writes one route row inside an open transaction.
func insertRoute(ctx context.Context, tx *sql.Tx, route acceptintent.NotificationRoute) error {
if err := route.Validate(); err != nil {
return fmt.Errorf("insert route: %w", err)
}
stmt := pgtable.Routes.INSERT(
pgtable.Routes.NotificationID,
pgtable.Routes.RouteID,
pgtable.Routes.Channel,
pgtable.Routes.RecipientRef,
pgtable.Routes.Status,
pgtable.Routes.AttemptCount,
pgtable.Routes.MaxAttempts,
pgtable.Routes.NextAttemptAt,
pgtable.Routes.ResolvedEmail,
pgtable.Routes.ResolvedLocale,
pgtable.Routes.LastErrorClassification,
pgtable.Routes.LastErrorMessage,
pgtable.Routes.LastErrorAt,
pgtable.Routes.CreatedAt,
pgtable.Routes.UpdatedAt,
pgtable.Routes.PublishedAt,
pgtable.Routes.DeadLetteredAt,
pgtable.Routes.SkippedAt,
).VALUES(
route.NotificationID,
route.RouteID,
string(route.Channel),
route.RecipientRef,
string(route.Status),
route.AttemptCount,
route.MaxAttempts,
nullableTime(route.NextAttemptAt),
route.ResolvedEmail,
route.ResolvedLocale,
route.LastErrorClassification,
route.LastErrorMessage,
nullableTime(route.LastErrorAt),
route.CreatedAt.UTC(),
route.UpdatedAt.UTC(),
nullableTime(route.PublishedAt),
nullableTime(route.DeadLetteredAt),
nullableTime(route.SkippedAt),
)
query, args := stmt.Sql()
if _, err := tx.ExecContext(ctx, query, args...); err != nil {
return err
}
return nil
}
// loadRoute returns one route row by composite key. found is false when no
// matching row exists.
func loadRoute(ctx context.Context, db *sql.DB, notificationID string, routeID string) (acceptintent.NotificationRoute, bool, error) {
stmt := pg.SELECT(routeSelectColumns).
FROM(pgtable.Routes).
WHERE(pg.AND(
pgtable.Routes.NotificationID.EQ(pg.String(notificationID)),
pgtable.Routes.RouteID.EQ(pg.String(routeID)),
))
query, args := stmt.Sql()
row := db.QueryRowContext(ctx, query, args...)
route, err := scanRoute(row)
if isNoRows(err) {
return acceptintent.NotificationRoute{}, false, nil
}
if err != nil {
return acceptintent.NotificationRoute{}, false, fmt.Errorf("load notification route: %w", err)
}
return route, true, nil
}
// loadRouteTx returns one route row by composite key inside an open
// transaction.
func loadRouteTx(ctx context.Context, tx *sql.Tx, notificationID string, routeID string) (acceptintent.NotificationRoute, bool, error) {
stmt := pg.SELECT(routeSelectColumns).
FROM(pgtable.Routes).
WHERE(pg.AND(
pgtable.Routes.NotificationID.EQ(pg.String(notificationID)),
pgtable.Routes.RouteID.EQ(pg.String(routeID)),
))
query, args := stmt.Sql()
row := tx.QueryRowContext(ctx, query, args...)
route, err := scanRoute(row)
if isNoRows(err) {
return acceptintent.NotificationRoute{}, false, nil
}
if err != nil {
return acceptintent.NotificationRoute{}, false, fmt.Errorf("load notification route: %w", err)
}
return route, true, nil
}
// updateRouteIfMatching writes the route columns back inside an open
// transaction, gated on `updated_at = expectedUpdatedAt`. Returns the
// number of rows actually updated; zero indicates an optimistic-concurrency
// loss.
func updateRouteIfMatching(ctx context.Context, tx *sql.Tx, route acceptintent.NotificationRoute, expectedUpdatedAt time.Time) (int64, error) {
stmt := pgtable.Routes.UPDATE(
pgtable.Routes.Status,
pgtable.Routes.AttemptCount,
pgtable.Routes.NextAttemptAt,
pgtable.Routes.ResolvedEmail,
pgtable.Routes.ResolvedLocale,
pgtable.Routes.LastErrorClassification,
pgtable.Routes.LastErrorMessage,
pgtable.Routes.LastErrorAt,
pgtable.Routes.UpdatedAt,
pgtable.Routes.PublishedAt,
pgtable.Routes.DeadLetteredAt,
pgtable.Routes.SkippedAt,
).SET(
string(route.Status),
route.AttemptCount,
nullableTime(route.NextAttemptAt),
route.ResolvedEmail,
route.ResolvedLocale,
route.LastErrorClassification,
route.LastErrorMessage,
nullableTime(route.LastErrorAt),
route.UpdatedAt.UTC(),
nullableTime(route.PublishedAt),
nullableTime(route.DeadLetteredAt),
nullableTime(route.SkippedAt),
).WHERE(pg.AND(
pgtable.Routes.NotificationID.EQ(pg.String(route.NotificationID)),
pgtable.Routes.RouteID.EQ(pg.String(route.RouteID)),
pgtable.Routes.UpdatedAt.EQ(pg.TimestampzT(expectedUpdatedAt.UTC())),
))
query, args := stmt.Sql()
result, err := tx.ExecContext(ctx, query, args...)
if err != nil {
return 0, err
}
rows, err := result.RowsAffected()
if err != nil {
return 0, err
}
return rows, nil
}