From 00c183963567c52d22016b2ded820b629bde2ed4 Mon Sep 17 00:00:00 2001 From: wonder Date: Sat, 20 Jun 2026 22:40:46 +0800 Subject: [PATCH] feat: migrate database from SQLite to MySQL with .env configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .env.example | 14 +++++ CLAUDE.md | 20 +++++--- Dockerfile | 5 +- config/config.go | 22 +++++++- database/db.go | 118 +++++++++++++++---------------------------- docker-compose.yml | 25 +++++++++ go.mod | 4 +- go.sum | 8 ++- handlers/auth.go | 2 +- handlers/generate.go | 2 +- handlers/repos.go | 6 +-- handlers/review.go | 2 +- handlers/settings.go | 2 +- main.go | 2 +- 14 files changed, 130 insertions(+), 102 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9440e96 --- /dev/null +++ b/.env.example @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 920487b..45cb23e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/Dockerfile b/Dockerfile index 33f525b..8fc4d03 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/config/config.go b/config/config.go index d4601c2..fa9cfee 100644 --- a/config/config.go +++ b/config/config.go @@ -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 { diff --git a/database/db.go b/database/db.go index 5f2f8da..af5a7d5 100644 --- a/database/db.go +++ b/database/db.go @@ -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) diff --git a/docker-compose.yml b/docker-compose.yml index f1b773f..8ba41d1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/go.mod b/go.mod index baf7952..ec57da6 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 6eafadc..7098545 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/handlers/auth.go b/handlers/auth.go index 43ca68b..783c4b3 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -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 diff --git a/handlers/generate.go b/handlers/generate.go index c85fd00..230997c 100644 --- a/handlers/generate.go +++ b/handlers/generate.go @@ -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) diff --git a/handlers/repos.go b/handlers/repos.go index f34458b..9b32377 100644 --- a/handlers/repos.go +++ b/handlers/repos.go @@ -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 { diff --git a/handlers/review.go b/handlers/review.go index 7aad2c0..3a0c40c 100644 --- a/handlers/review.go +++ b/handlers/review.go @@ -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) diff --git a/handlers/settings.go b/handlers/settings.go index 224b090..04f650b 100644 --- a/handlers/settings.go +++ b/handlers/settings.go @@ -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()}) diff --git a/main.go b/main.go index ddd7562..41679b6 100644 --- a/main.go +++ b/main.go @@ -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) }