feat: migrate database from SQLite to MySQL with .env configuration

- Replace go-sqlite3 with go-sql-driver/mysql (pure Go, no CGO needed)
- Add joho/godotenv for .env file loading
- Rewrite database/db.go with MySQL-compatible DDL (BIGINT AUTO_INCREMENT, InnoDB, utf8mb4)
- Convert all SQLite-specific SQL: INSERT OR IGNORE → INSERT IGNORE,
  INSERT OR REPLACE → INSERT ... ON DUPLICATE KEY UPDATE,
  datetime('now') → NOW(), date arithmetic → DATE_SUB()
- Add MySQL config fields to config.go (host/port/user/password/database)
- Add .env.example with connection parameter template
- Update Dockerfile: remove CGO/gcc/musl dependency, smaller build
- Update docker-compose.yml: add MySQL service container with healthcheck
- Update CLAUDE.md documentation
This commit is contained in:
2026-06-20 22:40:46 +08:00
parent 6c60b767df
commit 00c1839635
14 changed files with 130 additions and 102 deletions
+14
View File
@@ -0,0 +1,14 @@
# Server
PORT=8080
GIN_MODE=debug
SESSION_SECRET=
# Data directory (cloned repos, etc.)
DATA_DIR=data
# MySQL
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=
MYSQL_DATABASE=pr_helper
+12 -8
View File
@@ -8,7 +8,7 @@ PR-Helper is a self-hosted web service that auto-generates PR descriptions from
## Tech Stack
- **Backend**: Go + Gin + SQLite (go-sqlite3) + go-git
- **Backend**: Go + Gin + MySQL + go-git
- **Frontend**: Go html/template + HTMX + D3.js + diff2html + Tailwind CSS
- **LLM**: OpenAI-compatible API (SSE streaming)
- **Deploy**: Docker
@@ -16,14 +16,17 @@ PR-Helper is a self-hosted web service that auto-generates PR descriptions from
## Build & Run
```bash
# Build (CGO required for SQLite)
CGO_ENABLED=1 go build -o pr-helper .
# Build
go build -o pr-helper .
# Run
# Configure — copy .env.example to .env and set MySQL credentials
cp .env.example .env
# Run (requires MySQL server)
./pr-helper
# Server listens on :8080
# Docker
# Docker (includes MySQL container)
docker compose up --build
```
@@ -33,13 +36,13 @@ docker compose up --build
handlers/ → HTTP handlers (pages + JSON API + SSE endpoints)
services/ → Business logic (git ops, LLM calls, PDF gen, cache management)
models/ → Data models (repository, settings, analysis)
database/ → SQLite init and migrations
database/ → MySQL init and migrations
config/ → Configuration loading
templates/ → Go HTML templates (layouts/, pages/, partials/)
static/ → CSS (Tailwind output), JS (graph, diff-viewer, sse), vendor libs
```
**Data flow**: Browser ↔ Gin handlers → services (git/llm) → SQLite + filesystem (`data/`)
**Data flow**: Browser ↔ Gin handlers → services (git/llm) → MySQL + filesystem (`data/`)
**Key service layer responsibilities**:
- `services/git.go` — clone, diff, graph data extraction via go-git
@@ -63,8 +66,9 @@ static/ → CSS (Tailwind output), JS (graph, diff-viewer, sse), vendor libs
## Data Storage
- SQLite DB at `data/pr-helper.db` — settings (KV), repositories, analyses, review_notes
- MySQL database `pr_helper` — settings (KV), repositories, analyses, review_notes
- Cloned repos cached at `data/repos/` with configurable expiry (default 7 days)
- Database connection configured via `.env` file (see `.env.example`)
## Development Notes
+2 -3
View File
@@ -1,15 +1,14 @@
# Build stage
FROM golang:1.24-alpine AS builder
RUN apk add --no-cache gcc musl-dev
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=1 go build -o pr-helper .
RUN CGO_ENABLED=0 go build -o pr-helper .
# Runtime stage
FROM alpine:3.20
RUN apk add --no-cache ca-certificates
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY --from=builder /app/pr-helper .
COPY --from=builder /app/templates ./templates
+20 -2
View File
@@ -3,8 +3,11 @@ package config
import (
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"github.com/joho/godotenv"
)
type Config struct {
@@ -12,14 +15,27 @@ type Config struct {
GinMode string
DataDir string
SessionSecret string
MySQLHost string
MySQLPort string
MySQLUser string
MySQLPassword string
MySQLDatabase string
}
func Load() *Config {
// Load .env file if present (ignore error if missing)
godotenv.Load()
cfg := &Config{
Port: getEnv("PORT", "8080"),
GinMode: getEnv("GIN_MODE", "debug"),
DataDir: getEnv("DATA_DIR", "data"),
SessionSecret: getEnv("SESSION_SECRET", ""),
MySQLHost: getEnv("MYSQL_HOST", "127.0.0.1"),
MySQLPort: getEnv("MYSQL_PORT", "3306"),
MySQLUser: getEnv("MYSQL_USER", "root"),
MySQLPassword: getEnv("MYSQL_PASSWORD", ""),
MySQLDatabase: getEnv("MYSQL_DATABASE", "pr_helper"),
}
// Generate random session secret if not provided
if cfg.SessionSecret == "" {
@@ -33,8 +49,10 @@ func Load() *Config {
return cfg
}
func (c *Config) DBPath() string {
return filepath.Join(c.DataDir, "pr-helper.db")
// MySQLDSN returns the MySQL Data Source Name for go-sql-driver/mysql.
func (c *Config) MySQLDSN() string {
return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
c.MySQLUser, c.MySQLPassword, c.MySQLHost, c.MySQLPort, c.MySQLDatabase)
}
func (c *Config) ReposDir() string {
+40 -78
View File
@@ -4,7 +4,7 @@ import (
"database/sql"
"log"
_ "github.com/mattn/go-sqlite3"
_ "github.com/go-sql-driver/mysql"
"github.com/HoHD/PR-Helper/models"
)
@@ -13,11 +13,14 @@ type DB struct {
conn *sql.DB
}
func New(dbPath string) (*DB, error) {
conn, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
func New(dsn string) (*DB, error) {
conn, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
if err := conn.Ping(); err != nil {
return nil, err
}
db := &DB{conn: conn}
if err := db.migrate(); err != nil {
return nil, err
@@ -39,111 +42,70 @@ func (db *DB) Conn() *sql.DB {
func (db *DB) migrate() error {
stmts := []string{
`CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
key VARCHAR(255) PRIMARY KEY,
value TEXT NOT NULL
)`,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
id BIGINT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS user_settings (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
key TEXT NOT NULL,
user_id BIGINT NOT NULL,
key VARCHAR(255) NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (user_id, key)
)`,
PRIMARY KEY (user_id, key),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS repositories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
url TEXT NOT NULL,
local_path TEXT NOT NULL,
size_bytes INTEGER DEFAULT 0,
size_bytes BIGINT DEFAULT 0,
cloned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_used DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
last_used DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS analyses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
repo_id INTEGER REFERENCES repositories(id),
type TEXT NOT NULL,
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
repo_id BIGINT,
type VARCHAR(50) NOT NULL,
base_ref TEXT NOT NULL,
head_ref TEXT NOT NULL,
result TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
result LONGTEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (repo_id) REFERENCES repositories(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS review_notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
analysis_id INTEGER REFERENCES analyses(id),
scope TEXT NOT NULL,
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
analysis_id BIGINT,
scope VARCHAR(50) NOT NULL,
scope_key TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (analysis_id) REFERENCES analyses(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
}
for _, s := range stmts {
if _, err := db.conn.Exec(s); err != nil {
return err
}
}
// Run incremental migrations for existing databases
if err := db.migrateV2(); err != nil {
return err
}
return nil
}
// migrateV2 adds user_id columns to existing tables and removes the old UNIQUE constraint on url.
func (db *DB) migrateV2() error {
migrations := []string{
`ALTER TABLE repositories ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE CASCADE`,
`ALTER TABLE analyses ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE CASCADE`,
`ALTER TABLE review_notes ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE CASCADE`,
}
for _, m := range migrations {
// ALTER TABLE ADD COLUMN will fail if column already exists; ignore the error
db.conn.Exec(m)
}
// Recreate repositories table without the UNIQUE constraint on url (same url can be cloned by different users)
// SQLite doesn't support DROP CONSTRAINT, so we use a workaround
var hasOldUnique int
db.conn.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='repositories_old'`).Scan(&hasOldUnique)
if hasOldUnique == 0 {
// Check if the UNIQUE constraint exists by trying to detect the old schema
// We'll just do the migration idempotently
db.conn.Exec(`CREATE TABLE IF NOT EXISTS repositories_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
url TEXT NOT NULL,
local_path TEXT NOT NULL,
size_bytes INTEGER DEFAULT 0,
cloned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_used DATETIME DEFAULT CURRENT_TIMESTAMP
)`)
db.conn.Exec(`INSERT OR IGNORE INTO repositories_new (id, user_id, url, local_path, size_bytes, cloned_at, last_used)
SELECT id, user_id, url, local_path, size_bytes, cloned_at, last_used FROM repositories`)
// Only swap if the new table has data or old table is empty
var newCount, oldCount int
db.conn.QueryRow(`SELECT COUNT(*) FROM repositories_new`).Scan(&newCount)
db.conn.QueryRow(`SELECT COUNT(*) FROM repositories`).Scan(&oldCount)
if newCount >= oldCount {
db.conn.Exec(`DROP TABLE repositories`)
db.conn.Exec(`ALTER TABLE repositories_new RENAME TO repositories`)
} else {
db.conn.Exec(`DROP TABLE repositories_new`)
}
}
return nil
}
func (db *DB) seedDefaults() error {
for key, val := range models.DefaultSettings {
_, err := db.conn.Exec(
`INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)`, key, val,
`INSERT IGNORE INTO settings (key, value) VALUES (?, ?)`, key, val,
)
if err != nil {
log.Printf("warning: failed to seed setting %s: %v", key, err)
+25
View File
@@ -1,4 +1,20 @@
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD:-pr_helper_pass}
MYSQL_DATABASE: ${MYSQL_DATABASE:-pr_helper}
volumes:
- mysql-data:/var/lib/mysql
ports:
- "3306:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
pr-helper:
build: .
ports:
@@ -7,7 +23,16 @@ services:
- pr-helper-data:/app/data
environment:
- GIN_MODE=release
- MYSQL_HOST=mysql
- MYSQL_PORT=3306
- MYSQL_USER=root
- MYSQL_PASSWORD=${MYSQL_PASSWORD:-pr_helper_pass}
- MYSQL_DATABASE=${MYSQL_DATABASE:-pr_helper}
depends_on:
mysql:
condition: service_healthy
restart: unless-stopped
volumes:
mysql-data:
pr-helper-data:
+3 -1
View File
@@ -6,13 +6,15 @@ require (
github.com/gin-contrib/sessions v1.1.0
github.com/gin-gonic/gin v1.12.0
github.com/go-git/go-git/v5 v5.19.1
github.com/mattn/go-sqlite3 v1.14.24
github.com/go-sql-driver/mysql v1.10.0
github.com/joho/godotenv v1.5.1
github.com/sashabaranov/go-openai v1.41.2
golang.org/x/crypto v0.50.0
)
require (
dario.cat/mergo v1.0.0 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
+6 -2
View File
@@ -1,5 +1,7 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
@@ -54,6 +56,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
@@ -73,6 +77,8 @@ github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzq
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
@@ -90,8 +96,6 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
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/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+1 -1
View File
@@ -126,7 +126,7 @@ func (h *AuthHandler) HandleRegister(c *gin.Context) {
// Seed default settings for the user
for key, val := range defaultUserSettings {
h.db.Exec(`INSERT OR IGNORE INTO user_settings (user_id, key, value) VALUES (?, ?, ?)`, userID, key, val)
h.db.Exec(`INSERT IGNORE INTO user_settings (user_id, key, value) VALUES (?, ?, ?)`, userID, key, val)
}
// Auto-login: set session
+1 -1
View File
@@ -73,7 +73,7 @@ func (h *GenerateHandler) Generate(c *gin.Context) {
}
// Update last_used
h.db.Exec(`UPDATE repositories SET last_used = datetime('now') WHERE id = ?`, id)
h.db.Exec(`UPDATE repositories SET last_used = NOW() WHERE id = ?`, id)
// Generate PR description (pass user ID for per-user LLM config)
pr, err := services.GeneratePR(h.db, localPath, req.Base, req.Head, user.ID, sendEvent)
+3 -3
View File
@@ -104,7 +104,7 @@ func (h *ReposHandler) CleanupRepos(c *gin.Context) {
if maxAgeDays == "" {
maxAgeDays = "7"
}
rows, err := h.db.Query(`SELECT id, local_path FROM repositories WHERE user_id = ? AND last_used < datetime('now', '-' || ? || ' days')`, user.ID, maxAgeDays)
rows, err := h.db.Query(`SELECT id, local_path FROM repositories WHERE user_id = ? AND last_used < DATE_SUB(NOW(), INTERVAL ? DAY)`, user.ID, maxAgeDays)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
@@ -244,7 +244,7 @@ func (h *ReposHandler) GetGraph(c *gin.Context) {
}
// Update last_used
h.db.Exec(`UPDATE repositories SET last_used = datetime('now') WHERE id = ?`, id)
h.db.Exec(`UPDATE repositories SET last_used = NOW() WHERE id = ?`, id)
repo, err := services.OpenRepo(localPath)
if err != nil {
@@ -297,7 +297,7 @@ func (h *ReposHandler) GetDiff(c *gin.Context) {
}
// Update last_used
h.db.Exec(`UPDATE repositories SET last_used = datetime('now') WHERE id = ?`, id)
h.db.Exec(`UPDATE repositories SET last_used = NOW() WHERE id = ?`, id)
repo, err := services.OpenRepo(localPath)
if err != nil {
+1 -1
View File
@@ -105,7 +105,7 @@ func (h *ReviewHandler) Review(c *gin.Context) {
}
// Update last_used
h.db.Exec(`UPDATE repositories SET last_used = datetime('now') WHERE id = ?`, id)
h.db.Exec(`UPDATE repositories SET last_used = NOW() WHERE id = ?`, id)
// Run AI review (pass user ID for per-user LLM config)
reviewResult, err := services.GenerateReview(h.db, localPath, req.Base, req.Head, topN, concurrency, user.ID, sendEvent)
+1 -1
View File
@@ -56,7 +56,7 @@ func (h *SettingsHandler) UpdateSettings(c *gin.Context) {
return
}
for key, val := range body {
_, err := tx.Exec(`INSERT OR REPLACE INTO user_settings (user_id, key, value) VALUES (?, ?, ?)`, user.ID, key, val)
_, err := tx.Exec(`INSERT INTO user_settings (user_id, key, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value)`, user.ID, key, val)
if err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+1 -1
View File
@@ -25,7 +25,7 @@ import (
func main() {
cfg := config.Load()
db, err := database.New(cfg.DBPath())
db, err := database.New(cfg.MySQLDSN())
if err != nil {
log.Fatalf("failed to initialize database: %v", err)
}