Skip to content

Commit 0d66e7c

Browse files
committed
fix: BIGSERIAL pk models must be bun autoincrement (id=0 pkey collisions)
bun sends an explicit zero pk unless the field is tagged autoincrement, so the first PlanPrice/GatewayPayment/Refund row landed as id=0 and the second INSERT died on plan_prices_pkey (found by running scripts/seed against a live database). Tags the three int64 BIGSERIAL pks, returns the generated id on CreateRefund, and pins the rule in TestBigSerialPKsAreAutoIncrement.
1 parent 5d99cb1 commit 0d66e7c

5 files changed

Lines changed: 46 additions & 6 deletions

File tree

‎internal/model/refund.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ type Refund struct {
7070

7171
// ID is the BIGSERIAL row id — the model keeps int64 (like
7272
// GatewayPayment) because the ledger's own ids stay uuid TEXT.
73-
ID int64 `bun:",pk" json:"id"`
73+
ID int64 `bun:",pk,autoincrement" json:"id"`
7474
OrderID string `bun:",notnull" json:"order_id"`
7575
// PaymentProvider is one of the RefundProvider* values.
7676
PaymentProvider string `bun:",notnull,default:''" json:"payment_provider"`
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package store
2+
3+
import (
4+
"reflect"
5+
"strings"
6+
"testing"
7+
8+
"github.com/hitechcloud-vietnam/hitechcloud_keygate_licensing/internal/model"
9+
)
10+
11+
// BIGSERIAL rows pin: every int64 primary key backed by a Postgres
12+
// BIGSERIAL MUST carry the `autoincrement` bun tag. Without it bun
13+
// sends an explicit id=0 in the INSERT column list — the first row
14+
// lands as id 0 and the second INSERT dies on plan_prices_pkey (seen
15+
// live in the seed run against a real database). TEXT/uuid keys
16+
// generate their ids in the app and are unaffected.
17+
func TestBigSerialPKsAreAutoIncrement(t *testing.T) {
18+
cases := []any{
19+
PlanPrice{},
20+
GatewayPayment{},
21+
model.Refund{},
22+
}
23+
for _, m := range cases {
24+
typ := reflect.TypeOf(m)
25+
field, ok := typ.FieldByName("ID")
26+
if !ok {
27+
t.Errorf("%s has no ID field", typ.Name())
28+
continue
29+
}
30+
tag := field.Tag.Get("bun")
31+
if !strings.Contains(tag, "pk") {
32+
t.Errorf("%s.ID bun tag %q lacks pk", typ.Name(), tag)
33+
}
34+
if !strings.Contains(tag, "autoincrement") {
35+
t.Errorf("%s.ID bun tag %q lacks autoincrement — bun would insert id=0 explicitly and collide on the second row", typ.Name(), tag)
36+
}
37+
}
38+
}

‎internal/store/gateway_payments.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import (
2828
type GatewayPayment struct {
2929
bun.BaseModel `bun:"table:gateway_payments"`
3030

31-
ID int64 `bun:",pk" json:"id"`
31+
ID int64 `bun:",pk,autoincrement" json:"id"`
3232
OrderID string `bun:",notnull" json:"order_id"`
3333
Provider string `bun:",notnull" json:"provider"`
3434
ProviderRef string `bun:",notnull" json:"provider_ref"`

‎internal/store/plan_prices.go‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import (
3434
type PlanPrice struct {
3535
bun.BaseModel `bun:"table:plan_prices"`
3636

37-
ID int64 `bun:",pk" json:"id"`
37+
ID int64 `bun:",pk,autoincrement" json:"id"`
3838
PlanID string `bun:",notnull" json:"plan_id"`
3939
Currency string `bun:",notnull" json:"currency"`
4040
AmountMinor int64 `bun:",notnull" json:"amount_minor"`
@@ -76,8 +76,10 @@ func curFoldCurrency(raw string) string {
7676
// - the plan must exist (ErrPlanPricePlanNotFound) and the currency
7777
// must be a shaped ISO code (curValidCurrency).
7878
//
79-
// The model's ID is the BIGSERIAL row id; a caller leaves it zero on
80-
// create and the upsert ignores it (the conflict target decides).
79+
// The model's ID is the BIGSERIAL row id; `autoincrement` keeps bun
80+
// from sending an explicit zero on insert (which would pin every row
81+
// to id 0 and collide on the second one), and the conflict target
82+
// decides the upsert semantics.
8183
func (s *Store) SetPlanPrice(ctx context.Context, p *PlanPrice) error {
8284
if p == nil {
8385
return errors.New("plan price is required")

‎internal/store/refunds.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func (s *Store) CreateRefund(ctx context.Context, r *model.Refund) error {
6060
if r == nil {
6161
return errors.New("refund is required")
6262
}
63-
_, err := s.DB.NewInsert().Model(r).Exec(ctx)
63+
_, err := s.DB.NewInsert().Model(r).Returning("id").Exec(ctx)
6464
return err
6565
}
6666

0 commit comments

Comments
 (0)