From 0452068dfa40a44d5eee875624ab627ebd3f1af4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:00:33 +0000 Subject: [PATCH 1/5] Initial plan From 09ef84f2b98298dbe7c20b6db9ff78f32bb72b41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:04:19 +0000 Subject: [PATCH 2/5] feat: move database migrations to separate pre-deployment step Co-authored-by: 0ghost0-dev <70481054+0ghost0-dev@users.noreply.github.com> --- .dockerignore | 1 - Dockerfile | 1 + docker-entrypoint.sh | 10 +++- package.json | 2 +- scripts/migrate.js | 127 +++++++++++++++++++++++++++++++++++++++++++ src/hooks.server.ts | 3 +- 6 files changed, 139 insertions(+), 5 deletions(-) create mode 100755 scripts/migrate.js diff --git a/.dockerignore b/.dockerignore index fbe4336..aa00244 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,7 +15,6 @@ dist pgdata redis-data logs -drizzle # Environment .env diff --git a/Dockerfile b/Dockerfile index 3727263..a88409b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,6 +61,7 @@ COPY --from=builder --chown=node:node /usr/src/app/docker-entrypoint.sh ./docker COPY --from=builder --chown=node:node /usr/src/app/.env.example ./.env.example COPY --from=builder --chown=node:node /usr/src/app/exchanges ./exchanges COPY --from=builder --chown=node:node /usr/src/app/drizzle ./drizzle +COPY --from=builder --chown=node:node /usr/src/app/scripts ./scripts RUN chmod +x /usr/src/app/docker-entrypoint.sh diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 858043a..2c19eab 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -67,5 +67,13 @@ until pg_isready -h "${POSTGRESQL_HOST:-postgres}" -p "${POSTGRESQL_PORT:-5432}" sleep 1 done -echo "Postgres is available. Starting app..." +echo "Postgres is available. Running database migrations..." +node /usr/src/app/scripts/migrate.js + +if [ $? -ne 0 ]; then + echo "ERROR: Database migrations failed!" + exit 1 +fi + +echo "Database migrations completed successfully. Starting app..." exec "$@" \ No newline at end of file diff --git a/package.json b/package.json index 951b14a..85f0ee3 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "drizzle:generate": "npx drizzle-kit generate", "drizzle:generate:custom": "npx drizzle-kit generate --custom", - "drizzle:migrate": "npx drizzle-kit migrate", + "drizzle:migrate": "node scripts/migrate.js", "drizzle:push": "npx drizzle-kit push", "drizzle:studio": "npx drizzle-kit studio", "start": "node --env-file=.env build" diff --git a/scripts/migrate.js b/scripts/migrate.js new file mode 100755 index 0000000..720566d --- /dev/null +++ b/scripts/migrate.js @@ -0,0 +1,127 @@ +#!/usr/bin/env node +/** + * Standalone migration script + * Runs database migrations independently from application startup + */ + +import postgres from 'postgres'; +import { readdir, readFile } from 'fs/promises'; +import { join } from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Simple logger for migration script +const logger = { + info: (...args) => console.log('[INFO]', ...args), + error: (...args) => console.error('[ERROR]', ...args), + debug: (...args) => console.log('[DEBUG]', ...args) +}; + +// Read environment variables +const POSTGRESQL_USER = process.env.POSTGRESQL_USER || 'postgres'; +const POSTGRESQL_PASSWORD = process.env.POSTGRESQL_PASSWORD || 'postgres'; +const POSTGRESQL_HOST = process.env.POSTGRESQL_HOST || 'localhost'; +const POSTGRESQL_PORT = process.env.POSTGRESQL_PORT || '5432'; +const POSTGRESQL_NAME = process.env.POSTGRESQL_NAME || 'pjsedb'; +const POSTGRESQL_SSL = process.env.POSTGRESQL_SSL || 'disable'; + +const encodedPassword = encodeURIComponent(POSTGRESQL_PASSWORD); +const dbUrl = `postgres://${POSTGRESQL_USER}:${encodedPassword}@${POSTGRESQL_HOST}:${POSTGRESQL_PORT}/${POSTGRESQL_NAME}`; + +const msToSec = (v) => v ? Math.max(1, Math.ceil(v / 1000)) : undefined; + +// Create PostgreSQL client +const postgresClient = postgres(dbUrl, { + max: 10, + idle_timeout: msToSec(10000), + max_lifetime: msToSec(60000), + ssl: POSTGRESQL_SSL === 'disable' ? false : POSTGRESQL_SSL, +}); + +/** + * Run database migrations + */ +async function runMigrations() { + const migrationFolder = join(dirname(__dirname), 'drizzle'); + + try { + // Test database connection + await postgresClient\`SELECT 1\`; + logger.info('Connected to PostgreSQL'); + + // Create migrations meta table + await postgresClient\` + CREATE TABLE IF NOT EXISTS drizzle_migrations ( + id SERIAL PRIMARY KEY, + hash TEXT NOT NULL, + created_at BIGINT + ) + \`; + + // Read migration files + const files = await readdir(migrationFolder); + const sqlFiles = files + .filter(f => f.endsWith('.sql')) + .sort(); // Sort by filename + + if (sqlFiles.length === 0) { + logger.info('No migration files found'); + return; + } + + logger.info(\`Found \${sqlFiles.length} migration file(s)\`); + + for (const file of sqlFiles) { + const filePath = join(migrationFolder, file); + const sql = await readFile(filePath, 'utf-8'); + + // Check if migration already applied + const [existing] = await postgresClient\` + SELECT id FROM drizzle_migrations WHERE hash = \${file} + \`; + + if (!existing) { + logger.info(\`Running migration: \${file}\`); + + // Execute in transaction + await postgresClient.begin(async (tx) => { + // Execute SQL + await tx.unsafe(sql); + + // Record execution + await tx.unsafe( + 'INSERT INTO drizzle_migrations (hash, created_at) VALUES ($1, $2)', + [file, Date.now()] + ); + }); + + logger.info(\`Migration completed: \${file}\`); + } else { + logger.debug(\`Migration already applied: \${file}\`); + } + } + + logger.info('All migrations completed successfully'); + } catch (error) { + logger.error('Migration error:', error); + throw error; + } finally { + // Close connection + await postgresClient.end({ timeout: 5 }); + logger.info('PostgreSQL connection closed'); + } +} + +// Run migrations and exit +runMigrations() + .then(() => { + logger.info('Migration script completed successfully'); + process.exit(0); + }) + .catch((error) => { + logger.error('Migration script failed:', error); + process.exit(1); + }); diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 548de62..c48413b 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,6 +1,6 @@ import type { Handle, ServerInit } from '@sveltejs/kit'; import { paraglideMiddleware } from '$lib/paraglide/server'; -import { initPostgres, runMigrations } from "$lib/server/postgresql/db"; +import { initPostgres } from "$lib/server/postgresql/db"; import { initRedis } from "$lib/server/redis/db"; import { sequence } from "@sveltejs/kit/hooks"; import { Cron } from "croner"; @@ -26,7 +26,6 @@ export const init: ServerInit = async () => { await initPostgres(); await initRedis(); - await runMigrations(); if (!newLoad) { newLoad = true; From f4c30babc82abba9a0de08b8a285ba76f59bbb9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:04:56 +0000 Subject: [PATCH 3/5] fix: correct template literal syntax in migration script Co-authored-by: 0ghost0-dev <70481054+0ghost0-dev@users.noreply.github.com> --- scripts/migrate.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/migrate.js b/scripts/migrate.js index 720566d..10ab49c 100755 --- a/scripts/migrate.js +++ b/scripts/migrate.js @@ -49,17 +49,17 @@ async function runMigrations() { try { // Test database connection - await postgresClient\`SELECT 1\`; + await postgresClient`SELECT 1`; logger.info('Connected to PostgreSQL'); // Create migrations meta table - await postgresClient\` + await postgresClient` CREATE TABLE IF NOT EXISTS drizzle_migrations ( id SERIAL PRIMARY KEY, hash TEXT NOT NULL, created_at BIGINT ) - \`; + `; // Read migration files const files = await readdir(migrationFolder); @@ -72,19 +72,19 @@ async function runMigrations() { return; } - logger.info(\`Found \${sqlFiles.length} migration file(s)\`); + logger.info(`Found ${sqlFiles.length} migration file(s)`); for (const file of sqlFiles) { const filePath = join(migrationFolder, file); const sql = await readFile(filePath, 'utf-8'); // Check if migration already applied - const [existing] = await postgresClient\` - SELECT id FROM drizzle_migrations WHERE hash = \${file} - \`; + const [existing] = await postgresClient` + SELECT id FROM drizzle_migrations WHERE hash = ${file} + `; if (!existing) { - logger.info(\`Running migration: \${file}\`); + logger.info(`Running migration: ${file}`); // Execute in transaction await postgresClient.begin(async (tx) => { @@ -98,9 +98,9 @@ async function runMigrations() { ); }); - logger.info(\`Migration completed: \${file}\`); + logger.info(`Migration completed: ${file}`); } else { - logger.debug(\`Migration already applied: \${file}\`); + logger.debug(`Migration already applied: ${file}`); } } From a4b4adcab742e48904739b03d60e163079c6e7c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:05:34 +0000 Subject: [PATCH 4/5] docs: add database migration documentation to README Co-authored-by: 0ghost0-dev <70481054+0ghost0-dev@users.noreply.github.com> --- readme.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/readme.md b/readme.md index 0711829..8b8d973 100644 --- a/readme.md +++ b/readme.md @@ -38,3 +38,20 @@ - vite (빌드 도구) - Zod (데이터 검증) +--- +## Database Migrations / 데이터베이스 마이그레이션 + +Database migrations run automatically before the application starts when using Docker. + +For manual migration in development: +```bash +npm run drizzle:migrate +``` + +Docker 사용 시 데이터베이스 마이그레이션은 애플리케이션 시작 전에 자동으로 실행됩니다. + +개발 환경에서 수동으로 마이그레이션 실행: +```bash +npm run drizzle:migrate +``` + From abe860f41b8c61c896d52e9f3f25437cf5363b18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:08:49 +0000 Subject: [PATCH 5/5] fix: improve migration script with better error handling and race condition prevention Co-authored-by: 0ghost0-dev <70481054+0ghost0-dev@users.noreply.github.com> --- scripts/migrate.js | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/scripts/migrate.js b/scripts/migrate.js index 10ab49c..fa033bd 100755 --- a/scripts/migrate.js +++ b/scripts/migrate.js @@ -61,6 +61,17 @@ async function runMigrations() { ) `; + // Check if migration directory exists + try { + await readdir(migrationFolder); + } catch (error) { + if (error.code === 'ENOENT') { + logger.info(`Migration folder '${migrationFolder}' does not exist. No migrations to run.`); + return; + } + throw error; + } + // Read migration files const files = await readdir(migrationFolder); const sqlFiles = files @@ -78,16 +89,16 @@ async function runMigrations() { const filePath = join(migrationFolder, file); const sql = await readFile(filePath, 'utf-8'); - // Check if migration already applied - const [existing] = await postgresClient` - SELECT id FROM drizzle_migrations WHERE hash = ${file} - `; + // Execute in transaction with migration existence check inside for atomicity + await postgresClient.begin(async (tx) => { + // Check if migration already applied (inside transaction for atomicity) + const [existing] = await tx` + SELECT id FROM drizzle_migrations WHERE hash = ${file} + `; - if (!existing) { - logger.info(`Running migration: ${file}`); + if (!existing) { + logger.info(`Running migration: ${file}`); - // Execute in transaction - await postgresClient.begin(async (tx) => { // Execute SQL await tx.unsafe(sql); @@ -96,12 +107,12 @@ async function runMigrations() { 'INSERT INTO drizzle_migrations (hash, created_at) VALUES ($1, $2)', [file, Date.now()] ); - }); - logger.info(`Migration completed: ${file}`); - } else { - logger.debug(`Migration already applied: ${file}`); - } + logger.info(`Migration completed: ${file}`); + } else { + logger.debug(`Migration already applied: ${file}`); + } + }); } logger.info('All migrations completed successfully');