Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/gochat/gochat/internal/config"
|
||||
"github.com/gochat/gochat/internal/database"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
command := os.Args[1]
|
||||
|
||||
// Load config to get database connection info
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dbURL := cfg.Database.MigrateDSN()
|
||||
migrationsPath := cfg.Database.GetMigrationsPath()
|
||||
|
||||
switch command {
|
||||
case "up":
|
||||
if err := database.RunMigrations(dbURL, migrationsPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error running migrations up: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Migrations applied successfully (up)")
|
||||
|
||||
case "down":
|
||||
if err := database.RollbackMigrations(dbURL, migrationsPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error running migrations down: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Migrations rolled back successfully (down)")
|
||||
|
||||
case "force":
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Fprintf(os.Stderr, "Error: 'force' requires a version argument\n")
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
version, err := strconv.Atoi(os.Args[2])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: invalid version number '%s': %v\n", os.Args[2], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := database.ForceVersion(dbURL, migrationsPath, version); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error forcing version %d: %v\n", version, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Migration version forced to %d\n", version)
|
||||
|
||||
case "version":
|
||||
version, dirty, err := database.CurrentVersion(dbURL, migrationsPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error getting current version: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if dirty {
|
||||
fmt.Printf("Current version: %d (DIRTY — a migration partially failed)\n", version)
|
||||
} else {
|
||||
fmt.Printf("Current version: %d\n", version)
|
||||
}
|
||||
|
||||
case "steps":
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Fprintf(os.Stderr, "Error: 'steps' requires a step count argument\n")
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
steps, err := strconv.Atoi(os.Args[2])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: invalid step count '%s': %v\n", os.Args[2], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := database.MigrateSteps(dbURL, migrationsPath, steps); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error running %d migration steps: %v\n", steps, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Applied %d migration steps successfully\n", steps)
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command)
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Println("Usage: migrate <command> [args]")
|
||||
fmt.Println("")
|
||||
fmt.Println("Commands:")
|
||||
fmt.Println(" up Apply all pending migrations")
|
||||
fmt.Println(" down Rollback all migrations")
|
||||
fmt.Println(" force <v> Force version to v (fix dirty state)")
|
||||
fmt.Println(" version Show current migration version")
|
||||
fmt.Println(" steps <n> Apply n steps (positive=up, negative=down)")
|
||||
fmt.Println("")
|
||||
fmt.Println("Environment: Uses database config from Viper (config.yaml)")
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gochat/gochat/internal/database"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// setupTestMigrations creates a temporary directory with test migration files
|
||||
// that are compatible with SQLite (no PostgreSQL-specific features).
|
||||
func setupTestMigrations(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
|
||||
// Migration 1: Create a simple users table (SQLite-compatible)
|
||||
up1 := `
|
||||
CREATE TABLE IF NOT EXISTS test_users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`
|
||||
down1 := `DROP TABLE IF EXISTS test_users;`
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "000001_create_test_users.up.sql"), []byte(up1), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "000001_create_test_users.down.sql"), []byte(down1), 0644))
|
||||
|
||||
// Migration 2: Add a phone column
|
||||
up2 := `
|
||||
ALTER TABLE test_users ADD COLUMN phone TEXT;
|
||||
`
|
||||
down2 := `
|
||||
-- SQLite doesn't support DROP COLUMN before 3.35.0, so we recreate the table
|
||||
CREATE TABLE test_users_backup AS SELECT id, name, email, created_at, updated_at FROM test_users;
|
||||
DROP TABLE test_users;
|
||||
CREATE TABLE test_users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
INSERT INTO test_users SELECT id, name, email, created_at, updated_at FROM test_users_backup;
|
||||
DROP TABLE test_users_backup;
|
||||
`
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "000002_add_test_user_phone.up.sql"), []byte(up2), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "000002_add_test_user_phone.down.sql"), []byte(down2), 0644))
|
||||
|
||||
// Migration 3: Create a posts table
|
||||
up3 := `
|
||||
CREATE TABLE IF NOT EXISTS test_posts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES test_users(id),
|
||||
title TEXT NOT NULL,
|
||||
content TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`
|
||||
down3 := `DROP TABLE IF EXISTS test_posts;`
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "000003_create_test_posts.up.sql"), []byte(up3), 0644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "000003_create_test_posts.down.sql"), []byte(down3), 0644))
|
||||
|
||||
return dir
|
||||
}
|
||||
|
||||
// sqliteDBURL creates a SQLite database URL for golang-migrate.
|
||||
func sqliteDBURL(t *testing.T) string {
|
||||
t.Helper()
|
||||
dbPath := filepath.Join(t.TempDir(), "test.db")
|
||||
return fmt.Sprintf("sqlite3://%s", dbPath)
|
||||
}
|
||||
|
||||
func TestRunMigrationsUp(t *testing.T) {
|
||||
migrationsDir := setupTestMigrations(t)
|
||||
dbURL := sqliteDBURL(t)
|
||||
|
||||
err := database.RunMigrations(dbURL, migrationsDir)
|
||||
assert.NoError(t, err, "RunMigrations should apply all pending migrations")
|
||||
|
||||
// Verify current version is 3
|
||||
version, dirty, err := database.CurrentVersion(dbURL, migrationsDir)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, uint(3), version)
|
||||
assert.False(t, dirty)
|
||||
}
|
||||
|
||||
func TestRunMigrationsNoChange(t *testing.T) {
|
||||
migrationsDir := setupTestMigrations(t)
|
||||
dbURL := sqliteDBURL(t)
|
||||
|
||||
// First run: apply all migrations
|
||||
err := database.RunMigrations(dbURL, migrationsDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Second run: should return nil (ErrNoChange is handled)
|
||||
err = database.RunMigrations(dbURL, migrationsDir)
|
||||
assert.NoError(t, err, "Running migrations with no pending changes should succeed")
|
||||
}
|
||||
|
||||
func TestMigrateStepsUp(t *testing.T) {
|
||||
migrationsDir := setupTestMigrations(t)
|
||||
dbURL := sqliteDBURL(t)
|
||||
|
||||
// Apply 1 step at a time
|
||||
err := database.MigrateSteps(dbURL, migrationsDir, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
version, dirty, err := database.CurrentVersion(dbURL, migrationsDir)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint(1), version)
|
||||
assert.False(t, dirty)
|
||||
|
||||
// Apply 2 more steps
|
||||
err = database.MigrateSteps(dbURL, migrationsDir, 2)
|
||||
require.NoError(t, err)
|
||||
|
||||
version, dirty, err = database.CurrentVersion(dbURL, migrationsDir)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint(3), version)
|
||||
assert.False(t, dirty)
|
||||
}
|
||||
|
||||
func TestMigrateStepsDown(t *testing.T) {
|
||||
migrationsDir := setupTestMigrations(t)
|
||||
dbURL := sqliteDBURL(t)
|
||||
|
||||
// First, apply all migrations
|
||||
err := database.RunMigrations(dbURL, migrationsDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Rollback 1 step
|
||||
err = database.MigrateSteps(dbURL, migrationsDir, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
version, _, err := database.CurrentVersion(dbURL, migrationsDir)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint(2), version)
|
||||
|
||||
// Rollback 2 more steps
|
||||
err = database.MigrateSteps(dbURL, migrationsDir, -2)
|
||||
require.NoError(t, err)
|
||||
|
||||
version, _, err = database.CurrentVersion(dbURL, migrationsDir)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint(0), version)
|
||||
}
|
||||
|
||||
func TestRollbackMigrations(t *testing.T) {
|
||||
migrationsDir := setupTestMigrations(t)
|
||||
dbURL := sqliteDBURL(t)
|
||||
|
||||
// Apply all migrations first
|
||||
err := database.RunMigrations(dbURL, migrationsDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Rollback all
|
||||
err = database.RollbackMigrations(dbURL, migrationsDir)
|
||||
assert.NoError(t, err, "RollbackMigrations should succeed")
|
||||
|
||||
version, _, err := database.CurrentVersion(dbURL, migrationsDir)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint(0), version, "All migrations should be rolled back")
|
||||
}
|
||||
|
||||
func TestRollbackNoChange(t *testing.T) {
|
||||
migrationsDir := setupTestMigrations(t)
|
||||
dbURL := sqliteDBURL(t)
|
||||
|
||||
// Try to rollback when no migrations have been applied
|
||||
err := database.RollbackMigrations(dbURL, migrationsDir)
|
||||
assert.NoError(t, err, "Rollback with no applied migrations should succeed (ErrNoChange)")
|
||||
}
|
||||
|
||||
func TestForceVersion(t *testing.T) {
|
||||
migrationsDir := setupTestMigrations(t)
|
||||
dbURL := sqliteDBURL(t)
|
||||
|
||||
// Apply all migrations first
|
||||
err := database.RunMigrations(dbURL, migrationsDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Force version to 1
|
||||
err = database.ForceVersion(dbURL, migrationsDir, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
version, dirty, err := database.CurrentVersion(dbURL, migrationsDir)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint(1), version)
|
||||
// After Force, dirty state should be cleared
|
||||
assert.False(t, dirty)
|
||||
}
|
||||
|
||||
func TestCurrentVersion(t *testing.T) {
|
||||
migrationsDir := setupTestMigrations(t)
|
||||
dbURL := sqliteDBURL(t)
|
||||
|
||||
// Before any migrations, version should be 0 (with ErrNoChange handled internally)
|
||||
version, dirty, err := database.CurrentVersion(dbURL, migrationsDir)
|
||||
// When no migrations have been applied, Version returns an error
|
||||
// This is expected — golang-migrate returns ErrNoChange for version 0
|
||||
if err != nil {
|
||||
t.Logf("Expected: version 0 with no migrations applied may return error: %v", err)
|
||||
}
|
||||
|
||||
// Apply migrations
|
||||
err = database.RunMigrations(dbURL, migrationsDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
version, dirty, err = database.CurrentVersion(dbURL, migrationsDir)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint(3), version)
|
||||
assert.False(t, dirty)
|
||||
}
|
||||
Reference in New Issue
Block a user