feat: sync langpack and auth updates
Sync telesrv commits 49f9bab and 04d9563 into the public mirror. Exclude private docs and runtime key material per sync rules.
This commit is contained in:
parent
6af61f26ba
commit
1292540350
244 changed files with 1552172 additions and 27668 deletions
|
|
@ -2,6 +2,7 @@ package postgres
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
|
|
@ -68,6 +69,68 @@ func (s *LangPackStore) GetStrings(ctx context.Context, langPack, langCode strin
|
|||
return meta, nil
|
||||
}
|
||||
|
||||
func (s *LangPackStore) ListLanguages(ctx context.Context, langPack string) ([]domain.LangPackLanguage, error) {
|
||||
rows, err := s.q.ListLangPackLanguages(ctx, langPack)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list lang pack languages: %w", err)
|
||||
}
|
||||
out := make([]domain.LangPackLanguage, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, domain.LangPackLanguage{
|
||||
LangPack: row.LangPack,
|
||||
LangCode: row.LangCode,
|
||||
Name: row.Name,
|
||||
NativeName: row.NativeName,
|
||||
StringsCount: int(row.StringsCount),
|
||||
TranslatedCount: int(row.StringsCount),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *LangPackStore) GetSeedCatalog(ctx context.Context, catalog string) (domain.LangPackSeedCatalog, error) {
|
||||
if catalog == "" {
|
||||
catalog = "default"
|
||||
}
|
||||
encoded, err := s.q.GetLangPackSeedHash(ctx, langPackSeedManifestStateKey(catalog))
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.LangPackSeedCatalog{Catalog: catalog}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return domain.LangPackSeedCatalog{}, fmt.Errorf("get langpack seed catalog %q: %w", catalog, err)
|
||||
}
|
||||
var state domain.LangPackSeedCatalog
|
||||
if err := json.Unmarshal([]byte(encoded), &state); err != nil {
|
||||
return domain.LangPackSeedCatalog{}, fmt.Errorf("decode langpack seed catalog %q: %w", catalog, err)
|
||||
}
|
||||
if state.Catalog != catalog {
|
||||
return domain.LangPackSeedCatalog{}, fmt.Errorf("langpack seed catalog key %q contains catalog %q", catalog, state.Catalog)
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func (s *LangPackStore) ReconcileSeed(ctx context.Context, seed domain.LangPackSeed) (int, error) {
|
||||
txer, ok := s.db.(interface {
|
||||
Begin(context.Context) (pgx.Tx, error)
|
||||
})
|
||||
if !ok {
|
||||
return 0, errors.New("langpack seed reconciliation requires transaction support")
|
||||
}
|
||||
tx, err := txer.Begin(ctx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("begin langpack seed reconciliation: %w", err)
|
||||
}
|
||||
written, err := reconcileSeedWith(ctx, tx, s.q.WithTx(tx), seed)
|
||||
if err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
return 0, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return 0, fmt.Errorf("commit langpack seed reconciliation: %w", err)
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func (s *LangPackStore) UpsertPack(ctx context.Context, pack domain.LangPack) error {
|
||||
if txer, ok := s.db.(interface {
|
||||
Begin(context.Context) (pgx.Tx, error)
|
||||
|
|
@ -77,7 +140,15 @@ func (s *LangPackStore) UpsertPack(ctx context.Context, pack domain.LangPack) er
|
|||
return fmt.Errorf("begin lang pack upsert: %w", err)
|
||||
}
|
||||
q := s.q.WithTx(tx)
|
||||
if err := upsertPackWith(ctx, q, pack); err != nil {
|
||||
if err := replacePackWithCopy(ctx, tx, q, pack); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
return err
|
||||
}
|
||||
if err := q.DeleteLangPackSeedHash(ctx, langPackSeedStateKey(pack.LangPack, pack.LangCode)); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
return fmt.Errorf("delete lang pack seed hash: %w", err)
|
||||
}
|
||||
if err := invalidateLangPackSeedCatalogs(ctx, tx); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
return err
|
||||
}
|
||||
|
|
@ -86,10 +157,189 @@ func (s *LangPackStore) UpsertPack(ctx context.Context, pack domain.LangPack) er
|
|||
}
|
||||
return nil
|
||||
}
|
||||
return upsertPackWith(ctx, s.q, pack)
|
||||
if err := replacePackWithQueries(ctx, s.q, pack); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.q.DeleteLangPackSeedHash(ctx, langPackSeedStateKey(pack.LangPack, pack.LangCode)); err != nil {
|
||||
return fmt.Errorf("delete lang pack seed hash: %w", err)
|
||||
}
|
||||
if err := invalidateLangPackSeedCatalogs(ctx, s.db); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func upsertPackWith(ctx context.Context, q *sqlcgen.Queries, pack domain.LangPack) error {
|
||||
func reconcileSeedWith(ctx context.Context, tx pgx.Tx, q *sqlcgen.Queries, seed domain.LangPackSeed) (int, error) {
|
||||
catalog := seed.Catalog
|
||||
if catalog == "" {
|
||||
catalog = "default"
|
||||
}
|
||||
reconciledScopes := append([]string(nil), seed.Scopes...)
|
||||
scopes := make(map[string]struct{}, len(seed.Scopes))
|
||||
wanted := make(map[string]map[string]struct{}, len(seed.Scopes))
|
||||
for _, scope := range seed.Scopes {
|
||||
scopes[scope] = struct{}{}
|
||||
wanted[scope] = make(map[string]struct{})
|
||||
}
|
||||
previousScopesJSON, err := q.GetLangPackSeedHash(ctx, langPackSeedCatalogStateKey(catalog))
|
||||
if err == nil {
|
||||
var previousScopes []string
|
||||
if err := json.Unmarshal([]byte(previousScopesJSON), &previousScopes); err != nil {
|
||||
return 0, fmt.Errorf("decode previous langpack seed scopes for %q: %w", catalog, err)
|
||||
}
|
||||
for _, scope := range previousScopes {
|
||||
if _, exists := scopes[scope]; exists {
|
||||
continue
|
||||
}
|
||||
scopes[scope] = struct{}{}
|
||||
wanted[scope] = make(map[string]struct{})
|
||||
reconciledScopes = append(reconciledScopes, scope)
|
||||
}
|
||||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, fmt.Errorf("get previous langpack seed scopes for %q: %w", catalog, err)
|
||||
}
|
||||
for _, entry := range seed.Packs {
|
||||
if err := validateSeedEntry(entry); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if _, ok := scopes[entry.Pack.LangPack]; !ok {
|
||||
return 0, fmt.Errorf("seeded langpack %s/%s is outside reconciliation scopes", entry.Pack.LangPack, entry.Pack.LangCode)
|
||||
}
|
||||
if _, exists := wanted[entry.Pack.LangPack][entry.Pack.LangCode]; exists {
|
||||
return 0, fmt.Errorf("duplicate seeded langpack %s/%s", entry.Pack.LangPack, entry.Pack.LangCode)
|
||||
}
|
||||
wanted[entry.Pack.LangPack][entry.Pack.LangCode] = struct{}{}
|
||||
}
|
||||
|
||||
for _, scope := range reconciledScopes {
|
||||
codes, err := q.ListLangPackCodes(ctx, scope)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("list existing langpack codes for %q: %w", scope, err)
|
||||
}
|
||||
for _, code := range codes {
|
||||
if _, keep := wanted[scope][code]; keep {
|
||||
continue
|
||||
}
|
||||
params := sqlcgen.DeleteLangPackStringsParams{LangPack: scope, LangCode: code}
|
||||
if err := q.DeleteLangPackStrings(ctx, params); err != nil {
|
||||
return 0, fmt.Errorf("delete removed langpack strings %s/%s: %w", scope, code, err)
|
||||
}
|
||||
if err := q.DeleteLangPackMeta(ctx, sqlcgen.DeleteLangPackMetaParams(params)); err != nil {
|
||||
return 0, fmt.Errorf("delete removed langpack metadata %s/%s: %w", scope, code, err)
|
||||
}
|
||||
if err := q.DeleteLangPackSeedHash(ctx, langPackSeedStateKey(scope, code)); err != nil {
|
||||
return 0, fmt.Errorf("delete removed langpack seed hash %s/%s: %w", scope, code, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
written := 0
|
||||
for _, entry := range seed.Packs {
|
||||
pack := entry.Pack
|
||||
meta, metaErr := q.GetLangPackMeta(ctx, sqlcgen.GetLangPackMetaParams{
|
||||
LangPack: pack.LangPack,
|
||||
LangCode: pack.LangCode,
|
||||
})
|
||||
metaFound := metaErr == nil
|
||||
if metaErr != nil && !errors.Is(metaErr, pgx.ErrNoRows) {
|
||||
return 0, fmt.Errorf("get existing langpack metadata %s/%s: %w", pack.LangPack, pack.LangCode, metaErr)
|
||||
}
|
||||
if metaFound && pack.Version < int(meta.Version) {
|
||||
return 0, fmt.Errorf("langpack version rollback %s/%s: %d < %d", pack.LangPack, pack.LangCode, pack.Version, meta.Version)
|
||||
}
|
||||
|
||||
stateKey := langPackSeedStateKey(pack.LangPack, pack.LangCode)
|
||||
oldHash, hashErr := q.GetLangPackSeedHash(ctx, stateKey)
|
||||
hashFound := hashErr == nil
|
||||
if hashErr != nil && !errors.Is(hashErr, pgx.ErrNoRows) {
|
||||
return 0, fmt.Errorf("get langpack seed hash %s/%s: %w", pack.LangPack, pack.LangCode, hashErr)
|
||||
}
|
||||
if metaFound && hashFound && pack.Version == int(meta.Version) && oldHash != entry.ContentHash {
|
||||
return 0, fmt.Errorf("langpack %s/%s v%d content changed without version bump", pack.LangPack, pack.LangCode, pack.Version)
|
||||
}
|
||||
if metaFound && hashFound && pack.Version == int(meta.Version) && int(meta.StringsCount) == entry.StringsCount && oldHash == entry.ContentHash {
|
||||
continue
|
||||
}
|
||||
if !entry.ContentLoaded {
|
||||
return 0, fmt.Errorf("langpack %s/%s content is required but was not loaded", pack.LangPack, pack.LangCode)
|
||||
}
|
||||
|
||||
if err := replacePackWithCopy(ctx, tx, q, pack); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := q.PutLangPackSeedHash(ctx, sqlcgen.PutLangPackSeedHashParams{Key: stateKey, ContentHash: entry.ContentHash}); err != nil {
|
||||
return 0, fmt.Errorf("put langpack seed hash %s/%s: %w", pack.LangPack, pack.LangCode, err)
|
||||
}
|
||||
written += len(pack.Strings)
|
||||
}
|
||||
encodedScopes, err := json.Marshal(seed.Scopes)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("encode langpack seed scopes for %q: %w", catalog, err)
|
||||
}
|
||||
if err := q.PutLangPackSeedHash(ctx, sqlcgen.PutLangPackSeedHashParams{
|
||||
Key: langPackSeedCatalogStateKey(catalog),
|
||||
ContentHash: string(encodedScopes),
|
||||
}); err != nil {
|
||||
return 0, fmt.Errorf("put langpack seed scopes for %q: %w", catalog, err)
|
||||
}
|
||||
manifest := seedCatalogSnapshot(seed)
|
||||
encodedManifest, err := json.Marshal(manifest)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("encode langpack seed catalog %q: %w", catalog, err)
|
||||
}
|
||||
if err := q.PutLangPackSeedHash(ctx, sqlcgen.PutLangPackSeedHashParams{
|
||||
Key: langPackSeedManifestStateKey(catalog),
|
||||
ContentHash: string(encodedManifest),
|
||||
}); err != nil {
|
||||
return 0, fmt.Errorf("put langpack seed catalog %q: %w", catalog, err)
|
||||
}
|
||||
return written, nil
|
||||
}
|
||||
|
||||
func replacePackWithCopy(ctx context.Context, tx pgx.Tx, q *sqlcgen.Queries, pack domain.LangPack) error {
|
||||
params := sqlcgen.DeleteLangPackStringsParams{LangPack: pack.LangPack, LangCode: pack.LangCode}
|
||||
if err := q.DeleteLangPackStrings(ctx, params); err != nil {
|
||||
return fmt.Errorf("delete previous lang pack strings: %w", err)
|
||||
}
|
||||
if err := q.UpsertLangPackMeta(ctx, sqlcgen.UpsertLangPackMetaParams{
|
||||
LangPack: pack.LangPack,
|
||||
LangCode: pack.LangCode,
|
||||
Version: int32(pack.Version),
|
||||
StringsCount: int32(len(pack.Strings)),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("upsert lang pack meta: %w", err)
|
||||
}
|
||||
if len(pack.Strings) == 0 {
|
||||
return nil
|
||||
}
|
||||
count, err := tx.CopyFrom(
|
||||
ctx,
|
||||
pgx.Identifier{"lang_pack_strings"},
|
||||
[]string{
|
||||
"lang_pack", "lang_code", "key", "version", "pluralized", "value",
|
||||
"zero_value", "one_value", "two_value", "few_value", "many_value", "other_value", "deleted",
|
||||
},
|
||||
pgx.CopyFromSlice(len(pack.Strings), func(i int) ([]any, error) {
|
||||
item := pack.Strings[i]
|
||||
return []any{
|
||||
pack.LangPack, pack.LangCode, item.Key, int32(pack.Version), item.Pluralized, item.Value,
|
||||
item.ZeroValue, item.OneValue, item.TwoValue, item.FewValue, item.ManyValue, item.OtherValue, item.Deleted,
|
||||
}, nil
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copy lang pack strings: %w", err)
|
||||
}
|
||||
if count != int64(len(pack.Strings)) {
|
||||
return fmt.Errorf("copy lang pack strings wrote %d rows, want %d", count, len(pack.Strings))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func replacePackWithQueries(ctx context.Context, q *sqlcgen.Queries, pack domain.LangPack) error {
|
||||
if err := q.DeleteLangPackStrings(ctx, sqlcgen.DeleteLangPackStringsParams{LangPack: pack.LangPack, LangCode: pack.LangCode}); err != nil {
|
||||
return fmt.Errorf("delete previous lang pack strings: %w", err)
|
||||
}
|
||||
if err := q.UpsertLangPackMeta(ctx, sqlcgen.UpsertLangPackMetaParams{
|
||||
LangPack: pack.LangPack,
|
||||
LangCode: pack.LangCode,
|
||||
|
|
@ -120,6 +370,54 @@ func upsertPackWith(ctx context.Context, q *sqlcgen.Queries, pack domain.LangPac
|
|||
return nil
|
||||
}
|
||||
|
||||
func langPackSeedStateKey(langPack, langCode string) string {
|
||||
return "langpack:v1:entry:" + langPack + ":" + langCode
|
||||
}
|
||||
|
||||
func langPackSeedCatalogStateKey(catalog string) string {
|
||||
return "langpack:v1:catalog:" + catalog
|
||||
}
|
||||
|
||||
func langPackSeedManifestStateKey(catalog string) string {
|
||||
return "langpack:v2:catalog:" + catalog
|
||||
}
|
||||
|
||||
func validateSeedEntry(entry domain.LangPackSeedEntry) error {
|
||||
if entry.SourceHash == "" || entry.ContentHash == "" || entry.StringsCount <= 0 {
|
||||
return fmt.Errorf("langpack %s/%s has incomplete seed metadata", entry.Pack.LangPack, entry.Pack.LangCode)
|
||||
}
|
||||
if entry.ContentLoaded && len(entry.Pack.Strings) != entry.StringsCount {
|
||||
return fmt.Errorf("langpack %s/%s loaded %d strings, want %d", entry.Pack.LangPack, entry.Pack.LangCode, len(entry.Pack.Strings), entry.StringsCount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedCatalogSnapshot(seed domain.LangPackSeed) domain.LangPackSeedCatalog {
|
||||
state := domain.LangPackSeedCatalog{
|
||||
Catalog: seed.Catalog,
|
||||
Scopes: append([]string(nil), seed.Scopes...),
|
||||
Packs: make([]domain.LangPackSeedCatalogEntry, 0, len(seed.Packs)),
|
||||
}
|
||||
for _, entry := range seed.Packs {
|
||||
state.Packs = append(state.Packs, domain.LangPackSeedCatalogEntry{
|
||||
LangPack: entry.Pack.LangPack,
|
||||
LangCode: entry.Pack.LangCode,
|
||||
Version: entry.Pack.Version,
|
||||
SourceHash: entry.SourceHash,
|
||||
ContentHash: entry.ContentHash,
|
||||
StringsCount: entry.StringsCount,
|
||||
})
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func invalidateLangPackSeedCatalogs(ctx context.Context, db sqlcgen.DBTX) error {
|
||||
if _, err := db.Exec(ctx, `DELETE FROM seed_states WHERE key LIKE 'langpack:v2:catalog:%'`); err != nil {
|
||||
return fmt.Errorf("invalidate langpack seed catalogs: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LangPackStore) meta(ctx context.Context, langPack, langCode string) (domain.LangPack, bool, error) {
|
||||
row, err := s.q.GetLangPackMeta(ctx, sqlcgen.GetLangPackMetaParams{
|
||||
LangPack: langPack,
|
||||
|
|
|
|||
164
internal/store/postgres/langpack_seed_integration_test.go
Normal file
164
internal/store/postgres/langpack_seed_integration_test.go
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
applangpack "telesrv/internal/app/langpack"
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestLangPackSeedReconciliationPostgres(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
packName := "seedtest-" + randomSuffix(t)
|
||||
store := NewLangPackStore(pool)
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM lang_pack_strings WHERE lang_pack = $1", packName)
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM lang_packs WHERE lang_pack = $1", packName)
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM seed_states WHERE key LIKE $1 OR key = $2 OR key = $3", "langpack:v1:entry:"+packName+":%", "langpack:v1:catalog:"+packName, "langpack:v2:catalog:"+packName)
|
||||
})
|
||||
|
||||
seedV1 := domain.LangPackSeed{
|
||||
Catalog: packName,
|
||||
Scopes: []string{packName},
|
||||
Packs: []domain.LangPackSeedEntry{{
|
||||
SourceHash: "source-v1",
|
||||
ContentHash: "hash-v1",
|
||||
StringsCount: 2,
|
||||
ContentLoaded: true,
|
||||
Pack: domain.LangPack{
|
||||
LangPack: packName,
|
||||
LangCode: "fr",
|
||||
Version: 1,
|
||||
Strings: []domain.LangPackString{
|
||||
{Key: "LanguageName", Value: "Français"},
|
||||
{Key: "old", Value: "old"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
written, err := store.ReconcileSeed(ctx, seedV1)
|
||||
if err != nil || written != 2 {
|
||||
t.Fatalf("reconcile v1 = %d, %v", written, err)
|
||||
}
|
||||
seedV1Unloaded := seedV1
|
||||
seedV1Unloaded.Packs = append([]domain.LangPackSeedEntry(nil), seedV1.Packs...)
|
||||
seedV1Unloaded.Packs[0].Pack.Strings = nil
|
||||
seedV1Unloaded.Packs[0].ContentLoaded = false
|
||||
if written, err := store.ReconcileSeed(ctx, seedV1Unloaded); err != nil || written != 0 {
|
||||
t.Fatalf("reconcile unchanged v1 = %d, %v", written, err)
|
||||
}
|
||||
catalog, err := store.GetSeedCatalog(ctx, packName)
|
||||
if err != nil || len(catalog.Packs) != 1 || catalog.Packs[0].SourceHash != "source-v1" {
|
||||
t.Fatalf("seed catalog = %+v, %v", catalog, err)
|
||||
}
|
||||
|
||||
seedV2 := domain.LangPackSeed{
|
||||
Catalog: packName,
|
||||
Scopes: []string{packName},
|
||||
Packs: []domain.LangPackSeedEntry{{
|
||||
SourceHash: "source-v2",
|
||||
ContentHash: "hash-v2",
|
||||
StringsCount: 2,
|
||||
ContentLoaded: true,
|
||||
Pack: domain.LangPack{
|
||||
LangPack: packName,
|
||||
LangCode: "fr",
|
||||
Version: 2,
|
||||
Strings: []domain.LangPackString{
|
||||
{Key: "LanguageName", Value: "Français v2"},
|
||||
{Key: "new", Value: "new"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
written, err = store.ReconcileSeed(ctx, seedV2)
|
||||
if err != nil || written != 2 {
|
||||
t.Fatalf("reconcile v2 = %d, %v", written, err)
|
||||
}
|
||||
pack, err := store.GetPack(ctx, packName, "fr", 0)
|
||||
if err != nil || pack.Version != 2 || len(pack.Strings) != 2 || postgresLangPackValue(pack.Strings, "old") != "" || postgresLangPackValue(pack.Strings, "new") != "new" {
|
||||
t.Fatalf("replaced postgres pack = %+v, err %v", pack, err)
|
||||
}
|
||||
|
||||
mutated := seedV2
|
||||
mutated.Packs = append([]domain.LangPackSeedEntry(nil), seedV2.Packs...)
|
||||
mutated.Packs[0].ContentHash = "changed-without-version"
|
||||
if _, err := store.ReconcileSeed(ctx, mutated); err == nil || !strings.Contains(err.Error(), "without version bump") {
|
||||
t.Fatalf("same-version mutation error = %v", err)
|
||||
}
|
||||
rollback := seedV2
|
||||
rollback.Packs = append([]domain.LangPackSeedEntry(nil), seedV2.Packs...)
|
||||
rollback.Packs[0].Pack.Version = 1
|
||||
rollback.Packs[0].ContentHash = "rollback"
|
||||
if _, err := store.ReconcileSeed(ctx, rollback); err == nil || !strings.Contains(err.Error(), "version rollback") {
|
||||
t.Fatalf("rollback error = %v", err)
|
||||
}
|
||||
|
||||
if written, err := store.ReconcileSeed(ctx, domain.LangPackSeed{Catalog: packName}); err != nil || written != 0 {
|
||||
t.Fatalf("remove missing language = %d, %v", written, err)
|
||||
}
|
||||
languages, err := store.ListLanguages(ctx, packName)
|
||||
if err != nil || len(languages) != 0 {
|
||||
t.Fatalf("languages after manifest removal = %+v, err %v", languages, err)
|
||||
}
|
||||
}
|
||||
|
||||
func postgresLangPackValue(items []domain.LangPackString, key string) string {
|
||||
for _, item := range items {
|
||||
if item.Key == key {
|
||||
return item.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestBundledLangPackSeedPostgres(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
scopes := []string{"android", "android_x", "ios", "macos", "tdesktop", "weba"}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM lang_pack_strings WHERE lang_pack = ANY($1::text[])", scopes)
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM lang_packs WHERE lang_pack = ANY($1::text[])", scopes)
|
||||
for _, scope := range scopes {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM seed_states WHERE key LIKE $1", "langpack:v1:entry:"+scope+":%")
|
||||
}
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM seed_states WHERE key = 'langpack:v1:catalog:langpack'")
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM seed_states WHERE key = 'langpack:v2:catalog:langpack'")
|
||||
})
|
||||
|
||||
service := applangpack.NewService(NewLangPackStore(pool))
|
||||
root := filepath.Join("..", "..", "..", "data", "langpack")
|
||||
started := time.Now()
|
||||
seeded, err := service.SeedDirectory(ctx, root)
|
||||
firstDuration := time.Since(started)
|
||||
if err != nil {
|
||||
t.Fatalf("seed bundled langpacks to postgres: %v", err)
|
||||
}
|
||||
if seeded < 50000 {
|
||||
t.Fatalf("seeded bundled strings = %d, want full catalog", seeded)
|
||||
}
|
||||
|
||||
started = time.Now()
|
||||
seededAgain, err := service.SeedDirectory(ctx, root)
|
||||
secondDuration := time.Since(started)
|
||||
if err != nil || seededAgain != 0 {
|
||||
t.Fatalf("reconcile unchanged bundled langpacks = %d, %v", seededAgain, err)
|
||||
}
|
||||
var metadataCount, activeCount int64
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT
|
||||
(SELECT COALESCE(sum(strings_count), 0) FROM lang_packs WHERE lang_pack = ANY($1::text[])),
|
||||
(SELECT count(*) FROM lang_pack_strings WHERE lang_pack = ANY($1::text[]) AND NOT deleted)
|
||||
`, scopes).Scan(&metadataCount, &activeCount); err != nil {
|
||||
t.Fatalf("count seeded langpack rows: %v", err)
|
||||
}
|
||||
if metadataCount != activeCount {
|
||||
t.Fatalf("seeded strings_count = %d, active rows = %d", metadataCount, activeCount)
|
||||
}
|
||||
t.Logf("bundled langpack seed: first=%s unchanged=%s strings=%d", firstDuration, secondDuration, metadataCount)
|
||||
}
|
||||
|
|
@ -30,6 +30,36 @@ ON CONFLICT (lang_pack, lang_code, key) DO UPDATE SET
|
|||
deleted = EXCLUDED.deleted,
|
||||
updated_at = now();
|
||||
|
||||
-- name: ListLangPackCodes :many
|
||||
SELECT lang_code
|
||||
FROM lang_packs
|
||||
WHERE lang_pack = $1
|
||||
ORDER BY lang_code;
|
||||
|
||||
-- name: DeleteLangPackStrings :exec
|
||||
DELETE FROM lang_pack_strings
|
||||
WHERE lang_pack = $1 AND lang_code = $2;
|
||||
|
||||
-- name: DeleteLangPackMeta :exec
|
||||
DELETE FROM lang_packs
|
||||
WHERE lang_pack = $1 AND lang_code = $2;
|
||||
|
||||
-- name: GetLangPackSeedHash :one
|
||||
SELECT content_hash
|
||||
FROM seed_states
|
||||
WHERE key = $1;
|
||||
|
||||
-- name: PutLangPackSeedHash :exec
|
||||
INSERT INTO seed_states (key, content_hash)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (key) DO UPDATE SET
|
||||
content_hash = EXCLUDED.content_hash,
|
||||
updated_at = now();
|
||||
|
||||
-- name: DeleteLangPackSeedHash :exec
|
||||
DELETE FROM seed_states
|
||||
WHERE key = $1;
|
||||
|
||||
-- name: ListLangPackStrings :many
|
||||
SELECT
|
||||
lang_pack, lang_code, key, version, pluralized, value,
|
||||
|
|
@ -45,3 +75,48 @@ SELECT
|
|||
FROM lang_pack_strings
|
||||
WHERE lang_pack = $1 AND lang_code = $2 AND key = ANY(sqlc.arg(keys)::text[]) AND NOT deleted
|
||||
ORDER BY key;
|
||||
|
||||
-- name: ListLangPackLanguages :many
|
||||
SELECT
|
||||
p.lang_pack,
|
||||
p.lang_code,
|
||||
p.version,
|
||||
p.strings_count,
|
||||
COALESCE(
|
||||
MAX(s.value) FILTER (WHERE s.key = 'LanguageNameInEnglish'),
|
||||
MAX(s.value) FILTER (WHERE s.key = 'Localization.EnglishLanguageName'),
|
||||
''
|
||||
)::text AS name,
|
||||
CASE
|
||||
WHEN p.lang_pack = 'android' THEN COALESCE(
|
||||
MAX(s.value) FILTER (WHERE s.key = 'TranslateLanguage' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
|
||||
MAX(s.value) FILTER (WHERE s.key = 'PassportLanguage_' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
|
||||
MAX(s.value) FILTER (WHERE s.key = 'LanguageName'),
|
||||
''
|
||||
)
|
||||
ELSE COALESCE(
|
||||
MAX(s.value) FILTER (WHERE s.key = 'lng_language_name'),
|
||||
MAX(s.value) FILTER (WHERE s.key = 'LanguageName'),
|
||||
MAX(s.value) FILTER (WHERE s.key = 'Localization.LanguageName'),
|
||||
MAX(s.value) FILTER (WHERE s.key = 'TranslateLanguage' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
|
||||
MAX(s.value) FILTER (WHERE s.key = 'PassportLanguage_' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
|
||||
''
|
||||
)
|
||||
END::text AS native_name
|
||||
FROM lang_packs p
|
||||
LEFT JOIN lang_pack_strings s
|
||||
ON s.lang_pack = p.lang_pack
|
||||
AND s.lang_code = p.lang_code
|
||||
AND s.key = ANY(ARRAY[
|
||||
'lng_language_name',
|
||||
'LanguageName',
|
||||
'LanguageNameInEnglish',
|
||||
'Localization.LanguageName',
|
||||
'Localization.EnglishLanguageName',
|
||||
'TranslateLanguage' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1)),
|
||||
'PassportLanguage_' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))
|
||||
]::text[])
|
||||
AND NOT s.deleted
|
||||
WHERE p.lang_pack = $1
|
||||
GROUP BY p.lang_pack, p.lang_code, p.version, p.strings_count
|
||||
ORDER BY p.lang_code;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,46 @@ import (
|
|||
"context"
|
||||
)
|
||||
|
||||
const deleteLangPackMeta = `-- name: DeleteLangPackMeta :exec
|
||||
DELETE FROM lang_packs
|
||||
WHERE lang_pack = $1 AND lang_code = $2
|
||||
`
|
||||
|
||||
type DeleteLangPackMetaParams struct {
|
||||
LangPack string
|
||||
LangCode string
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteLangPackMeta(ctx context.Context, arg DeleteLangPackMetaParams) error {
|
||||
_, err := q.db.Exec(ctx, deleteLangPackMeta, arg.LangPack, arg.LangCode)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteLangPackSeedHash = `-- name: DeleteLangPackSeedHash :exec
|
||||
DELETE FROM seed_states
|
||||
WHERE key = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteLangPackSeedHash(ctx context.Context, key string) error {
|
||||
_, err := q.db.Exec(ctx, deleteLangPackSeedHash, key)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteLangPackStrings = `-- name: DeleteLangPackStrings :exec
|
||||
DELETE FROM lang_pack_strings
|
||||
WHERE lang_pack = $1 AND lang_code = $2
|
||||
`
|
||||
|
||||
type DeleteLangPackStringsParams struct {
|
||||
LangPack string
|
||||
LangCode string
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteLangPackStrings(ctx context.Context, arg DeleteLangPackStringsParams) error {
|
||||
_, err := q.db.Exec(ctx, deleteLangPackStrings, arg.LangPack, arg.LangCode)
|
||||
return err
|
||||
}
|
||||
|
||||
const getLangPackMeta = `-- name: GetLangPackMeta :one
|
||||
SELECT lang_pack, lang_code, version, strings_count
|
||||
FROM lang_packs
|
||||
|
|
@ -39,6 +79,19 @@ func (q *Queries) GetLangPackMeta(ctx context.Context, arg GetLangPackMetaParams
|
|||
return i, err
|
||||
}
|
||||
|
||||
const getLangPackSeedHash = `-- name: GetLangPackSeedHash :one
|
||||
SELECT content_hash
|
||||
FROM seed_states
|
||||
WHERE key = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetLangPackSeedHash(ctx context.Context, key string) (string, error) {
|
||||
row := q.db.QueryRow(ctx, getLangPackSeedHash, key)
|
||||
var content_hash string
|
||||
err := row.Scan(&content_hash)
|
||||
return content_hash, err
|
||||
}
|
||||
|
||||
const getLangPackStringsByKeys = `-- name: GetLangPackStringsByKeys :many
|
||||
SELECT
|
||||
lang_pack, lang_code, key, version, pluralized, value,
|
||||
|
|
@ -104,6 +157,115 @@ func (q *Queries) GetLangPackStringsByKeys(ctx context.Context, arg GetLangPackS
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const listLangPackCodes = `-- name: ListLangPackCodes :many
|
||||
SELECT lang_code
|
||||
FROM lang_packs
|
||||
WHERE lang_pack = $1
|
||||
ORDER BY lang_code
|
||||
`
|
||||
|
||||
func (q *Queries) ListLangPackCodes(ctx context.Context, langPack string) ([]string, error) {
|
||||
rows, err := q.db.Query(ctx, listLangPackCodes, langPack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []string
|
||||
for rows.Next() {
|
||||
var lang_code string
|
||||
if err := rows.Scan(&lang_code); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, lang_code)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listLangPackLanguages = `-- name: ListLangPackLanguages :many
|
||||
SELECT
|
||||
p.lang_pack,
|
||||
p.lang_code,
|
||||
p.version,
|
||||
p.strings_count,
|
||||
COALESCE(
|
||||
MAX(s.value) FILTER (WHERE s.key = 'LanguageNameInEnglish'),
|
||||
MAX(s.value) FILTER (WHERE s.key = 'Localization.EnglishLanguageName'),
|
||||
''
|
||||
)::text AS name,
|
||||
CASE
|
||||
WHEN p.lang_pack = 'android' THEN COALESCE(
|
||||
MAX(s.value) FILTER (WHERE s.key = 'TranslateLanguage' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
|
||||
MAX(s.value) FILTER (WHERE s.key = 'PassportLanguage_' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
|
||||
MAX(s.value) FILTER (WHERE s.key = 'LanguageName'),
|
||||
''
|
||||
)
|
||||
ELSE COALESCE(
|
||||
MAX(s.value) FILTER (WHERE s.key = 'lng_language_name'),
|
||||
MAX(s.value) FILTER (WHERE s.key = 'LanguageName'),
|
||||
MAX(s.value) FILTER (WHERE s.key = 'Localization.LanguageName'),
|
||||
MAX(s.value) FILTER (WHERE s.key = 'TranslateLanguage' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
|
||||
MAX(s.value) FILTER (WHERE s.key = 'PassportLanguage_' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))),
|
||||
''
|
||||
)
|
||||
END::text AS native_name
|
||||
FROM lang_packs p
|
||||
LEFT JOIN lang_pack_strings s
|
||||
ON s.lang_pack = p.lang_pack
|
||||
AND s.lang_code = p.lang_code
|
||||
AND s.key = ANY(ARRAY[
|
||||
'lng_language_name',
|
||||
'LanguageName',
|
||||
'LanguageNameInEnglish',
|
||||
'Localization.LanguageName',
|
||||
'Localization.EnglishLanguageName',
|
||||
'TranslateLanguage' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1)),
|
||||
'PassportLanguage_' || upper(split_part(replace(p.lang_code, '-', '_'), '_', 1))
|
||||
]::text[])
|
||||
AND NOT s.deleted
|
||||
WHERE p.lang_pack = $1
|
||||
GROUP BY p.lang_pack, p.lang_code, p.version, p.strings_count
|
||||
ORDER BY p.lang_code
|
||||
`
|
||||
|
||||
type ListLangPackLanguagesRow struct {
|
||||
LangPack string
|
||||
LangCode string
|
||||
Version int32
|
||||
StringsCount int32
|
||||
Name string
|
||||
NativeName string
|
||||
}
|
||||
|
||||
func (q *Queries) ListLangPackLanguages(ctx context.Context, langPack string) ([]ListLangPackLanguagesRow, error) {
|
||||
rows, err := q.db.Query(ctx, listLangPackLanguages, langPack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListLangPackLanguagesRow
|
||||
for rows.Next() {
|
||||
var i ListLangPackLanguagesRow
|
||||
if err := rows.Scan(
|
||||
&i.LangPack,
|
||||
&i.LangCode,
|
||||
&i.Version,
|
||||
&i.StringsCount,
|
||||
&i.Name,
|
||||
&i.NativeName,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listLangPackStrings = `-- name: ListLangPackStrings :many
|
||||
SELECT
|
||||
lang_pack, lang_code, key, version, pluralized, value,
|
||||
|
|
@ -168,6 +330,24 @@ func (q *Queries) ListLangPackStrings(ctx context.Context, arg ListLangPackStrin
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const putLangPackSeedHash = `-- name: PutLangPackSeedHash :exec
|
||||
INSERT INTO seed_states (key, content_hash)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (key) DO UPDATE SET
|
||||
content_hash = EXCLUDED.content_hash,
|
||||
updated_at = now()
|
||||
`
|
||||
|
||||
type PutLangPackSeedHashParams struct {
|
||||
Key string
|
||||
ContentHash string
|
||||
}
|
||||
|
||||
func (q *Queries) PutLangPackSeedHash(ctx context.Context, arg PutLangPackSeedHashParams) error {
|
||||
_, err := q.db.Exec(ctx, putLangPackSeedHash, arg.Key, arg.ContentHash)
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertLangPackMeta = `-- name: UpsertLangPackMeta :exec
|
||||
INSERT INTO lang_packs (lang_pack, lang_code, version, strings_count)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue