Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ DATABASE_URL_GCP=****
DATABASE_URL_LOCAL=postgresql://postgres:postgres@localhost:5433/f3_compare
DATABASE_URL_NEON=****
DATABASE_URL_SUPABASE=****
DATABASE_URL_METADATA=****
APP_ENVIRONMENT=local
CRON_SECRET=****
19 changes: 13 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,27 @@ npm run lint # ESLint (--max-warnings 0)
npm run typecheck # TypeScript strict check
npm run test # Vitest (9 tests)
npm run test:coverage # Vitest with v8 coverage
npm run docker:up # Start local Postgres 16 on :5433
npm run docker:down # Stop local Postgres
npm run db:up # Start local Postgres 16 on :5433
npm run db:down # Stop local Postgres
npm run db:pull:dump # tsx script: pull schema + data from GCP
npm run db:push:schema:local # Apply schema.sql to local Docker
npm run db:push:data:local # Apply data.sql to local Docker
npm run db:reset:<target> # Drop all + re-migrate + seed (local/neon/supabase, NEVER gcp)
npm run db:migrate:<target> # Apply schema.sql (local/neon/supabase)
npm run db:seed:<target> # Apply data.sql (local/neon/supabase)
npm run db:verify:local # Health check local Docker
npm run db:verify:gcp # Health check GCP
npm run db:verify:neon # Health check Neon
npm run db:verify:supabase # Health check Supabase
npm run db:generate # Drizzle generate migrations
npm run db:migrate:metadata # Push schema to metadata DB
npm run db:studio # Drizzle Studio GUI
```

## Stack

- Next.js 15, React 19, App Router, TypeScript strict
- shadcn/ui + Tailwind v3 + Recharts
- Raw `pg` Pool per platform (no ORM at runtime)
- Raw `pg` Pool per platform (no ORM for comparison queries)
- Drizzle ORM for metadata DB (latency analytics)
- Docker Postgres 16 on :5433, app on :3002

## Plugin System
Expand All @@ -48,10 +53,12 @@ npm run db:verify:supabase # Health check Supabase
- `POST /api/query` β€” {platformId, sql} -> QueryResult
- `POST /api/compare` β€” {leftId, rightId, sql} -> side-by-side
- `POST /api/compare/schema` β€” {leftId, rightId} -> schema diff
- `POST /api/cron/latency` β€” Collect health snapshots (QStash target)
- `GET /api/analytics/latency` β€” Query latency history + stats

## Env Variables

`DATABASE_URL_GCP`, `DATABASE_URL_LOCAL`, `DATABASE_URL_NEON`, `DATABASE_URL_SUPABASE`
`DATABASE_URL_GCP`, `DATABASE_URL_LOCAL`, `DATABASE_URL_NEON`, `DATABASE_URL_SUPABASE`, `DATABASE_URL_METADATA`, `APP_ENVIRONMENT`, `CRON_SECRET`

## Database Stats (GCP Source)

Expand Down
16 changes: 16 additions & 0 deletions apphosting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,19 @@ env:
availability:
- BUILD
- RUNTIME

- variable: DATABASE_URL_METADATA
secret: database-url-metadata
availability:
- BUILD
- RUNTIME

- variable: APP_ENVIRONMENT
value: firebase
availability:
- RUNTIME

- variable: CRON_SECRET
secret: cron-secret
availability:
- RUNTIME
6 changes: 5 additions & 1 deletion drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { config } from "dotenv";
import { defineConfig } from "drizzle-kit";

config({ path: ".env.local" });

export default defineConfig({
dialect: "postgresql",
schema: "./src/lib/db/schema.ts",
dbCredentials: {
url: process.env.DATABASE_URL_GCP!,
url: process.env.DATABASE_URL_METADATA!,
},
out: "./drizzle",
});
2 changes: 1 addition & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
serverExternalPackages: ["pg"],
serverExternalPackages: ["pg", "drizzle-orm"],
};

export default nextConfig;
9 changes: 4 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 16 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,27 @@
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"docker:up": "docker compose up -d",
"docker:down": "docker compose down",
"docker:reset": "docker compose down -v && docker compose up -d",
"db:up": "docker compose up -d",
"db:down": "docker compose down",
"db:reset:local": "tsx scripts/db-reset.ts local",
"db:reset:neon": "tsx scripts/db-reset.ts neon",
"db:reset:supabase": "tsx scripts/db-reset.ts supabase",
"db:pull:schema": "tsx scripts/db-pull-schema.ts",
"db:pull:dump": "tsx scripts/db-pull-dump.ts",
"db:push:schema:local": "bash scripts/db-push-schema.sh local",
"db:push:schema:neon": "bash scripts/db-push-schema.sh neon",
"db:push:schema:supabase": "bash scripts/db-push-schema.sh supabase",
"db:push:data:local": "bash scripts/db-push-data.sh local",
"db:push:data:neon": "bash scripts/db-push-data.sh neon",
"db:push:data:supabase": "bash scripts/db-push-data.sh supabase",
"db:migrate:local": "bash scripts/db-migrate.sh local",
"db:migrate:neon": "bash scripts/db-migrate.sh neon",
"db:migrate:supabase": "bash scripts/db-migrate.sh supabase",
"db:seed:local": "bash scripts/db-seed.sh local",
"db:seed:neon": "bash scripts/db-seed.sh neon",
"db:seed:supabase": "bash scripts/db-seed.sh supabase",
"db:verify:local": "tsx scripts/db-verify.ts local",
"db:verify:neon": "tsx scripts/db-verify.ts neon",
"db:verify:supabase": "tsx scripts/db-verify.ts supabase",
"db:verify:gcp": "tsx scripts/db-verify.ts gcp",
"firebase:env": "bash scripts/firebase-env.sh"
"firebase:env": "bash scripts/firebase-env.sh",
"db:generate": "drizzle-kit generate",
"db:migrate:metadata": "drizzle-kit push",
"db:studio": "drizzle-kit studio"
},
"dependencies": {
"@radix-ui/react-dialog": "^1.1.15",
Expand All @@ -39,6 +44,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.4",
"drizzle-orm": "^0.45.1",
"lucide-react": "^0.563.0",
"next": "16.1.6",
"pg": "^8.18.0",
Expand All @@ -55,7 +61,6 @@
"@types/react-dom": "^19",
"@vitest/coverage-v8": "^4.0.18",
"drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.45.1",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"prettier": "^3.8.1",
Expand Down
2 changes: 1 addition & 1 deletion scripts/db-push-schema.sh β†’ scripts/db-migrate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ SCHEMA_FILE="$DUMPS_DIR/schema.sql"
TARGET="${1:-}"

if [[ -z "$TARGET" ]]; then
echo "Usage: db-push-schema.sh <target>"
echo "Usage: db-migrate.sh <target>"
echo "Targets: local, neon, supabase"
exit 1
fi
Expand Down
125 changes: 125 additions & 0 deletions scripts/db-reset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { Pool } from "pg";
import { resolve } from "path";
import { config } from "dotenv";
import { execSync } from "child_process";
import { createInterface } from "readline";

// Load .env.local
config({ path: resolve(__dirname, "../.env.local") });

const target = process.argv[2];

if (!target) {
console.error("Usage: db-reset.ts <target>");
console.error("Targets: local, neon, supabase");
process.exit(1);
}

if (target === "gcp") {
console.error(
"ERROR: Refusing to reset GCP β€” it is the read-only source of truth.",
);
process.exit(1);
}

const envKeyMap: Record<string, string> = {
local: "DATABASE_URL_LOCAL",
neon: "DATABASE_URL_NEON",
supabase: "DATABASE_URL_SUPABASE",
};

const envKey = envKeyMap[target];
if (!envKey) {
console.error(`Unknown target: ${target}. Use: local, neon, supabase`);
process.exit(1);
}

const connectionString = process.env[envKey];
if (!connectionString) {
console.error(`${envKey} is not set. Add it to .env.local`);
process.exit(1);
}

function confirm(message: string): Promise<boolean> {
const rl = createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(`${message} [y/N] `, (answer) => {
rl.close();
resolve(answer.trim().toLowerCase() === "y");
});
});
}

function run(cmd: string) {
console.log(`$ ${cmd}`);
execSync(cmd, { stdio: "inherit", cwd: resolve(__dirname, "..") });
}

async function resetLocal() {
console.log("Tearing down Docker volumes and restarting...");
run("docker compose down -v");
run("docker compose up -d");

// Wait for pg_isready
console.log("Waiting for Postgres to be ready...");
const maxAttempts = 30;
for (let i = 0; i < maxAttempts; i++) {
try {
execSync("docker compose exec -T postgres pg_isready -U postgres", {
stdio: "pipe",
cwd: resolve(__dirname, ".."),
});
console.log("Postgres is ready.");
break;
} catch {
if (i === maxAttempts - 1) {
console.error("Postgres did not become ready in time.");
process.exit(1);
}
await new Promise((r) => setTimeout(r, 1000));
}
}

run("bash scripts/db-migrate.sh local");
run("bash scripts/db-seed.sh local");
}

async function resetRemote() {
const pool = new Pool({ connectionString, connectionTimeoutMillis: 10000 });

try {
console.log(`Dropping schemas on ${target}...`);
await pool.query("DROP SCHEMA IF EXISTS public CASCADE");
await pool.query("DROP SCHEMA IF EXISTS codex CASCADE");
await pool.query("DROP SCHEMA IF EXISTS regionpages CASCADE");
await pool.query("CREATE SCHEMA public");
console.log("Schemas dropped and public recreated.");
} finally {
await pool.end();
}

run(`bash scripts/db-migrate.sh ${target}`);
run(`bash scripts/db-seed.sh ${target}`);
}

async function main() {
const confirmed = await confirm(
`Reset ${target}? This will destroy ALL data.`,
);
if (!confirmed) {
console.log("Aborted.");
process.exit(0);
}

console.log(`\nResetting ${target}...`);

if (target === "local") {
await resetLocal();
} else {
await resetRemote();
}

console.log(`\n${target} reset complete.`);
}

main();
2 changes: 1 addition & 1 deletion scripts/db-push-data.sh β†’ scripts/db-seed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ DATA_FILE="$DUMPS_DIR/data.sql"
TARGET="${1:-}"

if [[ -z "$TARGET" ]]; then
echo "Usage: db-push-data.sh <target>"
echo "Usage: db-seed.sh <target>"
echo "Targets: local, neon, supabase"
exit 1
fi
Expand Down
4 changes: 2 additions & 2 deletions scripts/firebase-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ if [[ -f "$HOME/google-cloud-sdk/path.bash.inc" ]]; then
fi

# Configuration constants
SECRET_VARS=("DATABASE_URL_GCP" "DATABASE_URL_NEON" "DATABASE_URL_SUPABASE")
SECRET_IDS=("database-url-gcp" "database-url-neon" "database-url-supabase")
SECRET_VARS=("DATABASE_URL_GCP" "DATABASE_URL_NEON" "DATABASE_URL_SUPABASE" "DATABASE_URL_METADATA" "CRON_SECRET")
SECRET_IDS=("database-url-gcp" "database-url-neon" "database-url-supabase" "database-url-metadata" "cron-secret")

#####################################
# MAIN EXECUTION FUNCTION
Expand Down
Loading