diff --git a/README.md b/README.md index 632ab44..501d3e8 100644 --- a/README.md +++ b/README.md @@ -128,10 +128,10 @@ CodeForge’s goal is to provide a *simpler, clarity-first alternative* to platf - Lightweight CSS (“CodeForge UI”) without a large framework - ### Database - - MySQL (local/dev tests and prod) + - PostgreSQL (Neon for production, local for dev/tests) - Test DB reset via `DbReset` + `cleandb.sql` - Seed data managed in `src/test/resources/cleandb.sql` (predictable schema) - - **AWS RDS** — Cloud-hosted DB for deployment + - **Neon (Postgres)** — Cloud-hosted serverless DB for deployment - ### Authentication & Security - Amazon Cognito Hosted UI (servlet-based flow) diff --git a/docs/deployment.md b/docs/deployment.md index 0cf4db1..82b0dd4 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -3,8 +3,8 @@ This project runs as a WAR on Tomcat 9 (Jakarta Servlet API). The persistence layer uses Hibernate directly (no Spring). For MVP, schema is managed by Hibernate auto‑DDL; Flyway is a post‑MVP task. ## Environments -- Dev/Test: H2 in‑memory DB via `src/main/resources/hibernate.cfg.xml` -- Prod (Week 8): AWS RDS (PostgreSQL or MySQL) +- Dev/Test: PostgreSQL (local or Docker) +- Prod: Neon (serverless PostgreSQL) ## Required runtime configuration - Cognito secret: provided by environment or JVM property @@ -18,23 +18,18 @@ This project runs as a WAR on Tomcat 9 (Jakarta Servlet API). The persistence la ### Dev/Test (default) - `src/main/resources/hibernate.cfg.xml` points to H2 and sets `hbm2ddl.auto=update`. -### Production (RDS) -Use either approach: -1) Tomcat JNDI DataSource (recommended) - - Define a JNDI resource in Tomcat `conf/context.xml` or the webapp context `META-INF/context.xml`: - ```xml - - ``` - - Update your Hibernate configuration to use JNDI lookup (optional enhancement), or provide a prod `hibernate.cfg.xml` packaged for deployment. - -2) Prod `hibernate.cfg.xml` - - Build a production variant of `hibernate.cfg.xml` with your RDS JDBC URL, user, and password. - - First deploy: set `update` - - After verification: change to `validate` and redeploy +### Production (Neon PostgreSQL) +Use environment variables or system properties: +- `DB_HOST`: Neon PostgreSQL endpoint (e.g., `ep-xyz.us-east-1.aws.neon.tech`) +- `DB_PORT`: `5432` (default PostgreSQL port) +- `DB_NAME`: Database name +- `DB_USER`: Database username +- `DB_PASS`: Database password + +Connection string format: +``` +jdbc:postgresql://:5432/?ssl=true&sslmode=require +``` ## Balanced Approach (MVP) - Phase 1 (first deploy): allow Hibernate to create/update tables with `hbm2ddl.auto=update` @@ -58,13 +53,12 @@ cmd /c 'cd /d "C:\Users\nickh\Documents\My Projects\Java\code-forge" && mvn -q - - Or add to Tomcat service/`setenv` as `-DCOGNITO_CLIENT_SECRET=your_secret` - Start Tomcat and open `http://localhost:8080/codeforge/` -## RDS specifics -- Create DB (PostgreSQL/MySQL), note endpoint, user, password -- Security groups: allow inbound from your app host -- JDBC URL examples: - - Postgres: `jdbc:postgresql://:5432/` - - MySQL: `jdbc:mysql://:3306/?useSSL=true&requireSSL=true` -- On first deploy, keep DDL `update`; after verifying tables (`challenges`, `submissions`, `drill_items`), flip to `validate` +## RDS/Neon specifics +- Neon provides serverless PostgreSQL with automatic scaling +- Security: Neon requires SSL (`ssl=true&sslmode=require`) +- JDBC URL example: + - Postgres/Neon: `jdbc:postgresql://:5432/?ssl=true&sslmode=require` +- On first deploy, Hibernate can auto-create schema with `hbm2ddl.auto=update`; after verifying tables (`challenges`, `submissions`, `drill_items`), consider switching to `validate` ## QuoteService notes - `QuoteService` loads `application.properties` from the classpath and uses `HttpClient` for outbound calls @@ -90,48 +84,41 @@ For production (AWS RDS), override datasource and JPA properties with environmen --- -## 1) Provision AWS RDS - -- Choose engine/version (e.g., PostgreSQL 15.x or MySQL 8.x). -- Create database (DB name, username, password). -- Configure security group rules to allow inbound from your app host (e.g., Elastic Beanstalk instance SG). -- Note the JDBC endpoint: `host:port/dbname`. +## 1) Provision Neon Database -## 2) Configure application (first deploy) +- Create a new project in Neon (https://neon.tech) +- Note the connection details: host, database name, username, password +- Neon automatically provides SSL-enabled PostgreSQL endpoints -Set the following environment variables in your hosting environment (Elastic Beanstalk, ECS, EC2, etc.), or for an one‑off local test run: +## 2) Configure application (Render deployment) -- `SPRING_DATASOURCE_URL` - - PostgreSQL: `jdbc:postgresql://:/?sslmode=require` - - MySQL: `jdbc:mysql://:/?allowPublicKeyRetrieval=true&useSSL=true&requireSSL=true&useUnicode=true&characterEncoding=utf8` -- `SPRING_DATASOURCE_USERNAME` -- `SPRING_DATASOURCE_PASSWORD` -- `SPRING_JPA_HIBERNATE_DDL_AUTO=update` (first deploy only — lets Hibernate create missing tables/columns) +Set the following environment variables in Render: -Example one‑shot local run on Windows cmd: +- `DB_HOST`: Neon endpoint (e.g., `ep-xyz.us-east-1.aws.neon.tech`) +- `DB_PORT`: `5432` +- `DB_NAME`: Database name +- `DB_USER`: Database username +- `DB_PASS`: Database password -```bat -cmd /c 'set "SPRING_DATASOURCE_URL=jdbc:postgresql://mydb.abcdefg.us-east-1.rds.amazonaws.com:5432/codeforge?sslmode=require" && set "SPRING_DATASOURCE_USERNAME=appuser" && set "SPRING_DATASOURCE_PASSWORD=secret" && set "SPRING_JPA_HIBERNATE_DDL_AUTO=update" && mvn -q spring-boot:run' +The application will build the JDBC URL automatically with SSL enabled: +``` +jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}?ssl=true&sslmode=require ``` - -Notes -- The app already externalizes server port via `server.port=${PORT:5000}`. -- Keep test/dev profiles pointed at H2; only prod uses RDS overrides. ## 3) Deploy and verify -- Deploy the app to your AWS environment with the env vars above. -- Verify health: - - `GET /actuator/health` → `UP` +- Deploy the app to Render with the environment variables above. +- Hibernate will auto-create the schema on first startup (using entity annotations). - Verify schema: - - Check RDS tables exist: `challenges`, `submissions`, `drill_items`, including `drill_items.version` (added in Issue 38 via `@Version`). + - Connect to Neon and check that tables exist: `challenges`, `submissions`, `drill_items`, including `drill_items.version` (added via `@Version`). - Smoke test the app (e.g., run a Drill flow to insert data). -## 4) Flip ddl-auto to validate +## 4) Schema management notes -Once the schema looks correct in RDS: -- Change environment variable: `SPRING_JPA_HIBERNATE_DDL_AUTO=validate` -- Redeploy the app. Hibernate will now validate the schema at startup and fail fast if it drifts, but it will no longer apply changes automatically. +- Hibernate uses entity annotations to create/validate schema +- First deploy: Hibernate will auto-create tables based on `@Entity` classes +- After verification: you can set `hibernate.hbm2ddl.auto=validate` to prevent auto-changes +- Future: consider Flyway for versioned migrations (post-MVP) ## 5) Post‑MVP: Adopt Flyway migrations @@ -147,16 +134,20 @@ After MVP deployment stabilizes: ## 6) Troubleshooting - `relation/table not found` at startup: - - You likely deployed with `ddl-auto=validate` before the schema existed; switch back to `update` for the first run. -- Permission errors creating tables: - - Ensure the RDS user has `CREATE`/`ALTER` permissions. + - Ensure Hibernate can create tables (check DB user permissions). - Driver/URL errors: - - Confirm the correct JDBC driver is on the classpath (Spring Boot starters include both Postgres/MySQL when added) and that the JDBC URL is correct for your engine. + - Confirm PostgreSQL JDBC driver is in dependencies and JDBC URL is correct. - Connection timeouts: - - Check RDS SG rules, VPC networking, and that your app host can reach the RDS endpoint. - -## 7) Quick rollback plan - -- Keep `ddl-auto` in an env var so you can temporarily switch back to `update` if a small non‑destructive patch is needed. -- Prefer to schedule structural changes; adopt Flyway promptly post‑MVP for safer, versioned changes. + - Verify DB_HOST, DB_PORT, and that Neon endpoint is accessible from Render. + - Check that SSL is enabled (`ssl=true&sslmode=require`). + +## 7) Migration from AWS RDS MySQL + +If migrating from existing MySQL database: +1. Export data from MySQL using `mysqldump` or similar +2. Convert MySQL syntax to PostgreSQL (AUTO_INCREMENT → SERIAL, ENUM → VARCHAR/CHECK, etc.) +3. Import into Neon using `psql` or database client +4. Update Render environment variables to point to Neon +5. Deploy and verify +6. After successful verification, delete AWS RDS instance diff --git a/pom.xml b/pom.xml index 9d5f05c..670ffe3 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ 2.0.1.Final 6.2.5.Final 3.0.1-b11 - 8.4.0 + 42.7.7 3.13.0 3.2.5 @@ -164,11 +164,11 @@ ${javax.el.version} - + - com.mysql - mysql-connector-j - ${mysql.connector.version} + org.postgresql + postgresql + ${postgresql.connector.version} diff --git a/src/main/java/me/nickhanson/codeforge/persistence/SessionFactoryProvider.java b/src/main/java/me/nickhanson/codeforge/persistence/SessionFactoryProvider.java index 0f439ae..12984f3 100644 --- a/src/main/java/me/nickhanson/codeforge/persistence/SessionFactoryProvider.java +++ b/src/main/java/me/nickhanson/codeforge/persistence/SessionFactoryProvider.java @@ -60,11 +60,11 @@ private static SessionFactory buildSessionFactory() { url = explicitUrl; } else { String host = resolve("DB_HOST", "localhost"); - String port = resolve("DB_PORT", "3306"); + String port = resolve("DB_PORT", "5432"); String db = resolve("DB_NAME", "cf_test_db"); - url = "jdbc:mysql://" + host + ":" + port + "/" + db - + "?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC"; + url = "jdbc:postgresql://" + host + ":" + port + "/" + db + + "?ssl=true&sslmode=require"; } // Username: hibernate prop -> DB_USER -> default "root" @@ -91,14 +91,14 @@ private static SessionFactory buildSessionFactory() { // Dialect: can still be overridden if needed String dialect = resolve( "hibernate.dialect", - "org.hibernate.dialect.MySQL8Dialect" + "org.hibernate.dialect.PostgreSQLDialect" ); - // Ensure MySQL JDBC driver is present + // Ensure PostgreSQL JDBC driver is present try { - Class.forName("com.mysql.cj.jdbc.Driver"); + Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException e) { - throw new IllegalStateException("MySQL JDBC driver not found on classpath", e); + throw new IllegalStateException("PostgreSQL JDBC driver not found on classpath", e); } // Log *non-secret* connection info diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 540dacc..4487dd4 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -12,16 +12,16 @@ VALUES ('Median of Two Sorted Arrays', 'HARD', 'Find median of two sorted arrays.', 'Given two sorted arrays nums1 and nums2, return the median of the two sorted arrays.'); -- Seed submissions for a few challenges (dev only) -INSERT INTO SUBMISSIONS (CHALLENGE_ID, OUTCOME, CODE) +INSERT INTO SUBMISSIONS (CHALLENGE_ID, USER_ID, OUTCOME, CODE) VALUES - (1, 'CORRECT', 'int[] twoSum(int[] nums, int target) { /* ... */ }'), - (1, 'INCORRECT', '/* first attempt */'), - (2, 'ACCEPTABLE', 'boolean isValid(String s) { /* ... */ }'), - (3, 'SKIPPED', NULL); + (1, 'demo', 'CORRECT', 'int[] twoSum(int[] nums, int target) { /* ... */ }'), + (1, 'demo', 'INCORRECT', '/* first attempt */'), + (2, 'demo', 'ACCEPTABLE', 'boolean isValid(String s) { /* ... */ }'), + (3, 'demo', 'SKIPPED', NULL); -- Seed drill items for a few challenges (dev only) -INSERT INTO DRILL_ITEMS (CHALLENGE_ID, TIMES_SEEN, STREAK, NEXT_DUE_AT) +INSERT INTO DRILL_ITEMS (CHALLENGE_ID, USER_ID, TIMES_SEEN, STREAK, NEXT_DUE_AT) VALUES - (1, 2, 1, NULL), - (2, 1, 0, NULL), - (3, 3, 2, NULL); \ No newline at end of file + (1, 'demo', 2, 1, NULL), + (2, 'demo', 1, 0, NULL), + (3, 'demo', 3, 2, NULL); \ No newline at end of file diff --git a/src/main/resources/hibernate.cfg.xml b/src/main/resources/hibernate.cfg.xml index 1222aba..27e37f7 100644 --- a/src/main/resources/hibernate.cfg.xml +++ b/src/main/resources/hibernate.cfg.xml @@ -4,25 +4,21 @@ "https://hibernate.org/dtd/hibernate-configuration-3.0.dtd"> - - com.mysql.cj.jdbc.Driver - + + org.postgresql.Driver + - jdbc:mysql://${DB_HOST}:3306/${DB_NAME} - ?useSSL=false - &allowPublicKeyRetrieval=true - &serverTimezone=UTC - &useUnicode=true - &characterEncoding=utf8mb4 - &connectionCollation=utf8mb4_unicode_ci + jdbc:postgresql://${DB_HOST}:5432/${DB_NAME} + ?ssl=true + &sslmode=require ${DB_USER} ${DB_PASS} - - org.hibernate.dialect.MySQL8Dialect + + org.hibernate.dialect.PostgreSQLDialect true diff --git a/src/test/java/me/nickhanson/codeforge/testutil/Database.java b/src/test/java/me/nickhanson/codeforge/testutil/Database.java index 0cb0220..201626f 100644 --- a/src/test/java/me/nickhanson/codeforge/testutil/Database.java +++ b/src/test/java/me/nickhanson/codeforge/testutil/Database.java @@ -50,7 +50,7 @@ public void connect() throws Exception { try { Class.forName(properties.getProperty("hibernate.connection.driver_class")); } catch (ClassNotFoundException e) { - throw new Exception("MySQL Driver not found", e); + throw new Exception("Database Driver not found", e); } String url = properties.getProperty("hibernate.connection.url"); diff --git a/src/test/resources/cleandb.sql b/src/test/resources/cleandb.sql index c6aacf5..96a347e 100644 --- a/src/test/resources/cleandb.sql +++ b/src/test/resources/cleandb.sql @@ -1,42 +1,38 @@ -- Purpose: Reset schema and seed sample data for local/dev testing --- MySQL version +-- PostgreSQL version --- Disable FK checks for dropping -SET FOREIGN_KEY_CHECKS = 0; - -DROP TABLE IF EXISTS submissions; -DROP TABLE IF EXISTS drill_items; -DROP TABLE IF EXISTS challenges; - -SET FOREIGN_KEY_CHECKS = 1; +-- Drop tables (CASCADE handles foreign keys automatically) +DROP TABLE IF EXISTS submissions CASCADE; +DROP TABLE IF EXISTS drill_items CASCADE; +DROP TABLE IF EXISTS challenges CASCADE; -- ------------------------------------------ -- Recreate tables -- ------------------------------------------ CREATE TABLE challenges ( - id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, title VARCHAR(255) NOT NULL, blurb TEXT, prompt_md TEXT, expected_answer TEXT, - difficulty ENUM('EASY', 'MEDIUM', 'HARD') NOT NULL, + difficulty VARCHAR(20) NOT NULL CHECK (difficulty IN ('EASY', 'MEDIUM', 'HARD')), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - UNIQUE KEY uk_challenges_title (title) + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT uk_challenges_title UNIQUE (title) ); CREATE TABLE drill_items ( - id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, challenge_id BIGINT NOT NULL, user_id VARCHAR(64) NOT NULL, -- MVP: string-based user identifier (e.g., 'demo' or Cognito sub) times_seen INT NOT NULL DEFAULT 0, streak INT NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, next_due_at TIMESTAMP NULL DEFAULT NULL, version BIGINT NOT NULL DEFAULT 0, - UNIQUE KEY uk_drill_items_user_challenge (user_id, challenge_id), + CONSTRAINT uk_drill_items_user_challenge UNIQUE (user_id, challenge_id), CONSTRAINT fk_drill_items_challenge FOREIGN KEY (challenge_id) REFERENCES challenges(id) @@ -45,13 +41,13 @@ CREATE TABLE drill_items ( ); CREATE TABLE submissions ( - id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, challenge_id BIGINT NOT NULL, user_id VARCHAR(64) NOT NULL, -- MVP: string-based user identifier (e.g., 'demo' or Cognito sub) - outcome ENUM('CORRECT', 'INCORRECT', 'ACCEPTABLE', 'SKIPPED') NOT NULL, + outcome VARCHAR(20) NOT NULL CHECK (outcome IN ('CORRECT', 'INCORRECT', 'ACCEPTABLE', 'SKIPPED')), code TEXT, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_submissions_challenge FOREIGN KEY (challenge_id) REFERENCES challenges(id) diff --git a/src/test/resources/test-db.properties.example b/src/test/resources/test-db.properties.example new file mode 100644 index 0000000..90c44f6 --- /dev/null +++ b/src/test/resources/test-db.properties.example @@ -0,0 +1,9 @@ +# PostgreSQL connection settings for local testing +# Copy this file to test-db.properties and update with your local PostgreSQL credentials +# test-db.properties is git-ignored to prevent committing secrets + +hibernate.connection.driver_class=org.postgresql.Driver +hibernate.connection.url=jdbc:postgresql://localhost:5432/cf_test_db?ssl=false +hibernate.connection.username=postgres +hibernate.connection.password=your_password_here +hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect