first pass migrate back to sqlite
This commit is contained in:
parent
68ec8ce5ea
commit
412baeb46d
142
data/fights.json
142
data/fights.json
@ -962,5 +962,147 @@
|
|||||||
],
|
],
|
||||||
"created": 1755789095,
|
"created": 1755789095,
|
||||||
"updated": 1755789098
|
"updated": 1755789098
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"user_id": 1,
|
||||||
|
"monster_id": 7,
|
||||||
|
"monster_hp": 0,
|
||||||
|
"monster_max_hp": 12,
|
||||||
|
"monster_sleep": 0,
|
||||||
|
"monster_immune": 1,
|
||||||
|
"uber_damage": 0,
|
||||||
|
"uber_defense": 0,
|
||||||
|
"first_strike": false,
|
||||||
|
"turn": 6,
|
||||||
|
"ran_away": false,
|
||||||
|
"victory": true,
|
||||||
|
"won": true,
|
||||||
|
"reward_gold": 3,
|
||||||
|
"reward_exp": 9,
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"t": 1,
|
||||||
|
"d": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8,
|
||||||
|
"d": 1,
|
||||||
|
"n": "Shade"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 1,
|
||||||
|
"d": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8,
|
||||||
|
"d": 1,
|
||||||
|
"n": "Shade"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 1,
|
||||||
|
"d": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8,
|
||||||
|
"d": 1,
|
||||||
|
"n": "Shade"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 1,
|
||||||
|
"d": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8,
|
||||||
|
"d": 1,
|
||||||
|
"n": "Shade"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 1,
|
||||||
|
"d": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8,
|
||||||
|
"d": 1,
|
||||||
|
"n": "Shade"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 1,
|
||||||
|
"d": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 11,
|
||||||
|
"n": "Shade"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"created": 1755806329,
|
||||||
|
"updated": 1755806335
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18,
|
||||||
|
"user_id": 1,
|
||||||
|
"monster_id": 4,
|
||||||
|
"monster_hp": 4,
|
||||||
|
"monster_max_hp": 10,
|
||||||
|
"monster_sleep": 0,
|
||||||
|
"monster_immune": 0,
|
||||||
|
"uber_damage": 0,
|
||||||
|
"uber_defense": 0,
|
||||||
|
"first_strike": false,
|
||||||
|
"turn": 5,
|
||||||
|
"ran_away": false,
|
||||||
|
"victory": true,
|
||||||
|
"won": false,
|
||||||
|
"reward_gold": 0,
|
||||||
|
"reward_exp": 0,
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"t": 1,
|
||||||
|
"d": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8,
|
||||||
|
"d": 1,
|
||||||
|
"n": "Creature"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 1,
|
||||||
|
"d": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8,
|
||||||
|
"d": 1,
|
||||||
|
"n": "Creature"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 1,
|
||||||
|
"d": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8,
|
||||||
|
"d": 1,
|
||||||
|
"n": "Creature"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 7,
|
||||||
|
"n": "You failed to run away!"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8,
|
||||||
|
"d": 1,
|
||||||
|
"n": "Creature"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 7,
|
||||||
|
"n": "You failed to run away!"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 8,
|
||||||
|
"d": 1,
|
||||||
|
"n": "Creature"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"created": 1755806697,
|
||||||
|
"updated": 1755806703
|
||||||
}
|
}
|
||||||
]
|
]
|
11
go.mod
11
go.mod
@ -10,8 +10,19 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
golang.org/x/crypto v0.41.0 // indirect
|
golang.org/x/crypto v0.41.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
modernc.org/libc v1.65.7 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
modernc.org/sqlite v1.37.1 // indirect
|
||||||
|
zombiezen.com/go/sqlite v1.4.2 // indirect
|
||||||
)
|
)
|
||||||
|
23
go.sum
23
go.sum
@ -4,8 +4,18 @@ git.sharkk.net/Sharkk/Sushi v1.1.1 h1:ynU16l6vAhY/JUwHlI4zMQiPuL9lcs88W/mAGZsL4R
|
|||||||
git.sharkk.net/Sharkk/Sushi v1.1.1/go.mod h1:S84ACGkuZ+BKzBO4lb5WQnm5aw9+l7VSO2T1bjzxL3o=
|
git.sharkk.net/Sharkk/Sushi v1.1.1/go.mod h1:S84ACGkuZ+BKzBO4lb5WQnm5aw9+l7VSO2T1bjzxL3o=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
|
github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
|
||||||
@ -14,5 +24,18 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
|
|||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||||
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
||||||
|
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
|
||||||
|
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
|
||||||
|
zombiezen.com/go/sqlite v1.4.2 h1:KZXLrBuJ7tKNEm+VJcApLMeQbhmAUOKA5VWS93DfFRo=
|
||||||
|
zombiezen.com/go/sqlite v1.4.2/go.mod h1:5Kd4taTAD4MkBzT25mQ9uaAlLjyR0rFhsR6iINO70jc=
|
||||||
|
351
internal/database/wrapper.go
Normal file
351
internal/database/wrapper.go
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"zombiezen.com/go/sqlite"
|
||||||
|
"zombiezen.com/go/sqlite/sqlitex"
|
||||||
|
)
|
||||||
|
|
||||||
|
var placeholderRegex = regexp.MustCompile(`%[sd]`)
|
||||||
|
|
||||||
|
var db *sqlite.Conn
|
||||||
|
|
||||||
|
// Init initializes the database connection with WAL mode and cache settings
|
||||||
|
func Init(dbPath string) error {
|
||||||
|
conn, err := sqlite.OpenConn(dbPath, sqlite.OpenReadWrite|sqlite.OpenCreate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable WAL mode
|
||||||
|
if err := sqlitex.Execute(conn, "PRAGMA journal_mode=WAL", nil); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return fmt.Errorf("failed to enable WAL mode: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set generous cache size (64MB)
|
||||||
|
if err := sqlitex.Execute(conn, "PRAGMA cache_size=-65536", nil); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return fmt.Errorf("failed to set cache size: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable foreign keys
|
||||||
|
if err := sqlitex.Execute(conn, "PRAGMA foreign_keys=ON", nil); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return fmt.Errorf("failed to enable foreign keys: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db = conn
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DB returns the global database connection
|
||||||
|
func DB() *sqlite.Conn {
|
||||||
|
if db == nil {
|
||||||
|
panic("database not initialized - call Init() first")
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan scans a SQLite statement result into a struct using field names
|
||||||
|
func Scan(stmt *sqlite.Stmt, dest any) error {
|
||||||
|
v := reflect.ValueOf(dest)
|
||||||
|
if v.Kind() != reflect.Pointer || v.Elem().Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("dest must be a pointer to struct")
|
||||||
|
}
|
||||||
|
|
||||||
|
elem := v.Elem()
|
||||||
|
typ := elem.Type()
|
||||||
|
|
||||||
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
columnName := toSnakeCase(field.Name)
|
||||||
|
|
||||||
|
fieldValue := elem.Field(i)
|
||||||
|
if !fieldValue.CanSet() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find column index by name
|
||||||
|
colIndex := -1
|
||||||
|
for j := 0; j < stmt.ColumnCount(); j++ {
|
||||||
|
if stmt.ColumnName(j) == columnName {
|
||||||
|
colIndex = j
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if colIndex == -1 {
|
||||||
|
continue // Column not found
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fieldValue.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
fieldValue.SetInt(stmt.ColumnInt64(colIndex))
|
||||||
|
case reflect.String:
|
||||||
|
fieldValue.SetString(stmt.ColumnText(colIndex))
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
fieldValue.SetFloat(stmt.ColumnFloat(colIndex))
|
||||||
|
case reflect.Bool:
|
||||||
|
fieldValue.SetBool(stmt.ColumnInt(colIndex) != 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query executes a query with fmt-style placeholders and automatically binds parameters
|
||||||
|
func Query(query string, args ...any) (*sqlite.Stmt, error) {
|
||||||
|
// Replace fmt placeholders with SQLite placeholders
|
||||||
|
convertedQuery, paramTypes := convertPlaceholders(query)
|
||||||
|
|
||||||
|
stmt, err := DB().Prepare(convertedQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind parameters with correct types
|
||||||
|
for i, arg := range args {
|
||||||
|
if i >= len(paramTypes) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
switch paramTypes[i] {
|
||||||
|
case "s": // string
|
||||||
|
if s, ok := arg.(string); ok {
|
||||||
|
stmt.BindText(i+1, s)
|
||||||
|
} else {
|
||||||
|
stmt.BindText(i+1, fmt.Sprintf("%v", arg))
|
||||||
|
}
|
||||||
|
case "d": // integer
|
||||||
|
switch v := arg.(type) {
|
||||||
|
case int:
|
||||||
|
stmt.BindInt64(i+1, int64(v))
|
||||||
|
case int32:
|
||||||
|
stmt.BindInt64(i+1, int64(v))
|
||||||
|
case int64:
|
||||||
|
stmt.BindInt64(i+1, v)
|
||||||
|
case float64:
|
||||||
|
stmt.BindInt64(i+1, int64(v))
|
||||||
|
default:
|
||||||
|
if i64, err := strconv.ParseInt(fmt.Sprintf("%v", arg), 10, 64); err == nil {
|
||||||
|
stmt.BindInt64(i+1, i64)
|
||||||
|
} else {
|
||||||
|
stmt.BindInt64(i+1, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stmt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get executes a query and returns the first row
|
||||||
|
func Get(dest any, query string, args ...any) error {
|
||||||
|
stmt, err := Query(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Finalize()
|
||||||
|
|
||||||
|
hasRow, err := stmt.Step()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !hasRow {
|
||||||
|
return fmt.Errorf("no rows found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scan(stmt, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select executes a query and scans all rows into a slice
|
||||||
|
func Select(dest any, query string, args ...any) error {
|
||||||
|
destValue := reflect.ValueOf(dest)
|
||||||
|
if destValue.Kind() != reflect.Ptr || destValue.Elem().Kind() != reflect.Slice {
|
||||||
|
return fmt.Errorf("dest must be a pointer to slice")
|
||||||
|
}
|
||||||
|
|
||||||
|
sliceValue := destValue.Elem()
|
||||||
|
elemType := sliceValue.Type().Elem()
|
||||||
|
|
||||||
|
// Ensure element type is a pointer to struct
|
||||||
|
if elemType.Kind() != reflect.Ptr || elemType.Elem().Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("slice elements must be pointers to structs")
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, err := Query(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stmt.Finalize()
|
||||||
|
|
||||||
|
for {
|
||||||
|
hasRow, err := stmt.Step()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !hasRow {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new instance of the element type
|
||||||
|
newElem := reflect.New(elemType.Elem())
|
||||||
|
if err := Scan(stmt, newElem.Interface()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sliceValue.Set(reflect.Append(sliceValue, newElem))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec executes a statement with fmt-style placeholders
|
||||||
|
func Exec(query string, args ...any) error {
|
||||||
|
convertedQuery, paramTypes := convertPlaceholders(query)
|
||||||
|
|
||||||
|
sqlArgs := make([]any, len(args))
|
||||||
|
for i, arg := range args {
|
||||||
|
if i < len(paramTypes) && paramTypes[i] == "d" {
|
||||||
|
// Convert to int64 for integer parameters
|
||||||
|
switch v := arg.(type) {
|
||||||
|
case int:
|
||||||
|
sqlArgs[i] = int64(v)
|
||||||
|
case int32:
|
||||||
|
sqlArgs[i] = int64(v)
|
||||||
|
case int64:
|
||||||
|
sqlArgs[i] = v
|
||||||
|
default:
|
||||||
|
sqlArgs[i] = arg
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sqlArgs[i] = arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sqlitex.Execute(DB(), convertedQuery, &sqlitex.ExecOptions{
|
||||||
|
Args: sqlArgs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates specific fields in the database
|
||||||
|
func Update(tableName string, fields map[string]any, whereField string, whereValue any) error {
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return nil // No changes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build UPDATE query
|
||||||
|
setParts := make([]string, 0, len(fields))
|
||||||
|
args := make([]any, 0, len(fields)+1)
|
||||||
|
|
||||||
|
for field, value := range fields {
|
||||||
|
setParts = append(setParts, field+" = ?")
|
||||||
|
args = append(args, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, whereValue)
|
||||||
|
|
||||||
|
query := fmt.Sprintf("UPDATE %s SET %s WHERE %s = ?",
|
||||||
|
tableName, strings.Join(setParts, ", "), whereField)
|
||||||
|
|
||||||
|
return sqlitex.Execute(DB(), query, &sqlitex.ExecOptions{
|
||||||
|
Args: args,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert inserts a struct into the database
|
||||||
|
func Insert(tableName string, obj any, excludeFields ...string) (int64, error) {
|
||||||
|
v := reflect.ValueOf(obj)
|
||||||
|
if v.Kind() == reflect.Pointer {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
t := v.Type()
|
||||||
|
|
||||||
|
exclude := make(map[string]bool)
|
||||||
|
for _, field := range excludeFields {
|
||||||
|
exclude[toSnakeCase(field)] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var columns []string
|
||||||
|
var placeholders []string
|
||||||
|
var args []any
|
||||||
|
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
columnName := toSnakeCase(field.Name)
|
||||||
|
if exclude[columnName] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
columns = append(columns, columnName)
|
||||||
|
placeholders = append(placeholders, "?")
|
||||||
|
args = append(args, v.Field(i).Interface())
|
||||||
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
|
||||||
|
tableName, strings.Join(columns, ", "), strings.Join(placeholders, ", "))
|
||||||
|
|
||||||
|
stmt, err := DB().Prepare(query)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer stmt.Finalize()
|
||||||
|
|
||||||
|
// Bind parameters
|
||||||
|
for i, arg := range args {
|
||||||
|
switch v := arg.(type) {
|
||||||
|
case string:
|
||||||
|
stmt.BindText(i+1, v)
|
||||||
|
case int, int32, int64:
|
||||||
|
stmt.BindInt64(i+1, reflect.ValueOf(v).Int())
|
||||||
|
case float32, float64:
|
||||||
|
stmt.BindFloat(i+1, reflect.ValueOf(v).Float())
|
||||||
|
default:
|
||||||
|
stmt.BindText(i+1, fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = stmt.Step()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB().LastInsertRowID(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertPlaceholders(query string) (string, []string) {
|
||||||
|
var paramTypes []string
|
||||||
|
|
||||||
|
convertedQuery := placeholderRegex.ReplaceAllStringFunc(query, func(match string) string {
|
||||||
|
paramTypes = append(paramTypes, match[1:]) // Remove % prefix
|
||||||
|
return "?"
|
||||||
|
})
|
||||||
|
|
||||||
|
return convertedQuery, paramTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
// toSnakeCase converts PascalCase to snake_case
|
||||||
|
func toSnakeCase(s string) string {
|
||||||
|
var result strings.Builder
|
||||||
|
runes := []rune(s)
|
||||||
|
|
||||||
|
for i, r := range runes {
|
||||||
|
if i > 0 && unicode.IsUpper(r) {
|
||||||
|
// Don't add underscore if previous char was also uppercase
|
||||||
|
// unless next char is lowercase (end of acronym)
|
||||||
|
if !unicode.IsUpper(runes[i-1]) ||
|
||||||
|
(i+1 < len(runes) && unicode.IsLower(runes[i+1])) {
|
||||||
|
result.WriteByte('_')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.WriteRune(unicode.ToLower(r))
|
||||||
|
}
|
||||||
|
return result.String()
|
||||||
|
}
|
@ -5,80 +5,33 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"dk/internal/database"
|
||||||
"dk/internal/helpers"
|
"dk/internal/helpers"
|
||||||
|
|
||||||
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Town represents a town in the game
|
// Town represents a town in the game
|
||||||
type Town struct {
|
type Town struct {
|
||||||
ID int `json:"id"`
|
ID int
|
||||||
Name string `json:"name" db:"required,unique"`
|
Name string
|
||||||
X int `json:"x"`
|
X int
|
||||||
Y int `json:"y"`
|
Y int
|
||||||
InnCost int `json:"inn_cost"`
|
InnCost int
|
||||||
MapCost int `json:"map_cost"`
|
MapCost int
|
||||||
TPCost int `json:"tp_cost"`
|
TPCost int
|
||||||
ShopList string `json:"shop_list"`
|
ShopList string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global store
|
// New creates a new Town with sensible defaults
|
||||||
var store *nigiri.BaseStore[Town]
|
|
||||||
|
|
||||||
// coordsKey creates a key for coordinate-based lookup
|
|
||||||
func coordsKey(x, y int) string {
|
|
||||||
return strconv.Itoa(x) + "," + strconv.Itoa(y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init sets up the Nigiri store and indices
|
|
||||||
func Init(collection *nigiri.Collection) {
|
|
||||||
store = nigiri.NewBaseStore[Town]()
|
|
||||||
|
|
||||||
// Register custom indices
|
|
||||||
store.RegisterIndex("byName", nigiri.BuildCaseInsensitiveLookupIndex(func(t *Town) string {
|
|
||||||
return t.Name
|
|
||||||
}))
|
|
||||||
|
|
||||||
store.RegisterIndex("byCoords", nigiri.BuildStringLookupIndex(func(t *Town) string {
|
|
||||||
return coordsKey(t.X, t.Y)
|
|
||||||
}))
|
|
||||||
|
|
||||||
store.RegisterIndex("byInnCost", nigiri.BuildIntGroupIndex(func(t *Town) int {
|
|
||||||
return t.InnCost
|
|
||||||
}))
|
|
||||||
|
|
||||||
store.RegisterIndex("byTPCost", nigiri.BuildIntGroupIndex(func(t *Town) int {
|
|
||||||
return t.TPCost
|
|
||||||
}))
|
|
||||||
|
|
||||||
store.RegisterIndex("allByID", nigiri.BuildSortedListIndex(func(a, b *Town) bool {
|
|
||||||
return a.ID < b.ID
|
|
||||||
}))
|
|
||||||
|
|
||||||
store.RebuildIndices()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStore returns the towns store
|
|
||||||
func GetStore() *nigiri.BaseStore[Town] {
|
|
||||||
if store == nil {
|
|
||||||
panic("towns store not initialized - call Initialize first")
|
|
||||||
}
|
|
||||||
return store
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a new Town with sensible defaults
|
|
||||||
func New() *Town {
|
func New() *Town {
|
||||||
return &Town{
|
return &Town{
|
||||||
Name: "",
|
Name: "",
|
||||||
X: 0, // Default coordinates
|
X: 0,
|
||||||
Y: 0,
|
Y: 0,
|
||||||
InnCost: 50, // Default inn cost
|
InnCost: 50,
|
||||||
MapCost: 100, // Default map cost
|
MapCost: 100,
|
||||||
TPCost: 25, // Default teleport cost
|
TPCost: 25,
|
||||||
ShopList: "", // No items by default
|
ShopList: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,85 +53,86 @@ func (t *Town) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CRUD operations
|
// CRUD operations
|
||||||
func (t *Town) Save() error {
|
|
||||||
if t.ID == 0 {
|
|
||||||
id, err := store.Create(t)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.ID = id
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return store.Update(t.ID, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Town) Delete() error {
|
func (t *Town) Delete() error {
|
||||||
store.Remove(t.ID)
|
return database.Exec("DELETE FROM towns WHERE id = %d", t.ID)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert with ID assignment
|
|
||||||
func (t *Town) Insert() error {
|
func (t *Town) Insert() error {
|
||||||
id, err := store.Create(t)
|
id, err := database.Insert("towns", t, "ID")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.ID = id
|
t.ID = int(id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query functions
|
// Query functions
|
||||||
func Find(id int) (*Town, error) {
|
func Find(id int) (*Town, error) {
|
||||||
town, exists := store.Find(id)
|
var town Town
|
||||||
if !exists {
|
err := database.Get(&town, "SELECT * FROM towns WHERE id = %d", id)
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("town with ID %d not found", id)
|
return nil, fmt.Errorf("town with ID %d not found", id)
|
||||||
}
|
}
|
||||||
return town, nil
|
return &town, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func All() ([]*Town, error) {
|
func All() ([]*Town, error) {
|
||||||
return store.AllSorted("allByID"), nil
|
var towns []*Town
|
||||||
|
err := database.Select(&towns, "SELECT * FROM towns ORDER BY id ASC")
|
||||||
|
return towns, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByName(name string) (*Town, error) {
|
func ByName(name string) (*Town, error) {
|
||||||
town, exists := store.LookupByIndex("byName", strings.ToLower(name))
|
var town Town
|
||||||
if !exists {
|
err := database.Get(&town, "SELECT * FROM towns WHERE name = %s COLLATE NOCASE", name)
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("town with name '%s' not found", name)
|
return nil, fmt.Errorf("town with name '%s' not found", name)
|
||||||
}
|
}
|
||||||
return town, nil
|
return &town, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByMaxInnCost(maxCost int) ([]*Town, error) {
|
func ByMaxInnCost(maxCost int) ([]*Town, error) {
|
||||||
return store.FilterByIndex("allByID", func(t *Town) bool {
|
var towns []*Town
|
||||||
return t.InnCost <= maxCost
|
err := database.Select(&towns, "SELECT * FROM towns WHERE inn_cost <= %d ORDER BY id ASC", maxCost)
|
||||||
}), nil
|
return towns, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByMaxTPCost(maxCost int) ([]*Town, error) {
|
func ByMaxTPCost(maxCost int) ([]*Town, error) {
|
||||||
return store.FilterByIndex("allByID", func(t *Town) bool {
|
var towns []*Town
|
||||||
return t.TPCost <= maxCost
|
err := database.Select(&towns, "SELECT * FROM towns WHERE tp_cost <= %d ORDER BY id ASC", maxCost)
|
||||||
}), nil
|
return towns, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByCoords(x, y int) (*Town, error) {
|
func ByCoords(x, y int) (*Town, error) {
|
||||||
town, exists := store.LookupByIndex("byCoords", coordsKey(x, y))
|
var town Town
|
||||||
if !exists {
|
err := database.Get(&town, "SELECT * FROM towns WHERE x = %d AND y = %d", x, y)
|
||||||
|
if err != nil {
|
||||||
return nil, nil // Return nil if not found (like original)
|
return nil, nil // Return nil if not found (like original)
|
||||||
}
|
}
|
||||||
return town, nil
|
return &town, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExistsAt(x, y int) bool {
|
func ExistsAt(x, y int) bool {
|
||||||
_, exists := store.LookupByIndex("byCoords", coordsKey(x, y))
|
var count int
|
||||||
return exists
|
err := database.Get(&count, "SELECT COUNT(*) FROM towns WHERE x = %d AND y = %d", x, y)
|
||||||
|
return err == nil && count > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) {
|
func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) {
|
||||||
maxDistance2 := float64(maxDistance * maxDistance)
|
var towns []*Town
|
||||||
|
err := database.Select(&towns, "SELECT * FROM towns")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
result := store.FilterByIndex("allByID", func(t *Town) bool {
|
maxDistance2 := float64(maxDistance * maxDistance)
|
||||||
return t.DistanceFromSquared(fromX, fromY) <= maxDistance2
|
var result []*Town
|
||||||
})
|
|
||||||
|
for _, town := range towns {
|
||||||
|
if town.DistanceFromSquared(fromX, fromY) <= maxDistance2 {
|
||||||
|
result = append(result, town)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sort by distance, then by ID
|
// Sort by distance, then by ID
|
||||||
sort.Slice(result, func(i, j int) bool {
|
sort.Slice(result, func(i, j int) bool {
|
||||||
@ -244,14 +198,3 @@ func (t *Town) SetPosition(x, y int) {
|
|||||||
t.X = x
|
t.X = x
|
||||||
t.Y = y
|
t.Y = y
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy compatibility functions (will be removed later)
|
|
||||||
func LoadData(dataPath string) error {
|
|
||||||
// No longer needed - Nigiri handles this
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveData(dataPath string) error {
|
|
||||||
// No longer needed - Nigiri handles this
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -3,109 +3,59 @@ package users
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"dk/internal/database"
|
||||||
"dk/internal/helpers"
|
"dk/internal/helpers"
|
||||||
"dk/internal/helpers/exp"
|
"dk/internal/helpers/exp"
|
||||||
|
|
||||||
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// User represents a user in the game
|
// User represents a user in the game
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int `json:"id"`
|
ID int
|
||||||
Username string `json:"username" db:"required,unique"`
|
Username string
|
||||||
Password string `json:"password" db:"required"`
|
Password string
|
||||||
Email string `json:"email" db:"required,unique"`
|
Email string
|
||||||
Verified int `json:"verified"`
|
Verified int
|
||||||
Token string `json:"token"`
|
Token string
|
||||||
Registered int64 `json:"registered"`
|
Registered int64
|
||||||
LastOnline int64 `json:"last_online"`
|
LastOnline int64
|
||||||
Auth int `json:"auth"`
|
Auth int
|
||||||
X int `json:"x"`
|
X int
|
||||||
Y int `json:"y"`
|
Y int
|
||||||
ClassID int `json:"class_id"`
|
ClassID int
|
||||||
Currently string `json:"currently"`
|
Currently string
|
||||||
FightID int `json:"fight_id"`
|
FightID int
|
||||||
HP int `json:"hp"`
|
HP int
|
||||||
MP int `json:"mp"`
|
MP int
|
||||||
TP int `json:"tp"`
|
TP int
|
||||||
MaxHP int `json:"max_hp"`
|
MaxHP int
|
||||||
MaxMP int `json:"max_mp"`
|
MaxMP int
|
||||||
MaxTP int `json:"max_tp"`
|
MaxTP int
|
||||||
Level int `json:"level" db:"index"`
|
Level int
|
||||||
Gold int `json:"gold"`
|
Gold int
|
||||||
Exp int `json:"exp"`
|
Exp int
|
||||||
GoldBonus int `json:"gold_bonus"`
|
GoldBonus int
|
||||||
ExpBonus int `json:"exp_bonus"`
|
ExpBonus int
|
||||||
Strength int `json:"strength"`
|
Strength int
|
||||||
Dexterity int `json:"dexterity"`
|
Dexterity int
|
||||||
Attack int `json:"attack"`
|
Attack int
|
||||||
Defense int `json:"defense"`
|
Defense int
|
||||||
WeaponID int `json:"weapon_id"`
|
WeaponID int
|
||||||
ArmorID int `json:"armor_id"`
|
ArmorID int
|
||||||
ShieldID int `json:"shield_id"`
|
ShieldID int
|
||||||
Slot1ID int `json:"slot_1_id"`
|
Slot1ID int
|
||||||
Slot2ID int `json:"slot_2_id"`
|
Slot2ID int
|
||||||
Slot3ID int `json:"slot_3_id"`
|
Slot3ID int
|
||||||
WeaponName string `json:"weapon_name"`
|
WeaponName string
|
||||||
ArmorName string `json:"armor_name"`
|
ArmorName string
|
||||||
ShieldName string `json:"shield_name"`
|
ShieldName string
|
||||||
Slot1Name string `json:"slot_1_name"`
|
Slot1Name string
|
||||||
Slot2Name string `json:"slot_2_name"`
|
Slot2Name string
|
||||||
Slot3Name string `json:"slot_3_name"`
|
Slot3Name string
|
||||||
Spells string `json:"spells"`
|
Spells string
|
||||||
Towns string `json:"towns"`
|
Towns string
|
||||||
}
|
|
||||||
|
|
||||||
// Global store
|
|
||||||
var store *nigiri.BaseStore[User]
|
|
||||||
|
|
||||||
// Init sets up the Nigiri store and indices
|
|
||||||
func Init(collection *nigiri.Collection) {
|
|
||||||
store = nigiri.NewBaseStore[User]()
|
|
||||||
|
|
||||||
// Register custom indices
|
|
||||||
store.RegisterIndex("byUsername", nigiri.BuildCaseInsensitiveLookupIndex(func(u *User) string {
|
|
||||||
return u.Username
|
|
||||||
}))
|
|
||||||
|
|
||||||
store.RegisterIndex("byEmail", nigiri.BuildStringLookupIndex(func(u *User) string {
|
|
||||||
return u.Email
|
|
||||||
}))
|
|
||||||
|
|
||||||
store.RegisterIndex("byLevel", nigiri.BuildIntGroupIndex(func(u *User) int {
|
|
||||||
return u.Level
|
|
||||||
}))
|
|
||||||
|
|
||||||
store.RegisterIndex("allByRegistered", nigiri.BuildSortedListIndex(func(a, b *User) bool {
|
|
||||||
if a.Registered != b.Registered {
|
|
||||||
return a.Registered > b.Registered // DESC
|
|
||||||
}
|
|
||||||
return a.ID > b.ID // DESC
|
|
||||||
}))
|
|
||||||
|
|
||||||
store.RegisterIndex("allByLevelExp", nigiri.BuildSortedListIndex(func(a, b *User) bool {
|
|
||||||
if a.Level != b.Level {
|
|
||||||
return a.Level > b.Level // Level DESC
|
|
||||||
}
|
|
||||||
if a.Exp != b.Exp {
|
|
||||||
return a.Exp > b.Exp // Exp DESC
|
|
||||||
}
|
|
||||||
return a.ID < b.ID // ID ASC
|
|
||||||
}))
|
|
||||||
|
|
||||||
store.RebuildIndices()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStore returns the users store
|
|
||||||
func GetStore() *nigiri.BaseStore[User] {
|
|
||||||
if store == nil {
|
|
||||||
panic("users store not initialized - call Initialize first")
|
|
||||||
}
|
|
||||||
return store
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new User with sensible defaults
|
// New creates a new User with sensible defaults
|
||||||
@ -166,94 +116,73 @@ func (u *User) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRUD operations
|
|
||||||
func (u *User) Save() error {
|
|
||||||
if u.ID == 0 {
|
|
||||||
id, err := store.Create(u)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
u.ID = id
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return store.Update(u.ID, u)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) Delete() error {
|
func (u *User) Delete() error {
|
||||||
store.Remove(u.ID)
|
return database.Exec("DELETE FROM users WHERE id = %d", u.ID)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert with ID assignment
|
|
||||||
func (u *User) Insert() error {
|
func (u *User) Insert() error {
|
||||||
id, err := store.Create(u)
|
id, err := database.Insert("users", u, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.ID = id
|
u.ID = int(id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query functions
|
|
||||||
func Find(id int) (*User, error) {
|
func Find(id int) (*User, error) {
|
||||||
user, exists := store.Find(id)
|
var user User
|
||||||
if !exists {
|
err := database.Get(&user, "SELECT * FROM users WHERE id = %d", id)
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("user with ID %d not found", id)
|
return nil, fmt.Errorf("user with ID %d not found", id)
|
||||||
}
|
}
|
||||||
return user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetByID(id int) *User {
|
func GetByID(id int) *User {
|
||||||
user, exists := store.Find(id)
|
user, err := Find(id)
|
||||||
if !exists {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
func All() ([]*User, error) {
|
func All() ([]*User, error) {
|
||||||
return store.AllSorted("allByRegistered"), nil
|
var users []*User
|
||||||
|
err := database.Select(&users, "SELECT * FROM users ORDER BY registered DESC, id DESC")
|
||||||
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByUsername(username string) (*User, error) {
|
func ByUsername(username string) (*User, error) {
|
||||||
user, exists := store.LookupByIndex("byUsername", strings.ToLower(username))
|
var user User
|
||||||
if !exists {
|
err := database.Get(&user, "SELECT * FROM users WHERE username = %s COLLATE NOCASE", username)
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("user with username '%s' not found", username)
|
return nil, fmt.Errorf("user with username '%s' not found", username)
|
||||||
}
|
}
|
||||||
return user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByEmail(email string) (*User, error) {
|
func ByEmail(email string) (*User, error) {
|
||||||
user, exists := store.LookupByIndex("Email_idx", email)
|
var user User
|
||||||
if !exists {
|
err := database.Get(&user, "SELECT * FROM users WHERE email = %s", email)
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("user with email '%s' not found", email)
|
return nil, fmt.Errorf("user with email '%s' not found", email)
|
||||||
}
|
}
|
||||||
return user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByLevel(level int) ([]*User, error) {
|
func ByLevel(level int) ([]*User, error) {
|
||||||
return store.GroupByIndex("level_idx", level), nil
|
var users []*User
|
||||||
|
err := database.Select(&users, "SELECT * FROM users WHERE level = %d ORDER BY exp DESC, id ASC", level)
|
||||||
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Online(within time.Duration) ([]*User, error) {
|
func Online(within time.Duration) ([]*User, error) {
|
||||||
cutoff := time.Now().Add(-within).Unix()
|
cutoff := time.Now().Add(-within).Unix()
|
||||||
|
var users []*User
|
||||||
result := store.FilterByIndex("allByRegistered", func(u *User) bool {
|
err := database.Select(&users, "SELECT * FROM users WHERE last_online >= %d ORDER BY last_online DESC, id ASC", cutoff)
|
||||||
return u.LastOnline >= cutoff
|
return users, err
|
||||||
})
|
|
||||||
|
|
||||||
// Sort by last_online DESC, then ID ASC
|
|
||||||
sort.Slice(result, func(i, j int) bool {
|
|
||||||
if result[i].LastOnline != result[j].LastOnline {
|
|
||||||
return result[i].LastOnline > result[j].LastOnline // DESC
|
|
||||||
}
|
|
||||||
return result[i].ID < result[j].ID // ASC
|
|
||||||
})
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper methods
|
|
||||||
func (u *User) RegisteredTime() time.Time {
|
func (u *User) RegisteredTime() time.Time {
|
||||||
return time.Unix(u.Registered, 0)
|
return time.Unix(u.Registered, 0)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user