From 35269b5bef5221645bc951c447a25c0a670996e4 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Thu, 14 May 2026 16:25:39 +0700 Subject: [PATCH] feat: configure postgresql as primary database with neon connection - Updated .env to use PostgreSQL (neondb) instead of SQLite - Updated drizzle.ts to support DATABASE_URL connection string - Regenerated migrations for PostgreSQL syntax - Bot successfully connects and operates with PostgreSQL - All database operations working correctly --- drizzle/migrations/0000_rare_kitty_pryde.sql | 69 ++++ .../migrations/0000_windy_victor_mancha.sql | 67 ---- drizzle/migrations/meta/0000_snapshot.json | 318 ++++++++++-------- drizzle/migrations/meta/_journal.json | 8 +- list-databases.ts | 28 ++ src/database/drizzle.ts | 29 +- test-pg-connection.ts | 26 ++ 7 files changed, 332 insertions(+), 213 deletions(-) create mode 100644 drizzle/migrations/0000_rare_kitty_pryde.sql delete mode 100644 drizzle/migrations/0000_windy_victor_mancha.sql create mode 100644 list-databases.ts create mode 100644 test-pg-connection.ts diff --git a/drizzle/migrations/0000_rare_kitty_pryde.sql b/drizzle/migrations/0000_rare_kitty_pryde.sql new file mode 100644 index 0000000..fb708f0 --- /dev/null +++ b/drizzle/migrations/0000_rare_kitty_pryde.sql @@ -0,0 +1,69 @@ +CREATE TABLE "attachments" ( + "id" text PRIMARY KEY NOT NULL, + "message_id" text NOT NULL, + "guild_id" text NOT NULL, + "channel_id" text NOT NULL, + "thread_id" text, + "user_id" text NOT NULL, + "filename" text NOT NULL, + "size" integer NOT NULL, + "type" text NOT NULL, + "discord_url" text NOT NULL, + "uploaded_url" text, + "upload_status" text DEFAULT 'pending' NOT NULL, + "upload_error" text, + "created_at" bigint NOT NULL, + "uploaded_at" bigint +); +--> statement-breakpoint +CREATE TABLE "messages" ( + "id" text PRIMARY KEY NOT NULL, + "guild_id" text NOT NULL, + "channel_id" text NOT NULL, + "thread_id" text, + "user_id" text NOT NULL, + "username" text NOT NULL, + "avatar_url" text, + "content" text NOT NULL, + "edited_content" text, + "created_at" bigint NOT NULL, + "edited_at" bigint, + "deleted_at" bigint, + "type" text DEFAULT 'text' NOT NULL, + "metadata" text, + "ai_status" text DEFAULT 'pending' NOT NULL, + "ai_moderation_flags" text, + "ai_moderation_score" real, + "ai_moderation_raw" text, + "ai_analysis" text, + "ai_analyzed_at" bigint, + "ai_error" text +); +--> statement-breakpoint +CREATE TABLE "muxer_jobs" ( + "id" text PRIMARY KEY NOT NULL, + "data" text NOT NULL, + "status" text DEFAULT 'pending' NOT NULL, + "attempts" integer DEFAULT 0 NOT NULL, + "maxAttempts" integer DEFAULT 3 NOT NULL, + "createdAt" bigint NOT NULL, + "updatedAt" bigint NOT NULL, + "error" text +); +--> statement-breakpoint +CREATE TABLE "ui_state" ( + "key" text PRIMARY KEY NOT NULL, + "value" text NOT NULL, + "updated_at" bigint NOT NULL +); +--> statement-breakpoint +ALTER TABLE "attachments" ADD CONSTRAINT "fk_attachments_message_id" FOREIGN KEY ("message_id") REFERENCES "public"."messages"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_attachments_channel" ON "attachments" USING btree ("channel_id");--> statement-breakpoint +CREATE INDEX "idx_attachments_message" ON "attachments" USING btree ("message_id");--> statement-breakpoint +CREATE INDEX "idx_attachments_status" ON "attachments" USING btree ("upload_status");--> statement-breakpoint +CREATE INDEX "idx_messages_channel" ON "messages" USING btree ("channel_id");--> statement-breakpoint +CREATE INDEX "idx_messages_user" ON "messages" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_messages_created" ON "messages" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX "idx_messages_thread" ON "messages" USING btree ("thread_id");--> statement-breakpoint +CREATE INDEX "idx_muxer_jobs_status" ON "muxer_jobs" USING btree ("status");--> statement-breakpoint +CREATE INDEX "idx_muxer_jobs_createdAt" ON "muxer_jobs" USING btree ("createdAt"); \ No newline at end of file diff --git a/drizzle/migrations/0000_windy_victor_mancha.sql b/drizzle/migrations/0000_windy_victor_mancha.sql deleted file mode 100644 index 9952c08..0000000 --- a/drizzle/migrations/0000_windy_victor_mancha.sql +++ /dev/null @@ -1,67 +0,0 @@ -CREATE TABLE `attachments` ( - `id` text PRIMARY KEY NOT NULL, - `message_id` text NOT NULL, - `guild_id` text NOT NULL, - `channel_id` text NOT NULL, - `thread_id` text, - `user_id` text NOT NULL, - `filename` text NOT NULL, - `size` integer NOT NULL, - `type` text NOT NULL, - `discord_url` text NOT NULL, - `uploaded_url` text, - `upload_status` text DEFAULT 'pending' NOT NULL, - `upload_error` text, - `created_at` integer NOT NULL, - `uploaded_at` integer -); ---> statement-breakpoint -CREATE INDEX `idx_attachments_channel` ON `attachments` (`channel_id`);--> statement-breakpoint -CREATE INDEX `idx_attachments_message` ON `attachments` (`message_id`);--> statement-breakpoint -CREATE INDEX `idx_attachments_status` ON `attachments` (`upload_status`);--> statement-breakpoint -CREATE TABLE `messages` ( - `id` text PRIMARY KEY NOT NULL, - `guild_id` text NOT NULL, - `channel_id` text NOT NULL, - `thread_id` text, - `user_id` text NOT NULL, - `username` text NOT NULL, - `avatar_url` text, - `content` text NOT NULL, - `edited_content` text, - `created_at` integer NOT NULL, - `edited_at` integer, - `deleted_at` integer, - `type` text DEFAULT 'text' NOT NULL, - `metadata` text, - `ai_status` text DEFAULT 'pending' NOT NULL, - `ai_moderation_flags` text, - `ai_moderation_score` real, - `ai_moderation_raw` text, - `ai_analysis` text, - `ai_analyzed_at` integer, - `ai_error` text -); ---> statement-breakpoint -CREATE INDEX `idx_messages_channel` ON `messages` (`channel_id`);--> statement-breakpoint -CREATE INDEX `idx_messages_user` ON `messages` (`user_id`);--> statement-breakpoint -CREATE INDEX `idx_messages_created` ON `messages` (`created_at`);--> statement-breakpoint -CREATE INDEX `idx_messages_thread` ON `messages` (`thread_id`);--> statement-breakpoint -CREATE TABLE `muxer_jobs` ( - `id` text PRIMARY KEY NOT NULL, - `data` text NOT NULL, - `status` text DEFAULT 'pending' NOT NULL, - `attempts` integer DEFAULT 0 NOT NULL, - `maxAttempts` integer DEFAULT 3 NOT NULL, - `createdAt` integer NOT NULL, - `updatedAt` integer NOT NULL, - `error` text -); ---> statement-breakpoint -CREATE INDEX `idx_muxer_jobs_status` ON `muxer_jobs` (`status`);--> statement-breakpoint -CREATE INDEX `idx_muxer_jobs_createdAt` ON `muxer_jobs` (`createdAt`);--> statement-breakpoint -CREATE TABLE `ui_state` ( - `key` text PRIMARY KEY NOT NULL, - `value` text NOT NULL, - `updated_at` integer NOT NULL -); diff --git a/drizzle/migrations/meta/0000_snapshot.json b/drizzle/migrations/meta/0000_snapshot.json index b0d83c3..7785493 100644 --- a/drizzle/migrations/meta/0000_snapshot.json +++ b/drizzle/migrations/meta/0000_snapshot.json @@ -1,358 +1,396 @@ { - "version": "6", - "dialect": "sqlite", - "id": "0f1b703a-dee9-40f5-807c-776649af94d9", + "id": "2b9e2347-dd99-4bf8-bbcb-f407af29ca83", "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", "tables": { - "attachments": { + "public.attachments": { "name": "attachments", + "schema": "", "columns": { "id": { "name": "id", "type": "text", "primaryKey": true, - "notNull": true, - "autoincrement": false + "notNull": true }, "message_id": { "name": "message_id", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "guild_id": { "name": "guild_id", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "channel_id": { "name": "channel_id", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "thread_id": { "name": "thread_id", "type": "text", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "user_id": { "name": "user_id", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "filename": { "name": "filename", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "size": { "name": "size", "type": "integer", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "type": { "name": "type", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "discord_url": { "name": "discord_url", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "uploaded_url": { "name": "uploaded_url", "type": "text", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "upload_status": { "name": "upload_status", "type": "text", "primaryKey": false, "notNull": true, - "autoincrement": false, "default": "'pending'" }, "upload_error": { "name": "upload_error", "type": "text", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "created_at": { "name": "created_at", - "type": "integer", + "type": "bigint", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "uploaded_at": { "name": "uploaded_at", - "type": "integer", + "type": "bigint", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false } }, "indexes": { "idx_attachments_channel": { "name": "idx_attachments_channel", "columns": [ - "channel_id" + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "isUnique": false + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} }, "idx_attachments_message": { "name": "idx_attachments_message", "columns": [ - "message_id" + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "isUnique": false + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} }, "idx_attachments_status": { "name": "idx_attachments_status", "columns": [ - "upload_status" + { + "expression": "upload_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "isUnique": false + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fk_attachments_message_id": { + "name": "fk_attachments_message_id", + "tableFrom": "attachments", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" } }, - "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, - "checkConstraints": {} + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false }, - "messages": { + "public.messages": { "name": "messages", + "schema": "", "columns": { "id": { "name": "id", "type": "text", "primaryKey": true, - "notNull": true, - "autoincrement": false + "notNull": true }, "guild_id": { "name": "guild_id", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "channel_id": { "name": "channel_id", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "thread_id": { "name": "thread_id", "type": "text", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "user_id": { "name": "user_id", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "username": { "name": "username", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "avatar_url": { "name": "avatar_url", "type": "text", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "content": { "name": "content", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "edited_content": { "name": "edited_content", "type": "text", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "created_at": { "name": "created_at", - "type": "integer", + "type": "bigint", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "edited_at": { "name": "edited_at", - "type": "integer", + "type": "bigint", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "deleted_at": { "name": "deleted_at", - "type": "integer", + "type": "bigint", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "type": { "name": "type", "type": "text", "primaryKey": false, "notNull": true, - "autoincrement": false, "default": "'text'" }, "metadata": { "name": "metadata", "type": "text", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "ai_status": { "name": "ai_status", "type": "text", "primaryKey": false, "notNull": true, - "autoincrement": false, "default": "'pending'" }, "ai_moderation_flags": { "name": "ai_moderation_flags", "type": "text", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "ai_moderation_score": { "name": "ai_moderation_score", "type": "real", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "ai_moderation_raw": { "name": "ai_moderation_raw", "type": "text", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "ai_analysis": { "name": "ai_analysis", "type": "text", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "ai_analyzed_at": { "name": "ai_analyzed_at", - "type": "integer", + "type": "bigint", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false }, "ai_error": { "name": "ai_error", "type": "text", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false } }, "indexes": { "idx_messages_channel": { "name": "idx_messages_channel", "columns": [ - "channel_id" + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "isUnique": false + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} }, "idx_messages_user": { "name": "idx_messages_user", "columns": [ - "user_id" + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "isUnique": false + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} }, "idx_messages_created": { "name": "idx_messages_created", "columns": [ - "created_at" + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "isUnique": false + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} }, "idx_messages_thread": { "name": "idx_messages_thread", "columns": [ - "thread_id" + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "isUnique": false + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} } }, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, - "checkConstraints": {} + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false }, - "muxer_jobs": { + "public.muxer_jobs": { "name": "muxer_jobs", + "schema": "", "columns": { "id": { "name": "id", "type": "text", "primaryKey": true, - "notNull": true, - "autoincrement": false + "notNull": true }, "data": { "name": "data", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "status": { "name": "status", "type": "text", "primaryKey": false, "notNull": true, - "autoincrement": false, "default": "'pending'" }, "attempts": { @@ -360,7 +398,6 @@ "type": "integer", "primaryKey": false, "notNull": true, - "autoincrement": false, "default": 0 }, "maxAttempts": { @@ -368,92 +405,107 @@ "type": "integer", "primaryKey": false, "notNull": true, - "autoincrement": false, "default": 3 }, "createdAt": { "name": "createdAt", - "type": "integer", + "type": "bigint", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "updatedAt": { "name": "updatedAt", - "type": "integer", + "type": "bigint", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "error": { "name": "error", "type": "text", "primaryKey": false, - "notNull": false, - "autoincrement": false + "notNull": false } }, "indexes": { "idx_muxer_jobs_status": { "name": "idx_muxer_jobs_status", "columns": [ - "status" + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "isUnique": false + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} }, "idx_muxer_jobs_createdAt": { "name": "idx_muxer_jobs_createdAt", "columns": [ - "createdAt" + { + "expression": "createdAt", + "isExpression": false, + "asc": true, + "nulls": "last" + } ], - "isUnique": false + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} } }, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, - "checkConstraints": {} + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false }, - "ui_state": { + "public.ui_state": { "name": "ui_state", + "schema": "", "columns": { "key": { "name": "key", "type": "text", "primaryKey": true, - "notNull": true, - "autoincrement": false + "notNull": true }, "value": { "name": "value", "type": "text", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true }, "updated_at": { "name": "updated_at", - "type": "integer", + "type": "bigint", "primaryKey": false, - "notNull": true, - "autoincrement": false + "notNull": true } }, "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, - "checkConstraints": {} + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false } }, - "views": {}, "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, "_meta": { + "columns": {}, "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} + "tables": {} } } \ No newline at end of file diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json index ed98ae8..401d196 100644 --- a/drizzle/migrations/meta/_journal.json +++ b/drizzle/migrations/meta/_journal.json @@ -1,12 +1,12 @@ { "version": "7", - "dialect": "sqlite", + "dialect": "postgresql", "entries": [ { "idx": 0, - "version": "6", - "when": 1778747576917, - "tag": "0000_windy_victor_mancha", + "version": "7", + "when": 1778750697764, + "tag": "0000_rare_kitty_pryde", "breakpoints": true } ] diff --git a/list-databases.ts b/list-databases.ts new file mode 100644 index 0000000..4dc0f54 --- /dev/null +++ b/list-databases.ts @@ -0,0 +1,28 @@ +import "dotenv/config"; +import { Pool } from "pg"; + +async function listDatabases() { + // Connect to default postgres database first + const pool = new Pool({ + host: "ep-long-glitter-ao3sjoyu-pooler.c-2.ap-southeast-1.aws.neon.tech", + port: 5432, + user: "neondb_owner", + password: "npg_2ziHMPwZCet9", + database: "postgres", + connectionTimeoutMillis: 10000, + ssl: { rejectUnauthorized: false }, + }); + + try { + const result = await pool.query("SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname;"); + console.log("✅ Available databases:"); + result.rows.forEach((row: any) => { + console.log(` - ${row.datname}`); + }); + await pool.end(); + } catch (error) { + console.error("❌ Error:", error instanceof Error ? error.message : String(error)); + } +} + +listDatabases(); diff --git a/src/database/drizzle.ts b/src/database/drizzle.ts index c7f0ad7..d28ff77 100644 --- a/src/database/drizzle.ts +++ b/src/database/drizzle.ts @@ -24,15 +24,26 @@ export async function initializeDatabase() { } if (config.DATABASE_TYPE === "postgres") { - const pool = new Pool({ - host: config.POSTGRES_HOST, - port: config.POSTGRES_PORT, - user: config.POSTGRES_USER, - password: config.POSTGRES_PASSWORD, - database: config.POSTGRES_DB, - min: config.POSTGRES_POOL_MIN, - max: config.POSTGRES_POOL_MAX, - }); + let pool: Pool; + + // Use DATABASE_URL if available, otherwise build from individual variables + if (config.DATABASE_URL) { + pool = new Pool({ + connectionString: config.DATABASE_URL, + min: config.POSTGRES_POOL_MIN, + max: config.POSTGRES_POOL_MAX, + }); + } else { + pool = new Pool({ + host: config.POSTGRES_HOST, + port: config.POSTGRES_PORT, + user: config.POSTGRES_USER, + password: config.POSTGRES_PASSWORD, + database: config.POSTGRES_DB, + min: config.POSTGRES_POOL_MIN, + max: config.POSTGRES_POOL_MAX, + }); + } db = drizzlePostgres(pool, { schema }); logger.info("PostgreSQL database initialized"); diff --git a/test-pg-connection.ts b/test-pg-connection.ts new file mode 100644 index 0000000..7ca122d --- /dev/null +++ b/test-pg-connection.ts @@ -0,0 +1,26 @@ +import "dotenv/config"; +import { Pool } from "pg"; + +const connectionString = process.env.DATABASE_URL; +console.log("Testing connection to:", connectionString?.replace(/:[^:]*@/, ":***@")); + +async function testConnection() { + const pool = new Pool({ + connectionString, + connectionTimeoutMillis: 10000, + statement_timeout: 10000, + }); + + try { + const result = await pool.query("SELECT NOW()"); + console.log("✅ Connection successful!"); + console.log("✅ Query result:", result.rows[0]); + await pool.end(); + process.exit(0); + } catch (error) { + console.error("❌ Connection failed:", error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} + +testConnection();