internal/store/postgres/help.go
Rama main · réplica de lectura del repositorio original.
package postgres
import (
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"hash/fnv"
"github.com/jackc/pgx/v5"
"telesrv/internal/domain"
"telesrv/internal/store/postgres/sqlcgen"
)
// HelpStore implements store.AppConfigStore and store.CountryStore with PostgreSQL.
type HelpStore struct {
db sqlcgen.DBTX
q *sqlcgen.Queries
}
// NewHelpStore creates a HelpStore backed by a pgx pool (or transaction).
func NewHelpStore(db sqlcgen.DBTX) *HelpStore {
return &HelpStore{db: db, q: sqlcgen.New(db)}
}
func (s *HelpStore) GetAppConfig(ctx context.Context, client string) (domain.AppConfig, bool, error) {
row, err := s.q.GetAppConfig(ctx, client)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.AppConfig{Client: client, JSON: []byte("{}")}, false, nil
}
return domain.AppConfig{}, false, fmt.Errorf("get app config: %w", err)
}
return domain.AppConfig{
Client: row.Client,
Hash: int(row.Hash),
JSON: []byte(row.ConfigJson),
}, true, nil
}
func (s *HelpStore) UpsertAppConfig(ctx context.Context, cfg domain.AppConfig) error {
if len(cfg.JSON) == 0 {
cfg.JSON = []byte("{}")
}
if err := s.q.UpsertAppConfig(ctx, sqlcgen.UpsertAppConfigParams{
Client: cfg.Client,
Hash: int32(cfg.Hash),
ConfigJson: cfg.JSON,
}); err != nil {
return fmt.Errorf("upsert app config: %w", err)
}
return nil
}
func (s *HelpStore) ListCountries(ctx context.Context, _ string) (domain.CountriesList, error) {
rows, err := s.q.ListCountries(ctx)
if err != nil {
return domain.CountriesList{}, fmt.Errorf("list countries: %w", err)
}
byISO := make(map[string]int)
out := domain.CountriesList{}
for _, row := range rows {
idx, ok := byISO[row.Iso2]
if !ok {
idx = len(out.Countries)
byISO[row.Iso2] = idx
out.Countries = append(out.Countries, domain.Country{
ISO2: row.Iso2,
DefaultName: row.DefaultName,
Name: row.Name,
Hidden: row.Hidden,
})
}
out.Countries[idx].CountryCodes = append(out.Countries[idx].CountryCodes, domain.CountryCode{
CountryCode: row.CountryCode,
Prefixes: append([]string(nil), row.Prefixes...),
Patterns: append([]string(nil), row.Patterns...),
})
}
out.Hash = countriesHash(out.Countries)
return out, nil
}
// Write the whole catalog in one go. Previously each country/number had its own auto-committed
// statement (~600 round trips + fsync), costing 15 seconds at startup; here jsonb_to_recordset
// merges all rows into two statements.
const (
upsertCountriesSQL = `
INSERT INTO countries (iso2, default_name, name, hidden, order_index)
SELECT iso2, default_name, name, hidden, order_index
FROM jsonb_to_recordset($1::jsonb) AS x(
iso2 text, default_name text, name text, hidden boolean, order_index integer)
ON CONFLICT (iso2) DO UPDATE SET
default_name = EXCLUDED.default_name,
name = EXCLUDED.name,
hidden = EXCLUDED.hidden,
order_index = EXCLUDED.order_index,
updated_at = now()`
upsertCountryCodesSQL = `
INSERT INTO country_codes (iso2, country_code, prefixes, patterns, order_index)
SELECT iso2, country_code,
ARRAY(SELECT jsonb_array_elements_text(prefixes)),
ARRAY(SELECT jsonb_array_elements_text(patterns)),
order_index
FROM jsonb_to_recordset($1::jsonb) AS x(
iso2 text, country_code text, prefixes jsonb, patterns jsonb, order_index integer)
ON CONFLICT (iso2, country_code) DO UPDATE SET
prefixes = EXCLUDED.prefixes,
patterns = EXCLUDED.patterns,
order_index = EXCLUDED.order_index`
)
type countrySeedRow struct {
ISO2 string `json:"iso2"`
DefaultName string `json:"default_name"`
Name string `json:"name"`
Hidden bool `json:"hidden"`
OrderIndex int32 `json:"order_index"`
}
type countryCodeSeedRow struct {
ISO2 string `json:"iso2"`
CountryCode string `json:"country_code"`
Prefixes []string `json:"prefixes"`
Patterns []string `json:"patterns"`
OrderIndex int32 `json:"order_index"`
}
func (s *HelpStore) UpsertCountries(ctx context.Context, countries []domain.Country) error {
if len(countries) == 0 {
return nil
}
countryRows := make([]countrySeedRow, 0, len(countries))
codeRows := make([]countryCodeSeedRow, 0, len(countries)*3)
for i, country := range countries {
countryRows = append(countryRows, countrySeedRow{
ISO2: country.ISO2,
DefaultName: country.DefaultName,
Name: country.Name,
Hidden: country.Hidden,
OrderIndex: int32(i + 1),
})
for j, code := range country.CountryCodes {
// The prefixes/patterns columns are NOT NULL: for a country with no prefix/pattern
// (e.g. AD) the catalog value is nil, so it must be normalized to an empty array;
// otherwise jsonb null comes back as NULL.
prefixes := code.Prefixes
if prefixes == nil {
prefixes = []string{}
}
patterns := code.Patterns
if patterns == nil {
patterns = []string{}
}
codeRows = append(codeRows, countryCodeSeedRow{
ISO2: country.ISO2,
CountryCode: code.CountryCode,
Prefixes: prefixes,
Patterns: patterns,
OrderIndex: int32(j + 1),
})
}
}
countriesJSON, err := json.Marshal(countryRows)
if err != nil {
return fmt.Errorf("marshal country seed rows: %w", err)
}
codesJSON, err := json.Marshal(codeRows)
if err != nil {
return fmt.Errorf("marshal country code seed rows: %w", err)
}
// Put the two statements in one transaction to avoid leaving half a catalog on failure.
// An implementation without Begin (e.g. the passed handle is already a tx) executes directly.
if beginner, ok := s.db.(interface {
Begin(context.Context) (pgx.Tx, error)
}); ok {
tx, err := beginner.Begin(ctx)
if err != nil {
return fmt.Errorf("begin countries upsert: %w", err)
}
if err := upsertCountries(ctx, tx, countriesJSON, codesJSON); err != nil {
_ = tx.Rollback(ctx)
return err
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit countries upsert: %w", err)
}
return nil
}
return upsertCountries(ctx, s.db, countriesJSON, codesJSON)
}
func upsertCountries(ctx context.Context, db sqlcgen.DBTX, countriesJSON, codesJSON []byte) error {
if _, err := db.Exec(ctx, upsertCountriesSQL, countriesJSON); err != nil {
return fmt.Errorf("upsert countries: %w", err)
}
if _, err := db.Exec(ctx, upsertCountryCodesSQL, codesJSON); err != nil {
return fmt.Errorf("upsert country codes: %w", err)
}
return nil
}
func countriesHash(countries []domain.Country) int {
if len(countries) == 0 {
return 0
}
h := fnv.New32a()
var buf [4]byte
for _, country := range countries {
_, _ = h.Write([]byte(country.ISO2))
_, _ = h.Write([]byte(country.DefaultName))
_, _ = h.Write([]byte(country.Name))
if country.Hidden {
buf[0] = 1
} else {
buf[0] = 0
}
_, _ = h.Write(buf[:1])
for _, code := range country.CountryCodes {
_, _ = h.Write([]byte(code.CountryCode))
binary.LittleEndian.PutUint32(buf[:], uint32(len(code.Prefixes)))
_, _ = h.Write(buf[:])
for _, prefix := range code.Prefixes {
_, _ = h.Write([]byte(prefix))
}
binary.LittleEndian.PutUint32(buf[:], uint32(len(code.Patterns)))
_, _ = h.Write(buf[:])
for _, pattern := range code.Patterns {
_, _ = h.Write([]byte(pattern))
}
}
}
return int(h.Sum32())
}