summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorThomas Voss <mail@thomasvoss.com> 2025-07-05 11:12:37 +0200
committerThomas Voss <mail@thomasvoss.com> 2025-07-05 11:12:37 +0200
commit66ac5c365191d7515f7f796e95a754f6882cda8e (patch)
tree99de370773a33d21a6f8bf13c72b842ec0e782a6
parent5a12e3e229e112e6b316c9ee9f762e36f321271d (diff)
Code simplification
-rw-r--r--go.mod1
-rw-r--r--go.sum9
-rw-r--r--src/dbx/db.go232
-rw-r--r--src/dbx/mintages.go167
-rw-r--r--src/dbx/sql/000-genesis.sql26
-rw-r--r--src/dbx/users.go6
-rw-r--r--src/http.go8
-rw-r--r--src/templates.go12
-rw-r--r--src/templates/coins-mintages.html.tmpl19
9 files changed, 297 insertions, 183 deletions
diff --git a/go.mod b/go.mod
index ce85bca..27c5abf 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module git.thomasvoss.com/euro-cash.eu
go 1.24
require (
+ github.com/jmoiron/sqlx v1.4.0
github.com/mattn/go-sqlite3 v1.14.28
golang.org/x/crypto v0.39.0
golang.org/x/text v0.26.0
diff --git a/go.sum b/go.sum
index 513263d..105aca8 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,14 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
diff --git a/src/dbx/db.go b/src/dbx/db.go
index fcb345e..b8112b9 100644
--- a/src/dbx/db.go
+++ b/src/dbx/db.go
@@ -1,27 +1,24 @@
package dbx
import (
- "database/sql"
"fmt"
"io/fs"
"log"
- "reflect"
"sort"
- "strings"
"git.thomasvoss.com/euro-cash.eu/pkg/atexit"
. "git.thomasvoss.com/euro-cash.eu/pkg/try"
+ "github.com/jmoiron/sqlx"
"github.com/mattn/go-sqlite3"
)
var (
- db *sql.DB
+ db *sqlx.DB
DBName string
)
func Init(sqlDir fs.FS) {
- db = Try2(sql.Open("sqlite3", DBName))
- Try(db.Ping())
+ db = sqlx.MustConnect("sqlite3", DBName)
atexit.Register(Close)
Try(applyMigrations(sqlDir))
@@ -38,7 +35,7 @@ func Init(sqlDir fs.FS) {
Password: "420",
AdminP: false,
}))
- Try2(GetMintages("ad"))
+ Try2(GetMintages("ad", TypeCirc))
}
func Close() {
@@ -49,7 +46,7 @@ func applyMigrations(dir fs.FS) error {
var latest int
migratedp := true
- rows, err := db.Query("SELECT latest FROM migration")
+ err := db.QueryRow("SELECT latest FROM migration").Scan(&latest)
if err != nil {
e, ok := err.(sqlite3.Error)
/* IDK if there is a better way to do this… lol */
@@ -58,19 +55,9 @@ func applyMigrations(dir fs.FS) error {
} else {
return err
}
- } else {
- defer rows.Close()
}
- if migratedp {
- rows.Next()
- if err := rows.Err(); err != nil {
- return err
- }
- if err := rows.Scan(&latest); err != nil {
- return err
- }
- } else {
+ if !migratedp {
latest = -1
}
@@ -104,24 +91,31 @@ func applyMigrations(dir fs.FS) error {
return err
}
- if _, err := tx.Exec(string(qry)); err != nil {
- tx.Rollback()
- return fmt.Errorf("error in ‘%s’: %w", f, err)
+ var n int
+ if _, err = fmt.Sscanf(f, "%d", &n); err != nil {
+ goto error
}
- var n int
- if _, err := fmt.Sscanf(f, "%d", &n); err != nil {
- return err
+ if _, err = tx.Exec(string(qry)); err != nil {
+ err = fmt.Errorf("error in ‘%s’: %w", f, err)
+ goto error
}
+
_, err = tx.Exec("UPDATE migration SET latest = ? WHERE id = 1", n)
if err != nil {
- return err
+ goto error
}
- if err := tx.Commit(); err != nil {
- return err
+ if err = tx.Commit(); err != nil {
+ goto error
}
+
log.Printf("Applied database migration ‘%s’\n", f)
+ continue
+
+ error:
+ tx.Rollback()
+ return err
}
if last != "" {
@@ -138,96 +132,96 @@ func applyMigrations(dir fs.FS) error {
return nil
}
-func scanToStruct[T any](rs *sql.Rows) (T, error) {
- return scanToStruct2[T](rs, true)
-}
-
-func scanToStructs[T any](rs *sql.Rows) ([]T, error) {
- xs := []T{}
- for rs.Next() {
- x, err := scanToStruct2[T](rs, false)
- if err != nil {
- return nil, err
- }
- xs = append(xs, x)
- }
- return xs, rs.Err()
-}
-
-func scanToStruct2[T any](rs *sql.Rows, callNextP bool) (T, error) {
- var t, zero T
-
- cols, err := rs.Columns()
- if err != nil {
- return zero, err
- }
-
- v := reflect.ValueOf(&t).Elem()
- tType := v.Type()
-
- rawValues := make([]any, len(cols))
- for i := range rawValues {
- var zero any
- rawValues[i] = &zero
- }
-
- if callNextP {
- rs.Next()
- if err := rs.Err(); err != nil {
- return zero, err
- }
- }
- if err := rs.Scan(rawValues...); err != nil {
- return zero, err
- }
-
- /* col idx → [field idx, array idx] */
- arrayTargets := make(map[int][2]int)
- colToField := make(map[string]int)
-
- for i := 0; i < tType.NumField(); i++ {
- field := tType.Field(i)
- tag := field.Tag.Get("db")
- if tag == "" {
- continue
- }
-
- if strings.Contains(tag, ";") {
- dbcols := strings.Split(tag, ";")
- fv := v.Field(i)
- if fv.Kind() != reflect.Array {
- return zero, fmt.Errorf("field ‘%s’ is not array",
- field.Name)
- }
- if len(dbcols) != fv.Len() {
- return zero, fmt.Errorf("field ‘%s’ array length mismatch",
- field.Name)
- }
- for j, colName := range cols {
- for k, dbColName := range dbcols {
- if colName == dbColName {
- arrayTargets[j] = [2]int{i, k}
- }
- }
- }
- } else {
- colToField[tag] = i
- }
- }
-
- for i, col := range cols {
- vp := rawValues[i].(*any)
- if fieldIdx, ok := colToField[col]; ok {
- assignValue(v.Field(fieldIdx), *vp)
- } else if target, ok := arrayTargets[i]; ok {
- assignValue(v.Field(target[0]).Index(target[1]), *vp)
- }
- }
-
- return t, nil
-}
-
-func assignValue(fv reflect.Value, val any) {
+/* func scanToStruct[T any](rs *sql.Rows) (T, error) {
+ return scanToStruct2[T](rs, true)
+ }
+
+ func scanToStructs[T any](rs *sql.Rows) ([]T, error) {
+ xs := []T{}
+ for rs.Next() {
+ x, err := scanToStruct2[T](rs, false)
+ if err != nil {
+ return nil, err
+ }
+ xs = append(xs, x)
+ }
+ return xs, rs.Err()
+ }
+
+ func scanToStruct2[T any](rs *sql.Rows, callNextP bool) (T, error) {
+ var t, zero T
+
+ cols, err := rs.Columns()
+ if err != nil {
+ return zero, err
+ }
+
+ v := reflect.ValueOf(&t).Elem()
+ tType := v.Type()
+
+ rawValues := make([]any, len(cols))
+ for i := range rawValues {
+ var zero any
+ rawValues[i] = &zero
+ }
+
+ if callNextP {
+ rs.Next()
+ if err := rs.Err(); err != nil {
+ return zero, err
+ }
+ }
+ if err := rs.Scan(rawValues...); err != nil {
+ return zero, err
+ }
+
+ /\* col idx → [field idx, array idx] *\/
+ arrayTargets := make(map[int][2]int)
+ colToField := make(map[string]int)
+
+ for i := 0; i < tType.NumField(); i++ {
+ field := tType.Field(i)
+ tag := field.Tag.Get("db")
+ if tag == "" {
+ continue
+ }
+
+ if strings.Contains(tag, ";") {
+ dbcols := strings.Split(tag, ";")
+ fv := v.Field(i)
+ if fv.Kind() != reflect.Array {
+ return zero, fmt.Errorf("field ‘%s’ is not array",
+ field.Name)
+ }
+ if len(dbcols) != fv.Len() {
+ return zero, fmt.Errorf("field ‘%s’ array length mismatch",
+ field.Name)
+ }
+ for j, colName := range cols {
+ for k, dbColName := range dbcols {
+ if colName == dbColName {
+ arrayTargets[j] = [2]int{i, k}
+ }
+ }
+ }
+ } else {
+ colToField[tag] = i
+ }
+ }
+
+ for i, col := range cols {
+ vp := rawValues[i].(*any)
+ if fieldIdx, ok := colToField[col]; ok {
+ assignValue(v.Field(fieldIdx), *vp)
+ } else if target, ok := arrayTargets[i]; ok {
+ assignValue(v.Field(target[0]).Index(target[1]), *vp)
+ }
+ }
+
+ return t, nil
+ } */
+
+/* func assignValue(fv reflect.Value, val any) {
if val == nil {
fv.Set(reflect.Zero(fv.Type()))
return
@@ -236,4 +230,4 @@ func assignValue(fv reflect.Value, val any) {
if v.Type().ConvertibleTo(fv.Type()) {
fv.Set(v.Convert(fv.Type()))
}
-}
+} */
diff --git a/src/dbx/mintages.go b/src/dbx/mintages.go
index 4a6d5d3..d78e59c 100644
--- a/src/dbx/mintages.go
+++ b/src/dbx/mintages.go
@@ -1,31 +1,57 @@
package dbx
+import (
+ "database/sql"
+ "slices"
+)
+
type MintageData struct {
Standard []MSRow
Commemorative []MCRow
}
+type msRowInternal struct {
+ Country string
+ Type MintageType
+ Year int
+ Denomination float64
+ Mintmark sql.Null[string]
+ Mintage sql.Null[int]
+ Reference sql.Null[string]
+}
+
+type mcRowInternal struct {
+ Country string
+ Type MintageType
+ Year int
+ Name string
+ Number int
+ Mintmark sql.Null[string]
+ Mintage sql.Null[int]
+ Reference sql.Null[string]
+}
+
type MSRow struct {
- Type int `db:"type"`
- Year int `db:"year"`
- Mintmark string `db:"mintmark"`
- Mintages [ndenoms]int `db:"€0,01;€0,02;€0,05;€0,10;€0,20;€0,50;€1,00;€2,00"`
- Reference string `db:"reference"`
+ Year int
+ Mintmark string
+ Mintages [ndenoms]int
+ References []string
}
type MCRow struct {
- Type int `db:"type"`
- Year int `db:"year"`
- Name string `db:"name"`
- Number int `db:"number"`
- Mintmark string `db:"mintmark"`
- Mintage int `db:"mintage"`
- Reference string `db:"reference"`
+ Year int
+ Name string
+ Number int
+ Mintmark string
+ Mintage int
+ Reference string
}
+type MintageType int
+
/* DO NOT REORDER! */
const (
- TypeCirc = iota
+ TypeCirc MintageType = iota
TypeNifc
TypeProof
)
@@ -38,28 +64,119 @@ const (
const ndenoms = 8
-func GetMintages(country string) (MintageData, error) {
- var zero MintageData
+func NewMintageType(s string) MintageType {
+ switch s {
+ case "circ":
+ return TypeCirc
+ case "nifc":
+ return TypeNifc
+ case "proof":
+ return TypeProof
+ }
+ /* TODO: Handle this */
+ panic("TODO")
+}
- srows, err := db.Query(`SELECT * FROM mintages_s WHERE country = ?`, country)
+func GetMintages(country string, typ MintageType) (MintageData, error) {
+ var (
+ zero MintageData
+ xs []MSRow
+ ys []MCRow
+ )
+
+ rs, err := db.Queryx(`
+ SELECT * FROM mintages_s
+ WHERE country = ? AND type = ?
+ ORDER BY year, mintmark, denomination
+ `, country, typ)
if err != nil {
return zero, err
}
- defer srows.Close()
- xs, err := scanToStructs[MSRow](srows)
- if err != nil {
- return zero, err
+
+ for rs.Next() {
+ var x msRowInternal
+ if err = rs.StructScan(&x); err != nil {
+ return zero, err
+ }
+
+ loop:
+ msr := MSRow{
+ Year: x.Year,
+ Mintmark: sqlOr(x.Mintmark, ""),
+ References: make([]string, 0, ndenoms),
+ }
+ for i := range msr.Mintages {
+ msr.Mintages[i] = MintageUnknown
+ }
+ msr.Mintages[denomToIdx(x.Denomination)] =
+ sqlOr(x.Mintage, MintageUnknown)
+ if x.Reference.Valid {
+ msr.References = append(msr.References, x.Reference.V)
+ }
+
+ for rs.Next() {
+ var y msRowInternal
+ if err = rs.StructScan(&y); err != nil {
+ return zero, err
+ }
+
+ if x.Year != y.Year || x.Mintmark != y.Mintmark {
+ x = y
+ xs = append(xs, msr)
+ goto loop
+ }
+
+ msr.Mintages[denomToIdx(y.Denomination)] =
+ sqlOr(y.Mintage, MintageUnknown)
+ if y.Reference.Valid {
+ msr.References = append(msr.References, y.Reference.V)
+ }
+ }
+
+ xs = append(xs, msr)
}
- crows, err := db.Query(`SELECT * FROM mintages_c WHERE country = ?`, country)
- if err != nil {
+ if err = rs.Err(); err != nil {
return zero, err
}
- defer crows.Close()
- ys, err := scanToStructs[MCRow](crows)
+
+ rs, err = db.Queryx(`
+ SELECT * FROM mintages_c
+ WHERE country = ? AND type = ?
+ ORDER BY year, mintmark, number
+ `, country, typ)
if err != nil {
return zero, err
}
- return MintageData{xs, ys}, nil
+ for rs.Next() {
+ var y mcRowInternal
+ if err = rs.StructScan(&y); err != nil {
+ return zero, err
+ }
+ ys = append(ys, MCRow{
+ Year: y.Year,
+ Name: y.Name,
+ Number: y.Number,
+ Mintmark: sqlOr(y.Mintmark, ""),
+ Mintage: sqlOr(y.Mintage, MintageUnknown),
+ Reference: sqlOr(y.Reference, ""),
+ })
+ }
+
+ return MintageData{xs, ys}, rs.Err()
+}
+
+func sqlOr[T any](v sql.Null[T], dflt T) T {
+ if v.Valid {
+ return v.V
+ }
+ return dflt
+}
+
+func denomToIdx(d float64) int {
+ return slices.Index([]float64{
+ 0.01, 0.02, 0.05, 0.10,
+ 0.20, 0.50, 1.00, 2.00,
+ }, d)
}
diff --git a/src/dbx/sql/000-genesis.sql b/src/dbx/sql/000-genesis.sql
index 56ae7c3..c16c6ae 100644
--- a/src/dbx/sql/000-genesis.sql
+++ b/src/dbx/sql/000-genesis.sql
@@ -7,30 +7,26 @@ CREATE TABLE migration (
INSERT INTO migration (id, latest) VALUES (1, -1);
CREATE TABLE mintages_s (
- country CHAR(2) NOT NULL COLLATE BINARY
+ country CHAR(2) NOT NULL COLLATE BINARY
CHECK(length(country) = 2),
- type INTEGER NOT NULL -- Codes correspond to contants in mintages.go
+ -- Codes correspond to contants in mintages.go
+ type INTEGER NOT NULL
CHECK(type BETWEEN 0 AND 2),
- year INTEGER NOT NULL,
- mintmark TEXT,
- [€0,01] INTEGER,
- [€0,02] INTEGER,
- [€0,05] INTEGER,
- [€0,10] INTEGER,
- [€0,20] INTEGER,
- [€0,50] INTEGER,
- [€1,00] INTEGER,
- [€2,00] INTEGER,
- reference TEXT
+ year INTEGER NOT NULL,
+ denomination REAL NOT NULL,
+ mintmark TEXT,
+ mintage INTEGER,
+ reference TEXT
);
CREATE TABLE mintages_c (
country CHAR(2) NOT NULL COLLATE BINARY
CHECK(length(country) = 2),
- type INTEGER NOT NULL -- Codes correspond to contants in mintages.go
+ -- Codes correspond to contants in mintages.go
+ type INTEGER NOT NULL
CHECK(type BETWEEN 0 AND 2),
- name TEXT NOT NULL,
year INTEGER NOT NULL,
+ name TEXT NOT NULL,
number INTEGER NOT NULL,
mintmark TEXT,
mintage INTEGER,
diff --git a/src/dbx/users.go b/src/dbx/users.go
index e2270db..4235b28 100644
--- a/src/dbx/users.go
+++ b/src/dbx/users.go
@@ -44,13 +44,13 @@ func Login(username, password string) (User, error) {
password = norm.NFC.String(password)
/* TODO: Pass a context here? */
- rs, err := db.Query(`SELECT * FROM users WHERE username = ?`, username)
+ rs, err := db.Queryx(`SELECT * FROM users WHERE username = ?`, username)
if err != nil {
return User{}, err
}
- u, err := scanToStruct[User](rs)
- switch {
+ var u User
+ switch err = rs.Scan(&u); {
case errors.Is(err, sql.ErrNoRows):
return User{}, LoginFailed
case err != nil:
diff --git a/src/http.go b/src/http.go
index 6feb865..4a56430 100644
--- a/src/http.go
+++ b/src/http.go
@@ -139,13 +139,13 @@ func mintageHandler(next http.Handler) http.Handler {
}
var err error
- td.Mintages, err = dbx.GetMintages(td.Code)
+ td.Mintages, err = dbx.GetMintages(td.Code, dbx.NewMintageType(td.Type))
if err != nil {
throwError(http.StatusInternalServerError, err, w, r)
return
}
- processMintages(&td.Mintages, td.Type)
+ /* processMintages(&td.Mintages, td.Type) */
next.ServeHTTP(w, r)
})
}
@@ -191,7 +191,7 @@ func throwError(status int, err error, w http.ResponseWriter, r *http.Request) {
})
}
-func processMintages(md *dbx.MintageData, typeStr string) {
+/* func processMintages(md *dbx.MintageData, typeStr string) {
var typ int
switch typeStr {
case "nifc":
@@ -221,4 +221,4 @@ func processMintages(md *dbx.MintageData, typeStr string) {
}
return strings.Compare(x.Mintmark, y.Mintmark)
})
-}
+} */
diff --git a/src/templates.go b/src/templates.go
index 4deeb67..5ef0293 100644
--- a/src/templates.go
+++ b/src/templates.go
@@ -25,7 +25,6 @@ var (
errorTmpl *template.Template
templates map[string]*template.Template
funcmap = map[string]any{
- "denoms": denoms,
"locales": locales,
"safe": asHTML,
"sprintf": fmt.Sprintf,
@@ -79,13 +78,6 @@ func asHTML(s string) template.HTML {
return template.HTML(s)
}
-func denoms() [8]float64 {
- return [8]float64{
- 0.01, 0.02, 0.05, 0.10,
- 0.20, 0.50, 1.00, 2.00,
- }
-}
-
func locales() []locale {
return Locales[:]
}
@@ -97,3 +89,7 @@ func templateMakeTuple(args ...any) []any {
func (td templateData) T(fmt string, args ...any) string {
return td.Printer.T(fmt, args...)
}
+
+func (td templateData) M(n float64) string {
+ return td.Printer.M(n)
+}
diff --git a/src/templates/coins-mintages.html.tmpl b/src/templates/coins-mintages.html.tmpl
index 772db33..1cf7c70 100644
--- a/src/templates/coins-mintages.html.tmpl
+++ b/src/templates/coins-mintages.html.tmpl
@@ -71,15 +71,17 @@
<table class="mintage-table" role="grid">
<thead>
<th>{{ .T "Year" }}</th>
- {{ with $p := .Printer }}
- {{ range denoms }}
- <th>{{ $p.M . }}</th>
- {{ end }}
- {{ end }}
+ <th>{{ .M 0.01 }}</th>
+ <th>{{ .M 0.02 }}</th>
+ <th>{{ .M 0.05 }}</th>
+ <th>{{ .M 0.10 }}</th>
+ <th>{{ .M 0.20 }}</th>
+ <th>{{ .M 0.50 }}</th>
+ <th>{{ .M 1.00 }}</th>
+ <th>{{ .M 2.00 }}</th>
</thead>
<tbody>
{{ $p := .Printer }}
- {{ $type := .Type }}
{{ range .Mintages.Standard }}
<tr>
<th scope="col">
@@ -116,7 +118,6 @@
</thead>
<tbody>
{{ $p := .Printer }}
- {{ $type := .Type }}
{{ range .Mintages.Commemorative }}
<tr>
<th scope="col">
@@ -149,9 +150,9 @@
{{ end }}
{{ define "coin-type-radio" }}
-<label for=compact-{{ index . 1 }}>
+<label for={{ index . 1 }}>
<input
- id=compact-{{ index . 1 }}
+ id={{ index . 1 }}
name="type"
type="radio"
value={{ index . 1 }}