diff --git a/pkg/services/anonymous/anonimpl/anonstore/database.go b/pkg/services/anonymous/anonimpl/anonstore/database.go index 34470936217..0db2906709f 100644 --- a/pkg/services/anonymous/anonimpl/anonstore/database.go +++ b/pkg/services/anonymous/anonimpl/anonstore/database.go @@ -15,6 +15,7 @@ import ( const cacheKeyPrefix = "anon-device" const anonymousDeviceExpiration = 30 * 24 * time.Hour +const tableName = "anon_device" var ErrDeviceLimitReached = fmt.Errorf("device limit reached") @@ -25,7 +26,7 @@ type AnonDBStore struct { } type Device struct { - ID int64 `json:"-" xorm:"id" db:"id"` + ID int64 `json:"-" xorm:"pk autoincr 'id'" db:"id"` DeviceID string `json:"deviceId" xorm:"device_id" db:"device_id"` ClientIP string `json:"clientIp" xorm:"client_ip" db:"client_ip"` UserAgent string `json:"userAgent" xorm:"user_agent" db:"user_agent"` @@ -144,31 +145,34 @@ func (s *AnonDBStore) CreateOrUpdateDevice(ctx context.Context, device *Device) } } - args := []any{device.DeviceID, device.ClientIP, device.UserAgent, - device.CreatedAt.UTC(), device.UpdatedAt.UTC()} + if s.sqlStore.GetDBType() == migrator.Spanner { + return s.insertIntoSpanner(ctx, device) + } + + args := []any{device.DeviceID, device.ClientIP, device.UserAgent, device.CreatedAt.UTC(), device.UpdatedAt.UTC()} switch s.sqlStore.GetDBType() { case migrator.Postgres: query = `INSERT INTO anon_device (device_id, client_ip, user_agent, created_at, updated_at) -VALUES ($1, $2, $3, $4, $5) -ON CONFLICT (device_id) DO UPDATE SET -client_ip = $2, -user_agent = $3, -updated_at = $5 -RETURNING id` + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (device_id) DO UPDATE SET + client_ip = $2, + user_agent = $3, + updated_at = $5 + RETURNING id` case migrator.MySQL: query = `INSERT INTO anon_device (device_id, client_ip, user_agent, created_at, updated_at) -VALUES (?, ?, ?, ?, ?) -ON DUPLICATE KEY UPDATE -client_ip = VALUES(client_ip), -user_agent = VALUES(user_agent), -updated_at = VALUES(updated_at)` + VALUES (?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + client_ip = VALUES(client_ip), + user_agent = VALUES(user_agent), + updated_at = VALUES(updated_at)` case migrator.SQLite: query = `INSERT INTO anon_device (device_id, client_ip, user_agent, created_at, updated_at) -VALUES (?, ?, ?, ?, ?) -ON CONFLICT (device_id) DO UPDATE SET -client_ip = excluded.client_ip, -user_agent = excluded.user_agent, -updated_at = excluded.updated_at` + VALUES (?, ?, ?, ?, ?) + ON CONFLICT (device_id) DO UPDATE SET + client_ip = excluded.client_ip, + user_agent = excluded.user_agent, + updated_at = excluded.updated_at` default: return fmt.Errorf("unsupported database driver: %s", s.sqlStore.GetDBType()) } @@ -182,6 +186,26 @@ updated_at = excluded.updated_at` return err } +// In Spanner INSERT OR UPDATE only works when conflict is on primary key. However here we expect conflict on non-PK +// column "device_id", so we need to use a transaction instead. +func (s *AnonDBStore) insertIntoSpanner(ctx context.Context, dev *Device) error { + return s.sqlStore.WithTransactionalDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + prev := &Device{DeviceID: dev.DeviceID} + ok, err := dbSession.Table(tableName).Get(prev) + if err != nil { + return err + } + if !ok { + _, err = dbSession.Table(tableName).Insert(dev) + return err + } + + // Include all columns in the update, even when empty (it's what inserts for other databases do too). + _, err = dbSession.Table(tableName).Where("device_id=?", dev.DeviceID).MustCols("client_ip", "user_agent", "created_at", "updated_at").Update(dev) + return err + }) +} + func (s *AnonDBStore) CountDevices(ctx context.Context, from time.Time, to time.Time) (int64, error) { var count int64 err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { @@ -229,7 +253,7 @@ func (s *AnonDBStore) SearchDevices(ctx context.Context, query *SearchDeviceQuer query.To = time.Now() } - sess := dbSess.Table("anon_device").Alias("d") + sess := dbSess.Table(tableName).Alias("d") if query.Limit > 0 { offset := query.Limit * (query.Page - 1)