20260629154839
This commit is contained in:
1 parent
e687846eb4
commit
bb596f3245
41 files changed
+5735
-86
No files matched your search
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"allapp-go/internal/bootstrap/boots"
|
||||
"allapp-go/internal/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: go run ./cmd/apply_sql <sql-file>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
sqlPath, err := filepath.Abs(os.Args[1])
|
||||
if err != nil {
|
||||
exitErr("resolve sql path", err)
|
||||
}
|
||||
|
||||
sqlBytes, err := os.ReadFile(sqlPath)
|
||||
if err != nil {
|
||||
exitErr("read sql file", err)
|
||||
}
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
exitErr("load config", err)
|
||||
}
|
||||
|
||||
pool, err := boots.InitPgsql(cfg)
|
||||
if err != nil {
|
||||
exitErr("connect postgres", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if _, err := pool.Exec(ctx, string(sqlBytes)); err != nil {
|
||||
exitErr("execute sql", err)
|
||||
}
|
||||
|
||||
fmt.Printf("applied sql: %s\n", sqlPath)
|
||||
}
|
||||
|
||||
func exitErr(step string, err error) {
|
||||
fmt.Fprintf(os.Stderr, "%s failed: %v\n", step, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"allapp-go/internal/bootstrap/boots"
|
||||
"allapp-go/internal/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
exitErr("load config", err)
|
||||
}
|
||||
|
||||
pool, err := boots.InitPgsql(cfg)
|
||||
if err != nil {
|
||||
exitErr("connect postgres", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rows, err := pool.Query(ctx, `
|
||||
select table_name
|
||||
from information_schema.tables
|
||||
where table_schema = 'public'
|
||||
and table_name like 'b_clothing_%'
|
||||
order by table_name
|
||||
`)
|
||||
if err != nil {
|
||||
exitErr("query tables", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
var tableName string
|
||||
if err := rows.Scan(&tableName); err != nil {
|
||||
exitErr("scan table", err)
|
||||
}
|
||||
count++
|
||||
fmt.Println(tableName)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
exitErr("read rows", err)
|
||||
}
|
||||
|
||||
fmt.Printf("total=%d\n", count)
|
||||
}
|
||||
|
||||
func exitErr(step string, err error) {
|
||||
fmt.Fprintf(os.Stderr, "%s failed: %v\n", step, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"allapp-go/internal/bootstrap/boots"
|
||||
"allapp-go/internal/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
exitErr("load config", err)
|
||||
}
|
||||
|
||||
pool, err := boots.InitPgsql(cfg)
|
||||
if err != nil {
|
||||
exitErr("connect postgres", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rows, err := pool.Query(ctx, `
|
||||
select table_name, column_name
|
||||
from information_schema.columns
|
||||
where table_schema = 'public'
|
||||
and table_name in (
|
||||
'b_finance_category_default',
|
||||
'b_finance_category',
|
||||
'b_clothing_clothes',
|
||||
'b_clothing_outfit'
|
||||
)
|
||||
and column_name in ('icon_mime', 'image_mime', 'cover_mime')
|
||||
order by table_name, column_name
|
||||
`)
|
||||
if err != nil {
|
||||
exitErr("query columns", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
var tableName string
|
||||
var columnName string
|
||||
if err := rows.Scan(&tableName, &columnName); err != nil {
|
||||
exitErr("scan column", err)
|
||||
}
|
||||
count++
|
||||
fmt.Printf("%s.%s\n", tableName, columnName)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
exitErr("read rows", err)
|
||||
}
|
||||
|
||||
fmt.Printf("removed_mime_columns_remaining=%d\n", count)
|
||||
}
|
||||
|
||||
func exitErr(step string, err error) {
|
||||
fmt.Fprintf(os.Stderr, "%s failed: %v\n", step, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"allapp-go/internal/bootstrap/boots"
|
||||
"allapp-go/internal/config"
|
||||
"allapp-go/pkg/s3store"
|
||||
"allapp-go/pkg/uniqueid"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type iconManifestItem struct {
|
||||
Name string `json:"name"`
|
||||
Type int16 `json:"type"`
|
||||
TypeName string `json:"typeName"`
|
||||
Source string `json:"source"`
|
||||
File string `json:"file"`
|
||||
Key string `json:"key"`
|
||||
Mime string `json:"mime"`
|
||||
SortNumber int `json:"sortNumber"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
exitErr("load config", err)
|
||||
}
|
||||
|
||||
boots.InitUniqueId(cfg)
|
||||
s3store.NewS3Client(cfg)
|
||||
|
||||
pool, err := boots.InitPgsql(cfg)
|
||||
if err != nil {
|
||||
exitErr("connect postgres", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
workspaceRoot, err := filepath.Abs("..")
|
||||
if err != nil {
|
||||
exitErr("resolve workspace", err)
|
||||
}
|
||||
|
||||
manifestPath := filepath.Join(workspaceRoot, "tools", "finance-category-icons", "dist", "manifest.json")
|
||||
manifestBytes, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
exitErr("read manifest", err)
|
||||
}
|
||||
|
||||
var items []iconManifestItem
|
||||
if err := json.Unmarshal(manifestBytes, &items); err != nil {
|
||||
exitErr("parse manifest", err)
|
||||
}
|
||||
if len(items) == 0 {
|
||||
exitErr("validate manifest", fmt.Errorf("manifest is empty"))
|
||||
}
|
||||
|
||||
tableName, err := resolveDefaultCategoryTable(ctx, pool)
|
||||
if err != nil {
|
||||
exitErr("resolve table", err)
|
||||
}
|
||||
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
exitErr("begin transaction", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback(ctx)
|
||||
}()
|
||||
|
||||
uploaded := 0
|
||||
inserted := 0
|
||||
updated := 0
|
||||
|
||||
for _, item := range items {
|
||||
if err := validateItem(item); err != nil {
|
||||
exitErr("validate item", err)
|
||||
}
|
||||
|
||||
filePath := filepath.Join(workspaceRoot, filepath.FromSlash(item.File))
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
exitErr("read converted icon", err)
|
||||
}
|
||||
|
||||
if err := s3store.UploadToRustFS(ctx, bytes.NewReader(data), item.Key, int64(len(data)), item.Mime); err != nil {
|
||||
exitErr("upload "+item.Key, err)
|
||||
}
|
||||
uploaded++
|
||||
|
||||
existingID, found, err := findExistingCategory(ctx, tx, tableName, item.Name, item.Type)
|
||||
if err != nil {
|
||||
exitErr("query existing category", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if found {
|
||||
if err := updateCategory(ctx, tx, tableName, existingID, item, now); err != nil {
|
||||
exitErr("update category", err)
|
||||
}
|
||||
updated++
|
||||
continue
|
||||
}
|
||||
|
||||
if err := insertCategory(ctx, tx, tableName, uniqueid.NextId(), item, now); err != nil {
|
||||
exitErr("insert category", err)
|
||||
}
|
||||
inserted++
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
exitErr("commit transaction", err)
|
||||
}
|
||||
|
||||
expenseCount, incomeCount, err := countImportedCategories(ctx, pool, tableName)
|
||||
if err != nil {
|
||||
exitErr("verify imported categories", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Imported finance category icons successfully.\n")
|
||||
fmt.Printf("table=%s uploaded=%d inserted=%d updated=%d total=%d\n", tableName, uploaded, inserted, updated, len(items))
|
||||
fmt.Printf("verified prefix=finance-category-default/ expense=%d income=%d total=%d\n", expenseCount, incomeCount, expenseCount+incomeCount)
|
||||
for _, item := range items {
|
||||
fmt.Printf("%s type=%d sort=%d key=%s\n", item.Name, item.Type, item.SortNumber, item.Key)
|
||||
}
|
||||
}
|
||||
|
||||
func validateItem(item iconManifestItem) error {
|
||||
if item.Name == "" || item.Key == "" || item.File == "" || item.Mime != "image/webp" {
|
||||
return fmt.Errorf("invalid manifest item: %+v", item)
|
||||
}
|
||||
if item.Type != 0 && item.Type != 1 {
|
||||
return fmt.Errorf("invalid type for %s: %d", item.Name, item.Type)
|
||||
}
|
||||
if item.Width != 256 || item.Height != 256 {
|
||||
return fmt.Errorf("invalid dimensions for %s: %dx%d", item.Name, item.Width, item.Height)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveDefaultCategoryTable(ctx context.Context, q pgxQueryer) (string, error) {
|
||||
candidates := []string{"finance_category_default", "b_finance_category_default"}
|
||||
for _, table := range candidates {
|
||||
var exists bool
|
||||
if err := q.QueryRow(ctx, "select to_regclass($1) is not null", table).Scan(&exists); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if exists {
|
||||
return table, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("neither finance_category_default nor b_finance_category_default exists")
|
||||
}
|
||||
|
||||
func findExistingCategory(ctx context.Context, tx pgx.Tx, tableName string, name string, categoryType int16) (int64, bool, error) {
|
||||
query := fmt.Sprintf("select id from %s where name=$1 and type=$2 order by id limit 1", safeTableName(tableName))
|
||||
var id int64
|
||||
err := tx.QueryRow(ctx, query, name, categoryType).Scan(&id)
|
||||
if err == nil {
|
||||
return id, true, nil
|
||||
}
|
||||
if err == pgx.ErrNoRows {
|
||||
return 0, false, nil
|
||||
}
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
func updateCategory(ctx context.Context, tx pgx.Tx, tableName string, id int64, item iconManifestItem, now time.Time) error {
|
||||
query := fmt.Sprintf(`
|
||||
update %s
|
||||
set icon_file_key=$1, sort_number=$2, status=1, update_time=$3
|
||||
where id=$4
|
||||
`, safeTableName(tableName))
|
||||
_, err := tx.Exec(ctx, query, item.Key, item.SortNumber, now, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func insertCategory(ctx context.Context, tx pgx.Tx, tableName string, id int64, item iconManifestItem, now time.Time) error {
|
||||
query := fmt.Sprintf(`
|
||||
insert into %s (id, name, type, icon_file_key, sort_number, status, create_time, update_time)
|
||||
values ($1, $2, $3, $4, $5, 1, $6, $6)
|
||||
`, safeTableName(tableName))
|
||||
_, err := tx.Exec(ctx, query, id, item.Name, item.Type, item.Key, item.SortNumber, now)
|
||||
return err
|
||||
}
|
||||
|
||||
func countImportedCategories(ctx context.Context, q pgxQueryer, tableName string) (int, int, error) {
|
||||
query := fmt.Sprintf(`
|
||||
select
|
||||
count(*) filter (where type = 0),
|
||||
count(*) filter (where type = 1)
|
||||
from %s
|
||||
where icon_file_key like 'finance-category-default/%%'
|
||||
and status = 1
|
||||
`, safeTableName(tableName))
|
||||
|
||||
var expenseCount int
|
||||
var incomeCount int
|
||||
if err := q.QueryRow(ctx, query).Scan(&expenseCount, &incomeCount); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return expenseCount, incomeCount, nil
|
||||
}
|
||||
|
||||
func safeTableName(tableName string) string {
|
||||
switch tableName {
|
||||
case "finance_category_default", "b_finance_category_default":
|
||||
return tableName
|
||||
default:
|
||||
panic("unsafe table name: " + tableName)
|
||||
}
|
||||
}
|
||||
|
||||
type pgxQueryer interface {
|
||||
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
|
||||
}
|
||||
|
||||
func exitErr(step string, err error) {
|
||||
fmt.Fprintf(os.Stderr, "%s failed: %v\n", step, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
Reference in new issue
Block a user