Skip to content

Commit d6610af

Browse files
feat(migrations): auto-run
1 parent 1071c09 commit d6610af

7 files changed

Lines changed: 122 additions & 60 deletions

File tree

Makefile

Lines changed: 3 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
.PHONY: generate build run test test-integration test-integration-verbose test-all lint swagger docker docker-down ci benchmark \
2-
migrate-create migrate-up migrate-down migrate-down-all migrate-down-one migrate-version migrate-force \
3-
migrate-up-docker migrate-down-docker migrate-down-all-docker migrate-down-one-docker migrate-version-docker migrate-force-docker
2+
migrate-create migrate-up migrate-down migrate-version
43

54
MIGRATE ?= migrate
65
MIGRATIONS_DIR ?= migrations
@@ -24,64 +23,20 @@ migrate-create:
2423
$(MIGRATE) create -ext sql -dir $(MIGRATIONS_DIR) $(name)
2524

2625
migrate-up:
27-
@db_url="$${MIGRATE_DATABASE_URL:-$${DATABASE_URL:-$$(grep -E '^DATABASE_URL=' .env 2>/dev/null | head -n1 | cut -d= -f2-)}}"; \
28-
test -n "$$db_url" || (echo "Set MIGRATE_DATABASE_URL or DATABASE_URL (or add DATABASE_URL to .env)" && exit 1); \
29-
$(MIGRATE) -path $(MIGRATIONS_DIR) -database "$$db_url" up
30-
31-
migrate-down:
32-
@$(MAKE) migrate-down-all
33-
34-
migrate-down-all:
35-
@db_url="$${MIGRATE_DATABASE_URL:-$${DATABASE_URL:-$$(grep -E '^DATABASE_URL=' .env 2>/dev/null | head -n1 | cut -d= -f2-)}}"; \
36-
test -n "$$db_url" || (echo "Set MIGRATE_DATABASE_URL or DATABASE_URL (or add DATABASE_URL to .env)" && exit 1); \
37-
$(MIGRATE) -path $(MIGRATIONS_DIR) -database "$$db_url" down -all
38-
39-
migrate-down-one:
40-
@db_url="$${MIGRATE_DATABASE_URL:-$${DATABASE_URL:-$$(grep -E '^DATABASE_URL=' .env 2>/dev/null | head -n1 | cut -d= -f2-)}}"; \
41-
test -n "$$db_url" || (echo "Set MIGRATE_DATABASE_URL or DATABASE_URL (or add DATABASE_URL to .env)" && exit 1); \
42-
$(MIGRATE) -path $(MIGRATIONS_DIR) -database "$$db_url" down 1
43-
44-
migrate-version:
45-
@db_url="$${MIGRATE_DATABASE_URL:-$${DATABASE_URL:-$$(grep -E '^DATABASE_URL=' .env 2>/dev/null | head -n1 | cut -d= -f2-)}}"; \
46-
test -n "$$db_url" || (echo "Set MIGRATE_DATABASE_URL or DATABASE_URL (or add DATABASE_URL to .env)" && exit 1); \
47-
$(MIGRATE) -path $(MIGRATIONS_DIR) -database "$$db_url" version
48-
49-
migrate-force:
50-
@test -n "$(version)" || (echo "Usage: make migrate-force version=<number>" && exit 1)
51-
@db_url="$${MIGRATE_DATABASE_URL:-$${DATABASE_URL:-$$(grep -E '^DATABASE_URL=' .env 2>/dev/null | head -n1 | cut -d= -f2-)}}"; \
52-
test -n "$$db_url" || (echo "Set MIGRATE_DATABASE_URL or DATABASE_URL (or add DATABASE_URL to .env)" && exit 1); \
53-
$(MIGRATE) -path $(MIGRATIONS_DIR) -database "$$db_url" force $(version)
54-
55-
# Database migrations via Docker (uses Compose network, works with DATABASE_URL=db:5432)
56-
migrate-up-docker:
5726
@db_url="$${MIGRATE_DATABASE_URL:-$${DATABASE_URL:-$$(grep -E '^DATABASE_URL=' .env 2>/dev/null | head -n1 | cut -d= -f2-)}}"; \
5827
test -n "$$db_url" || (echo "Set MIGRATE_DATABASE_URL or DATABASE_URL (or add DATABASE_URL to .env)" && exit 1); \
5928
docker run --rm --network $(COMPOSE_NETWORK) -v "$(CURDIR)/$(MIGRATIONS_DIR):/migrations" $(MIGRATE_DOCKER_IMAGE) -path /migrations -database "$$db_url" up
6029

61-
migrate-down-docker:
62-
@$(MAKE) migrate-down-all-docker
63-
64-
migrate-down-all-docker:
65-
@db_url="$${MIGRATE_DATABASE_URL:-$${DATABASE_URL:-$$(grep -E '^DATABASE_URL=' .env 2>/dev/null | head -n1 | cut -d= -f2-)}}"; \
66-
test -n "$$db_url" || (echo "Set MIGRATE_DATABASE_URL or DATABASE_URL (or add DATABASE_URL to .env)" && exit 1); \
67-
docker run --rm --network $(COMPOSE_NETWORK) -v "$(CURDIR)/$(MIGRATIONS_DIR):/migrations" $(MIGRATE_DOCKER_IMAGE) -path /migrations -database "$$db_url" down -all
68-
69-
migrate-down-one-docker:
30+
migrate-down:
7031
@db_url="$${MIGRATE_DATABASE_URL:-$${DATABASE_URL:-$$(grep -E '^DATABASE_URL=' .env 2>/dev/null | head -n1 | cut -d= -f2-)}}"; \
7132
test -n "$$db_url" || (echo "Set MIGRATE_DATABASE_URL or DATABASE_URL (or add DATABASE_URL to .env)" && exit 1); \
7233
docker run --rm --network $(COMPOSE_NETWORK) -v "$(CURDIR)/$(MIGRATIONS_DIR):/migrations" $(MIGRATE_DOCKER_IMAGE) -path /migrations -database "$$db_url" down 1
7334

74-
migrate-version-docker:
35+
migrate-version:
7536
@db_url="$${MIGRATE_DATABASE_URL:-$${DATABASE_URL:-$$(grep -E '^DATABASE_URL=' .env 2>/dev/null | head -n1 | cut -d= -f2-)}}"; \
7637
test -n "$$db_url" || (echo "Set MIGRATE_DATABASE_URL or DATABASE_URL (or add DATABASE_URL to .env)" && exit 1); \
7738
docker run --rm --network $(COMPOSE_NETWORK) -v "$(CURDIR)/$(MIGRATIONS_DIR):/migrations" $(MIGRATE_DOCKER_IMAGE) -path /migrations -database "$$db_url" version
7839

79-
migrate-force-docker:
80-
@test -n "$(version)" || (echo "Usage: make migrate-force-docker version=<number>" && exit 1)
81-
@db_url="$${MIGRATE_DATABASE_URL:-$${DATABASE_URL:-$$(grep -E '^DATABASE_URL=' .env 2>/dev/null | head -n1 | cut -d= -f2-)}}"; \
82-
test -n "$$db_url" || (echo "Set MIGRATE_DATABASE_URL or DATABASE_URL (or add DATABASE_URL to .env)" && exit 1); \
83-
docker run --rm --network $(COMPOSE_NETWORK) -v "$(CURDIR)/$(MIGRATIONS_DIR):/migrations" $(MIGRATE_DOCKER_IMAGE) -path /migrations -database "$$db_url" force $(version)
84-
8540
# Build
8641
build: generate
8742
go build -o bin/capy-server ./cmd/server

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ make docker
5454

5555
### 3. Run Migrations & Generate Code
5656
```bash
57-
# If your DB is running via docker-compose on your host machine, override host:
58-
# export MIGRATE_DATABASE_URL=postgres://capy:devpassword@localhost:5432/capy_db?sslmode=disable or use make migrate-up-docker
5957
make migrate-up
6058
make generate
6159
```
6260

61+
`make migrate-up` runs all pending migrations in Docker on the Compose network by default. `make migrate-down` rolls back exactly one migration. `make migrate-version` shows the current version.
62+
6363
Create a new migration:
6464
```bash
6565
make migrate-create name=add_event_capacity
@@ -100,6 +100,8 @@ make run
100100
# Health check: http://localhost:8080/health
101101
```
102102

103+
The API applies pending migrations automatically on startup before serving requests.
104+
103105
## Testing
104106

105107
### Unit Tests

cmd/server/main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ func main() {
5252

5353
ctx := context.Background()
5454

55+
if err := database.RunMigrations(ctx, cfg.Database.URL, cfg.Database.MigrationsPath); err != nil {
56+
slog.Error("failed to run migrations", "error", err, "path", cfg.Database.MigrationsPath)
57+
os.Exit(1)
58+
}
59+
60+
slog.Info("migrations applied", "path", cfg.Database.MigrationsPath)
61+
5562
// Connect to database
5663
pool, err := database.NewPool(ctx, cfg.Database.URL)
5764
if err != nil {

docker-compose.yml

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ services:
1010
volumes:
1111
- pgdata:/var/lib/postgresql/data
1212
healthcheck:
13-
test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}" ]
13+
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
1414
interval: 5s
1515
timeout: 5s
1616
retries: 5
@@ -25,14 +25,5 @@ services:
2525
db:
2626
condition: service_healthy
2727

28-
tunnel:
29-
image: cloudflare/cloudflared:latest
30-
restart: unless-stopped
31-
command: tunnel run
32-
env_file:
33-
- .env
34-
depends_on:
35-
- api
36-
3728
volumes:
3829
pgdata:
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
DROP TRIGGER IF EXISTS update_events_modtime ON events;
2+
DROP TRIGGER IF EXISTS update_orgs_modtime ON organizations;
3+
DROP TRIGGER IF EXISTS update_users_modtime ON users;
4+
5+
DROP INDEX IF EXISTS idx_bot_tokens_active;
6+
7+
DROP TABLE IF EXISTS bot_tokens;
8+
DROP TABLE IF EXISTS event_registrations;
9+
DROP TABLE IF EXISTS event_hosting;
10+
DROP TABLE IF EXISTS events;
11+
DROP TABLE IF EXISTS org_members;
12+
DROP TABLE IF EXISTS organizations;
13+
DROP TABLE IF EXISTS users;
14+
15+
DROP FUNCTION IF EXISTS update_modified_column();
16+
DROP TYPE IF EXISTS user_role;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
-- schema.sql
2+
-- Database Schema for CAPY (Club Assistant in Python)
3+
4+
-- 1. ENUMs & Functions
5+
CREATE TYPE user_role AS ENUM ('student', 'alumni', 'faculty', 'external');
6+
7+
CREATE OR REPLACE FUNCTION update_modified_column()
8+
RETURNS TRIGGER AS $$
9+
BEGIN
10+
NEW.date_modified = CURRENT_DATE;
11+
RETURN NEW;
12+
END;
13+
$$ language 'plpgsql';
14+
15+
-- 2. Tables
16+
CREATE TABLE IF NOT EXISTS users (
17+
uid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
18+
first_name TEXT NOT NULL,
19+
last_name TEXT NOT NULL,
20+
personal_email TEXT UNIQUE,
21+
school_email TEXT UNIQUE,
22+
phone TEXT,
23+
grad_year INT,
24+
role user_role DEFAULT 'student',
25+
date_created DATE DEFAULT CURRENT_DATE,
26+
date_modified DATE DEFAULT CURRENT_DATE
27+
);
28+
29+
CREATE TABLE IF NOT EXISTS organizations (
30+
oid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
31+
name TEXT NOT NULL,
32+
date_created DATE DEFAULT CURRENT_DATE,
33+
date_modified DATE DEFAULT CURRENT_DATE
34+
);
35+
36+
CREATE TABLE IF NOT EXISTS org_members (
37+
uid UUID REFERENCES users(uid) ON DELETE CASCADE,
38+
oid UUID REFERENCES organizations(oid) ON DELETE CASCADE,
39+
is_admin BOOLEAN DEFAULT FALSE,
40+
date_joined DATE DEFAULT CURRENT_DATE,
41+
last_active DATE DEFAULT CURRENT_DATE,
42+
PRIMARY KEY (uid, oid)
43+
);
44+
45+
CREATE TABLE IF NOT EXISTS events (
46+
eid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
47+
location TEXT,
48+
event_time TIMESTAMP,
49+
description TEXT,
50+
date_created DATE DEFAULT CURRENT_DATE,
51+
date_modified DATE DEFAULT CURRENT_DATE
52+
);
53+
54+
CREATE TABLE IF NOT EXISTS event_hosting (
55+
eid UUID REFERENCES events(eid) ON DELETE CASCADE,
56+
oid UUID REFERENCES organizations(oid) ON DELETE CASCADE,
57+
PRIMARY KEY (eid, oid)
58+
);
59+
60+
CREATE TABLE IF NOT EXISTS event_registrations (
61+
uid UUID REFERENCES users(uid) ON DELETE CASCADE,
62+
eid UUID REFERENCES events(eid) ON DELETE CASCADE,
63+
is_attending BOOLEAN DEFAULT FALSE,
64+
is_admin BOOLEAN DEFAULT FALSE,
65+
date_registered DATE DEFAULT CURRENT_DATE,
66+
PRIMARY KEY (uid, eid)
67+
);
68+
69+
-- 3. Bot Tokens (global access for M2M authentication)
70+
CREATE TABLE IF NOT EXISTS bot_tokens (
71+
token_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
72+
token_hash TEXT NOT NULL, -- bcrypt hash of the token
73+
name TEXT NOT NULL, -- human-readable name for the bot
74+
created_by UUID NOT NULL REFERENCES users(uid),
75+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
76+
last_used_at TIMESTAMP,
77+
expires_at TIMESTAMP, -- NULL = never expires
78+
is_active BOOLEAN DEFAULT TRUE
79+
);
80+
81+
CREATE INDEX IF NOT EXISTS idx_bot_tokens_active ON bot_tokens(is_active) WHERE is_active = TRUE;
82+
83+
-- 4. Triggers
84+
DROP TRIGGER IF EXISTS update_users_modtime ON users;
85+
CREATE TRIGGER update_users_modtime BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_modified_column();
86+
87+
DROP TRIGGER IF EXISTS update_orgs_modtime ON organizations;
88+
CREATE TRIGGER update_orgs_modtime BEFORE UPDATE ON organizations FOR EACH ROW EXECUTE FUNCTION update_modified_column();
89+
90+
DROP TRIGGER IF EXISTS update_events_modtime ON events;
91+
CREATE TRIGGER update_events_modtime BEFORE UPDATE ON events FOR EACH ROW EXECUTE FUNCTION update_modified_column();

server

30.9 MB
Binary file not shown.

0 commit comments

Comments
 (0)