Skip to content

Conversation

@WxTaco
Copy link
Collaborator

@WxTaco WxTaco commented Oct 29, 2025

Drizzle to Prisma Database Migration

This PR completes a comprehensive migration of the database infrastructure from Drizzle ORM to Prisma, implementing a multi-file schema structure and updating all database interactions across the codebase. The migration includes 55 commits addressing schema consolidation, query layer updates, type safety improvements, and critical bug fixes.

Overview

The migration replaces all Drizzle ORM usage with Prisma Client, providing better type safety, improved query performance, and a more maintainable schema structure. All database operations have been rewritten to use Prisma's API, including queries, mutations, and raw SQL where necessary.

Schema Structure

Implemented Prisma schema:

  • Base configuration in prisma/schema.prisma
  • Consolidated all model definitions (Guild, User, Channel, Member, and event types)
  • Prisma Client generation configured for automatic type generation
  • Support for SQLite, PostgreSQL, and MySQL through datasource configuration

Database Layer Changes

  • Updated database factory (src/db/factory.ts) to create PrismaClient connections
  • Migrated connection management to use Prisma with support for multiple database backends
  • Added URL encoding for database credentials to handle special characters safely
  • Removed all Drizzle-specific connection implementations

Query Layer Migration

All query classes migrated to Prisma Client with database portability improvements:

  • MessageQueries: Using Prisma aggregate and parameterized $queryRaw
  • UserQueries: Using $queryRaw with INNER JOINs for complex queries
  • VoiceQueries: Using Prisma aggregate functions
  • ChannelQueries: Using $queryRaw for statistics with proper quoting
  • MemberQueries: Using $queryRaw with ORDER BY for deterministic results
  • ReactionQueries: Using $queryRaw with Prisma.sql fragments for safe parameterization

Query Improvements

  • Replaced SQLite-specific date() and strftime() functions with JavaScript aggregation for database portability
  • Implemented Prisma.sql fragments for safe parameterized optional query conditions
  • Added proper identifier quoting (double quotes) for all table and column names
  • Fixed SQL GROUP BY clauses to include all non-aggregated columns
  • Added ORDER BY clauses for deterministic result ordering
  • Moved date/hour aggregation from SQL to JavaScript for cross-database compatibility

Processor Updates

All event processors migrated to Prisma operations:

  • BaseProcessor: Upsert methods for users, guilds, and channels with automatic @updatedAt management
  • MemberProcessor: Join/leave event handling with composite ID validation
  • MessageProcessor: Message event creation
  • VoiceProcessor: Voice state tracking
  • PresenceProcessor: Presence update handling with userId validation
  • ReactionProcessor: Reaction event tracking
  • GuildProcessor: Guild and member management with composite ID support

Processor Improvements

  • Removed manual updatedAt assignments to let Prisma manage timestamps automatically
  • Added composite ID fields to upsert create blocks to prevent duplicate records
  • Implemented strict user validation to prevent undefined runtime errors
  • Added userId validation to prevent empty strings from being stored

Handler Updates

  • GuildHandler: Updated to use Prisma upsert and update operations

Analytics and Export

  • InsightsEngine: Migrated to Prisma queries with createdAt column consistency
  • DataExporter: Rewritten using Prisma findMany and $queryRaw with guild-scoped filtering
  • User Export: Added WHERE clause to restrict exports to users with activity in the target guild, preventing data leaks in multi-guild deployments

Type Safety Improvements

  • Fixed all TypeScript compilation errors (TS7006, TS2345)
  • Added explicit return types to all query methods
  • Typed all raw SQL query results with proper bigint/number conversions
  • Added explicit type annotations to map callback parameters
  • Implemented proper type guards for user validation
  • Fixed type assertions in aggregator functions

Security Improvements

  • Replaced unsafe string interpolation with Prisma.sql parameterized queries
  • Added URL encoding for database credentials to prevent DSN parsing errors
  • Implemented guild-scoped filtering in user exports to prevent data leaks
  • Added validation for user IDs and presence data to prevent injection attacks

Cleanup

  • Removed drizzle-orm and drizzle-kit dependencies
  • Removed old schema files and generators
  • Removed migration directories
  • Removed unused utility files
  • Fixed code complexity issues in factory.ts

CI/CD Updates

  • Added prisma generate step to TypeScript check workflow
  • Updated workflows to use npm instead of Bun for consistency
  • Removed Biome binary rebuild steps (no longer needed)
  • Simplified workflow configuration

Commits Summary

Recent Fixes (Latest 13 commits)

  • 8b43a07: fix: restrict user export to requested guild only
  • 95233e5: fix: move date/hour aggregation from SQL to JavaScript for DB portability
  • ca53566: fix: add username to GROUP BY clause in getTopReactors query
  • 03e0682: fix: use Prisma.sql fragments and quoted identifiers in reaction queries
  • a7ff1f2: fix: use Prisma.sql for parameterized optional query conditions
  • 09c64ae: fix: add ORDER BY to member growth queries and type map callbacks
  • 220c4d6: fix: validate userId in presence processor and prevent empty values
  • 9de0862: fix: add composite id to member upsert and tighten user validation
  • 0bd0f7b: fix: provide composite id in member upsert create block
  • 5a28676: fix: remove manual updatedAt assignment from upsert operations
  • d58b650: fix: URL-encode database credentials in connection strings
  • afeafac: fix: use createdAt instead of timestamp in analytics queries

Schema & Build (Commits 14-38)

  • ebda430: fix: consolidate Prisma schema files and fix TypeScript compilation errors
  • 5633098: ci: add prisma generate step to typescript-check workflow
  • 967e479: build: upgrade Prisma to 6.18.0
  • 5e76aca: build: add prisma as dev dependency

Type Safety (Commits 39-42)

  • eb99aa1: fix: add type annotations for Prisma raw SQL query results
  • be0a650: fix: add explicit return type to getHourlyActivity method
  • ba7c840: fix: add explicit type to activityMap and use nullish coalescing

Core Migration (Commits 43-55)

  • 019f6ff: feat: migrate remaining processors and handlers to Prisma
  • 69860c5: refactor: reduce complexity in factory.ts
  • 0aafaf7: chore: remove Drizzle dependencies and unused files
  • 3cf058f: feat: migrate DataExporter to Prisma

Breaking Changes

None - This is an internal database layer migration. All public APIs remain unchanged.

Migration Notes

  • Database credentials with special characters (e.g., @, :, %, &) are now properly handled
  • Multi-guild deployments now have proper data isolation in exports
  • Queries are now database-agnostic and work with SQLite, PostgreSQL, and MySQL
  • All timestamp handling uses UTC consistently

taco added 12 commits October 29, 2025 17:25
- Create main schema.prisma with all models
- Add individual schema files in organized folders (guild/, events/)
- Configure Prisma in package.json with new scripts
- Add prisma.config.ts for multi-database configuration
- Move schema files to prisma/schema/ directory
- Create separate files for each model (guild, user, channel, events)
- Add prisma.config.ts with proper configuration
- Update package.json to remove deprecated prisma config
- Successfully generate Prisma Client with new structure
- Update types.ts to use PrismaClient instead of Drizzle types
- Rewrite factory.ts to create Prisma connections
- Update connection.ts to remove schema imports
- Remove old Drizzle connection files (sqlite, postgres)
- Remove old schema, generators, migrations directories
- Remove Drizzle config files
- Remove generate-schemas script
- Update all query classes to use Prisma Client
- Replace Drizzle ORM queries with Prisma queries and raw SQL
- Update MessageQueries, UserQueries, VoiceQueries
- Update ChannelQueries, MemberQueries, ReactionQueries
- Update db/index.ts to remove schema exports
- Update db/interface.ts to use PrismaClient type
- Update MemberProcessor to use Prisma Client
- Update ReactionProcessor to use Prisma Client
- Update InsightsEngine to use Prisma queries
- Remove Drizzle imports from helpers.ts
- Update all processor methods to use Prisma syntax
- Update upsertUser to use Prisma upsert
- Update upsertGuild to use Prisma upsert
- Update upsertChannel to use Prisma upsert
- Remove schema imports from base processor
- Update gatherUserData to use Prisma raw SQL
- Update gatherChannelData to use Prisma raw SQL
- Update gatherMessageData to use Prisma findMany
- Update gatherVoiceData to use Prisma findMany
- Update gatherMemberData to use Prisma findMany
- Remove all Drizzle imports and schema references
- Remove drizzle-orm and drizzle-kit from package.json
- Remove unused src/db/utils.ts file
- Complete migration to Prisma
- Extract buildPostgresUrl helper function
- Extract buildMysqlUrl helper function
- Extract buildDatasourceUrl helper function
- Fix all formatting issues
- Pass all linting checks
- Update MessageProcessor to use Prisma create
- Update PresenceProcessor to use Prisma create
- Update VoiceProcessor to use Prisma create
- Update GuildProcessor to use Prisma upsert
- Update GuildHandler to use Prisma upsert/update
- Fix mention.ts import to use GuildMember type
- Fix aggregator.ts type assertion
- All TypeScript compilation errors resolved
- Remove drizzle/ directory with old migrations
- Complete cleanup of Drizzle-related files
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 29, 2025

Walkthrough

Complete migration from Drizzle ORM to Prisma as the database layer. Removed all Drizzle configuration, migrations, and schema generation infrastructure. Updated database handlers, processors, analytics, and type system to use Prisma client and raw SQL queries where appropriate. Added new Prisma schema file.

Changes

Cohort / File(s) Summary
Drizzle Configuration Removal
drizzle.mysql.config.ts, drizzle.postgres.config.ts, drizzle.sqlite.config.ts
Removed all Drizzle ORM configuration files for MySQL, PostgreSQL, and SQLite databases.
Drizzle Migration Files (MySQL)
drizzle/mysql/0000_short_mercury.sql, drizzle/mysql/0001_giant_dreadnoughts.sql, drizzle/mysql/0002_square_rhino.sql
Deleted all MySQL migration SQL files including table creation and schema modification statements.
Drizzle Migration Metadata (MySQL)
drizzle/mysql/meta/0000_snapshot.json, drizzle/mysql/meta/0001_snapshot.json, drizzle/mysql/meta/0002_snapshot.json, drizzle/mysql/meta/_journal.json
Removed all MySQL schema snapshots and migration journal metadata.
Drizzle Migration Files (PostgreSQL)
drizzle/postgres/0000_fast_peter_parker.sql, drizzle/postgres/0001_burly_molly_hayes.sql, drizzle/postgres/0002_oval_pyro.sql, drizzle/postgres/0003_cheerful_blacklash.sql
Deleted all PostgreSQL migration SQL files with DDL and constraint modifications.
Drizzle Migration Metadata (PostgreSQL)
drizzle/postgres/meta/0000_snapshot.json, drizzle/postgres/meta/0001_snapshot.json, drizzle/postgres/meta/0002_snapshot.json, drizzle/postgres/meta/0003_snapshot.json, drizzle/postgres/meta/_journal.json
Removed all PostgreSQL schema snapshots and migration journal metadata.
Drizzle Migration Files (SQLite)
drizzle/sqlite/0000_good_purple_man.sql, drizzle/sqlite/0001_tough_tag.sql, drizzle/sqlite/0002_salty_layla_miller.sql, drizzle/sqlite/0003_spooky_roughhouse.sql, drizzle/sqlite/0004_melted_darwin.sql
Deleted all SQLite migration SQL files including table creation and schema rewrites.
Drizzle Migration Metadata (SQLite)
drizzle/sqlite/meta/0000_snapshot.json, drizzle/sqlite/meta/0001_snapshot.json, drizzle/sqlite/meta/0002_snapshot.json, drizzle/sqlite/meta/0003_snapshot.json, drizzle/sqlite/meta/0004_snapshot.json, drizzle/sqlite/meta/_journal.json
Removed all SQLite schema snapshots and migration journal metadata.
Schema Generation System
src/generators/base.ts, src/generators/generator.ts, src/generators/mysql.ts, src/generators/postgres.ts, src/generators/sqlite.ts, src/generators/index.ts
Removed entire Zod-based schema generation infrastructure and all database-specific generator implementations.
Schema Definitions
src/schemas/base.ts, src/schemas/events.ts, src/schemas/guild.ts, src/schemas/index.ts
Removed all Zod schema definitions and type exports previously used for ORM generation.
Migration Manager
src/migrations/index.ts, src/migrations/manager.ts, scripts/generate-schemas.ts
Removed migration management system and schema generation script.
Database Connection Layer
src/db/connection.ts, src/db/factory.ts, src/db/index.ts, src/db/interface.ts, src/db/types.ts, src/db/utils.ts, src/db/schema/index.ts
Refactored to use Prisma client; replaced ORM abstraction with PrismaClient type alias; added database URL builders; removed schema-switching logic and ORM utility functions.
Database Adapter Removal
src/db/postgres/connection.ts, src/db/sqlite/connection.ts
Removed database-specific connection factories; consolidated into unified Prisma-based factory.
Package Configuration
package.json
Version bumped to 2.3.0; replaced Drizzle scripts with Prisma equivalents; added @prisma/client and prisma; removed drizzle-orm and drizzle-kit dependencies.
Database Handlers & Processors
src/handlers/guildHandler.ts, src/processors/base.ts, src/processors/guild.ts, src/processors/member.ts, src/processors/message.ts, src/processors/presence.ts, src/processors/reaction.ts, src/processors/voice.ts
Migrated from ORM insert/update patterns to Prisma create/upsert/update methods; updated data access layer.
Analytics & Insights
src/analytics/insights.ts, src/export/exporter.ts
Replaced ORM queries with raw SQL via $queryRaw and Prisma aggregation; converted bigint results to numbers.
Stats Query Layer
src/stats/helpers.ts, src/stats/queries/channel.ts, src/stats/queries/member.ts, src/stats/queries/message.ts, src/stats/queries/reaction.ts, src/stats/queries/user.ts, src/stats/queries/voice.ts, src/stats/aggregator.ts
Migrated from ORM query builder to raw SQL queries and Prisma aggregation; removed helper functions; added numeric type coercions.
Type System Updates
src/types/api/message/mention.ts
Updated imports to reference GuildMember type instead of Member.
New Prisma Schema
prisma/schema.prisma
Added complete Prisma schema defining User, Guild, Channel, Member, and event models (MessageEvent, VoiceEvent, MemberEvent, PresenceEvent, ReactionEvent) with relations and table mappings.
Miscellaneous
SECURITY.md, LICENSE, dummy.md
Updated security contact email; changed LICENSE header format; added placeholder file.

Sequence Diagram(s)

sequenceDiagram
    participant Old as Old (Drizzle ORM)
    participant New as New (Prisma)
    
    rect rgb(200, 220, 255)
        Note over Old,New: Database Connection Flow
        
        Old->>Old: Load Drizzle config<br/>(drizzle.*.config.ts)
        Old->>Old: Connect via drizzle-orm<br/>getDb(), getSqliteDb(), etc.
        Old->>Old: Build query with ORM<br/>db.select().from(schema).where()
        Old->>Old: Execute & map results
        
        New->>New: Load Prisma schema<br/>(prisma/schema.prisma)
        New->>New: Create PrismaClient<br/>createDatabaseConnection()
        New->>New: Use client methods<br/>db.user.findMany()
        New->>New: Execute & return typed data
    end
    
    rect rgb(200, 255, 220)
        Note over Old,New: Schema Generation (Removed)
        
        Old->>Old: Read Zod schemas
        Old->>Old: Generate SQL via<br/>SchemaGenerator
        Old->>Old: Write migrations
        
        New->>New: Schema defined directly<br/>in prisma/schema.prisma
        New->>New: No code generation needed
    end
    
    rect rgb(255, 220, 200)
        Note over Old,New: Query Execution Changes
        
        Old->>Old: ORM: db.select().from().where()
        Old->>Old: Returns typed ORM objects
        
        New->>New: Prisma: db.model.findMany({where})
        New->>New: Raw SQL: db.$queryRaw\`SELECT...\`
        New->>New: Returns plain objects/bigint
        New->>New: Convert bigint to number
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Areas requiring extra attention:

  • Complete ORM replacement: Every database interaction has been rewritten. Verify that Prisma queries produce equivalent results to previous Drizzle queries, especially in:

    • src/stats/queries/* (aggregations, grouping, joins)
    • src/analytics/insights.ts (hourly activity, engagement metrics)
    • src/export/exporter.ts (data gathering for exports)
  • Raw SQL queries: New $queryRaw calls across stats and analytics modules need careful validation:

    • Ensure SQL syntax is correct for all supported databases (currently migrating schema)
    • Verify bigint-to-number conversions don't lose precision
    • Check GROUP BY, JOIN, and WHERE clauses match original intent
  • Type safety changes: Migration from typed ORM results to plain objects/raw query results:

    • src/db/interface.ts now uses PrismaClient directly
    • Check all downstream code properly handles new typing
  • Upsert logic in processors: Migration from insert().onConflictDoUpdate() to upsert() patterns:

    • src/processors/* - verify conflict resolution logic is equivalent
    • Check composite key handling (e.g., member id as ${guildId}-${userId})
  • New Prisma schema: prisma/schema.prisma is the new source of truth:

    • Validate all model definitions, relations, and cascading deletes
    • Confirm table name mappings via @@map match database expectations
  • Migration path: Verify the transition from Drizzle migrations to Prisma can be executed safely (consider existing production data)

Poem

🐰 From Drizzle's queries to Prisma's embrace,
A new schema takes its rightful place!
Raw SQL flows where ORM once stood,
Type safety blooms—this refactor's good!
The future calls with async/await delight,
Hop forward, brave code—we're getting it right! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description Check ❓ Inconclusive The pull request description is comprehensive and well-organized with clear sections covering Overview, Schema Structure, Database Layer Changes, Query Layer Migration, Processor Updates, Handler Updates, Analytics and Export, Type Safety Improvements, Security Improvements, Cleanup, CI/CD Updates, detailed Commits Summary, Breaking Changes, and Migration Notes. However, the provided description_template only shows a selection prompt for choosing between "New Feature" or "Bug Fix" templates without providing the actual detailed template structure required for validation. Without access to the specific template content and required sections, it cannot be conclusively determined whether the PR description meets all mandatory requirements or if optional sections are appropriately handled. To properly validate this PR description against the template requirements, the actual detailed content of both the "New Feature" and "Bug Fix" templates should be provided. The PR description itself appears substantial and professional; however, formal validation against the repository's specific template guidelines cannot be completed without access to the required template structure. Recommend clarifying which template applies to this migration work and confirming all required sections align with that template's specifications.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "feat: Migrate database infrastructure from Drizzle ORM to Prisma" is directly related to the primary change in the changeset and clearly summarizes the main work being performed. The title is concise, uses clear language without unnecessary noise or vague terms, and a teammate reviewing the repository history would immediately understand that this PR involves migrating the database system from Drizzle ORM to Prisma. The title effectively captures the scope and intent of this substantial infrastructure change.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch database-to-prisma

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@WxTaco WxTaco requested a review from a team October 29, 2025 16:55
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/processors/base.ts (1)

55-73: Require owner_id for guild upsert; avoid empty-string FKs.

Empty ownerId can violate FK constraints and create invalid data. Validate presence and fail fast.

-  private validateGuildData(guild: unknown): boolean {
+  private validateGuildData(guild: unknown): boolean {
@@
-    return !!(g.id && typeof g.id === 'string');
+    return !!(
+      g.id && typeof g.id === 'string' &&
+      g.owner_id && typeof g.owner_id === 'string'
+    );
   }
-          ownerId: (guildData.owner_id as string) || '',
+          ownerId: guildData.owner_id as string,

Also applies to: 87-95

package.json (1)

7-18: Ensure Prisma Client is installed and generated during install/CI.

Add @prisma/client and prisma, and run prisma generate automatically to fix TS2307 and keep client in sync.

   "scripts": {
+    "postinstall": "prisma generate",
@@
-    "db:generate": "prisma generate",
+    "db:generate": "prisma generate",
@@
-    "db:studio": "prisma studio",
+    "db:studio": "prisma studio",
   },
@@
   "dependencies": {
+    "@prisma/client": "^5.19.0",
@@
   "devDependencies": {
+    "prisma": "^5.19.0",

Optional: if Drizzle-era drivers are no longer used directly, consider removing "better-sqlite3" and "postgres" to slim the install.

src/export/exporter.ts (1)

5-10: Implement XLSX formatter or remove xlsx export option.

The xlsx formatter is mapped to JSONFormatter, which returns JSON data with .json file extension and application/json content-type. Since 'xlsx' is listed as a valid format option in ExportOptions, users can request this format but will receive incorrect output. Either implement a real XLSXFormatter class or remove 'xlsx' from the format options.

🧹 Nitpick comments (28)
src/handlers/guildHandler.ts (2)

42-61: Minor: consider relying on Prisma's @updatedat instead of manual timestamp.

If your Guild.updatedAt is annotated with @updatedAt, Prisma sets it automatically; you can drop updatedAt: new Date() from the update payload. If not, keep as-is.

Please confirm the schema for Guild.updatedAt.


65-75: Make update resilient to missed create events (use upsert or handle P2025).

If the bot restarts and misses a create, update() throws "Record not found." Prefer upsert here or catch P2025 and fallback to create.

-  await this.database.guild.update({
-    where: { id: data.id },
-    data: {
-      name: data.name,
-      icon: data.icon,
-      ownerId: data.ownerId,
-      updatedAt: new Date(),
-    },
-  });
+  await this.database.guild.upsert({
+    where: { id: data.id },
+    create: {
+      id: data.id,
+      name: data.name,
+      icon: data.icon,
+      ownerId: data.ownerId,
+      memberCount: (data as any).memberCount ?? 0,
+    },
+    update: {
+      name: data.name,
+      icon: data.icon,
+      ownerId: data.ownerId,
+      // If using @updatedAt, you can omit this:
+      updatedAt: new Date(),
+    },
+  });

If you intentionally want an exception on missing guilds, keep update() but add contextual logging.

src/processors/message.ts (1)

21-32: Use upsert for idempotency and parallelize pre‑work.

Create will fail on duplicates (retries, replays). Upsert avoids P2002 and makes the processor safe to retry. Also run user/channel upserts in parallel.

-      await this.upsertUser(message.author);
-      await this.upsertChannel(message);
+      await Promise.all([this.upsertUser(message.author), this.upsertChannel(message)]);
-
-      await this.db.messageEvent.create({
-        data: {
+      await this.db.messageEvent.upsert({
+        where: { id: message.id },
+        create: {
           id: message.id,
           guildId: message.guild_id,
           channelId: message.channel_id,
           userId: message.author.id,
-          content: (message.content || '').slice(0, 2000),
+          content: (message.content ?? '').slice(0, 2000),
           attachmentCount: message.attachments?.length || 0,
           embedCount: message.embeds?.length || 0,
           timestamp: new Date(message.timestamp),
-        },
+        },
+        update: {
+          // Typically immutable; keep empty for true idempotency:
+        },
       });

If MessageEvent.id is not the Discord message ID, adjust the where field to your unique constraint.

prisma/schema/reaction-event.prisma (2)

5-16: Constrain action and add query indexes.

Use an enum for action and add indexes you frequently filter by (guildId/timestamp, messageId, userId). This improves correctness and analytics performance.

+enum ReactionAction {
+  add
+  remove
+}
+
 model ReactionEvent {
   id            String   @id @default(uuid())
   timestamp     DateTime
   createdAt     DateTime @default(now())
   guildId       String
   channelId     String
   messageId     String
   userId        String
   emojiId       String?
   emojiName     String
   emojiAnimated Boolean  @default(false)
-  action        String
+  action        ReactionAction
@@
-  @@map("reactionevent")
+  @@map("reactionevent")
+  @@index([guildId, timestamp])
+  @@index([messageId])
+  @@index([guildId, userId, timestamp])
 }

If you already have equivalent indexes at the DB level, align them here so Prisma can generate/validate them.


17-21: Optional: add relations to Channel/Message models if available.

If Channel and MessageEvent exist, relating channelId/messageId improves referential integrity and enables cascading deletes.

Example:

channel   Channel     @relation(fields: [channelId], references: [id], onDelete: Cascade)
message   MessageEvent @relation(fields: [messageId], references: [id], onDelete: Cascade)

Only add if you have corresponding models and constraints.

prisma.config.ts (1)

4-10: Config LGTM; consider resolving schema to an absolute path.

Minor robustness tweak for alternate CWDs.

-import path from 'node:path';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
 
 export default defineConfig({
-  schema: path.join('prisma', 'schema'),
+  schema: path.resolve(__dirname, 'prisma', 'schema'),
   engine: 'classic',
   datasource: {
     url: env('DATABASE_URL'),
   },
 });

Confirm your Prisma version supports prisma/config and directory schemas (generation already passing suggests yes).

src/db/connection.ts (1)

9-16: Harden singleton against concurrent initialization.

Two concurrent getDb() calls can race and create multiple clients. Guard with a shared promise.

-let db: DatabaseInstance;
+let db: DatabaseInstance | undefined;
+let dbInitPromise: Promise<DatabaseInstance> | null = null;
@@
 export const getDb = async (
   config: DatabaseConfig = { type: 'sqlite', path: './data/stats.db' }
 ): Promise<DatabaseInstance> => {
-  if (!db) {
-    db = await createDatabaseConnection(config);
-  }
-  return db;
+  if (db) return db;
+  if (!dbInitPromise) {
+    dbInitPromise = createDatabaseConnection(config).then((instance) => {
+      db = instance;
+      return instance;
+    });
+  }
+  return dbInitPromise;
 };

Optional: expose export const closeDb = async () => db?.$disconnect(); for graceful shutdowns.

src/stats/aggregator.ts (1)

76-77: Prefer typed query rows over per-field casts.

Define an interface for getChannelStats rows and type the query so you can drop these as casts.

Example:

type ChannelStatsRow = { channelId: string; channelName?: string; messageCount: number; uniqueUsers: number };
// In StatsQueries.getChannelStats(): return Promise<ChannelStatsRow[]>

Then here:

topChannels: channelStats.map((ch) => ({
  channelId: ch.channelId,
  name: ch.channelName ?? undefined,
  messageCount: Number(ch.messageCount),
  uniqueUsers: Number(ch.uniqueUsers),
}))
```<!-- review_comment_end -->

</blockquote></details>
<details>
<summary>src/stats/queries/member.ts (1)</summary><blockquote>

`13-22`: **Optional: compute joins and leaves in one pass.**

A single query with conditional aggregation reduces I/O:


```ts
type GrowthRow = { date: string; joins: bigint; leaves: bigint };
const rows = await this.db.$queryRaw<GrowthRow[]>`
  SELECT
    date(timestamp) as date,
    SUM(CASE WHEN action = 'join'  THEN 1 ELSE 0 END) as joins,
    SUM(CASE WHEN action = 'leave' THEN 1 ELSE 0 END) as leaves
  FROM memberevent
  WHERE guildId = ${guildId}
    AND timestamp >= ${since}
  GROUP BY date(timestamp)
  ORDER BY date(timestamp)
`;

return {
  joins: rows.map(r => ({ date: r.date, joins: Number(r.joins) })),
  leaves: rows.map(r => ({ date: r.date, leaves: Number(r.leaves) })),
};

This keeps the same shape with fewer round trips.

Also applies to: 24-33

src/processors/reaction.ts (2)

104-106: Replace find+create with upsert to avoid races.

Concurrent events can double-create and fail. Use upsert or reuse BaseProcessor.upsertUser.

Apply:

-    const existing = await this.db.user.findUnique({
-      where: { id: userId },
-    });
-
-    if (!existing) {
-      await this.db.user.create({
-        data: {
-          id: userId,
-          username: 'Unknown',
-          discriminator: '0000',
-        },
-      });
-    }
+    await this.db.user.upsert({
+      where: { id: userId },
+      update: {},
+      create: { id: userId, username: 'Unknown', discriminator: '0000' },
+    });

Alternatively, normalize on the shared helper:

-  private async ensureUserExists(userId: string): Promise<void> {
-    const existing = await this.db.user.findUnique({ where: { id: userId } });
-    if (!existing) { await this.db.user.create({ data: { id: userId, username: 'Unknown', discriminator: '0000' } }); }
-  }
+  private async ensureUserExists(userId: string): Promise<void> {
+    await this.upsertUser({ id: userId });
+  }

Also applies to: 108-115


120-122: Same race on channel creation — use upsert or shared helper.

Apply:

-    const existing = await this.db.channel.findUnique({
-      where: { id: channelId },
-    });
-
-    if (!existing) {
-      await this.db.channel.create({
-        data: {
-          id: channelId,
-          guildId,
-          name: 'Unknown',
-          type: 0,
-        },
-      });
-    }
+    await this.db.channel.upsert({
+      where: { id: channelId },
+      update: {},
+      create: { id: channelId, guildId, name: 'Unknown', type: 0 },
+    });

Optionally reuse BaseProcessor.upsertChannel for consistency.

Also applies to: 124-131

prisma/schema/channel.prisma (1)

1-16: Add updatedAt and helpful indexes.

Common queries filter by guildId/parentId; updatedAt aids auditing/sync.

Apply:

 model Channel {
   id        String   @id
   guildId   String
   name      String?
   type      Int
   parentId  String?
-  createdAt DateTime @default(now())
+  createdAt DateTime @default(now())
+  updatedAt DateTime @updatedAt
@@
   guild Guild @relation(fields: [guildId], references: [id], onDelete: Cascade)
 
   @@map("channel")
+  @@index([guildId])
+  @@index([parentId])
 }
prisma/schema/user.prisma (1)

1-23: Consider uniqueness and query indexes.

If your domain expects (username, discriminator) uniqueness, encode it; add an index on bot for frequent filters.

Apply if appropriate:

 model User {
   id            String   @id
   createdAt     DateTime @default(now())
   updatedAt     DateTime @updatedAt
   username      String
   discriminator String
   avatar        String?
   bot           Boolean  @default(false)
@@
   @@map("user")
+  @@unique([username, discriminator]) // if required by business rules
+  @@index([bot])
 }
src/processors/voice.ts (1)

45-64: Switch case writes should be atomic.

Wrap leave+join writes in a single transaction to avoid partial persistence on failure.

Apply:

-            await this.db.voiceEvent.create({
-              data: {
-                guildId: voiceState.guild_id,
-                channelId: currentSession.channelId,
-                userId: voiceState.user_id,
-                action: 'leave',
-                duration: Math.floor(duration / 1000),
-                timestamp: new Date(),
-              },
-            });
-
-            await this.db.voiceEvent.create({
-              data: {
-                guildId: voiceState.guild_id,
-                channelId: voiceState.channel_id,
-                userId: voiceState.user_id,
-                action: 'join',
-                timestamp: new Date(),
-              },
-            });
+            await this.db.$transaction([
+              this.db.voiceEvent.create({
+                data: {
+                  guildId: voiceState.guild_id,
+                  channelId: currentSession.channelId,
+                  userId: voiceState.user_id,
+                  action: 'leave',
+                  duration: Math.floor(duration / 1000),
+                  timestamp: new Date(),
+                },
+              }),
+              this.db.voiceEvent.create({
+                data: {
+                  guildId: voiceState.guild_id,
+                  channelId: voiceState.channel_id,
+                  userId: voiceState.user_id,
+                  action: 'join',
+                  timestamp: new Date(),
+                },
+              }),
+            ]);

Optional: ensure user/channel exist (reuse BaseProcessor helpers) to prevent FK failures.

src/processors/member.ts (1)

55-64: Optional: log leave only when an active membership existed.

You already filter leftAt: null, but updateMany may affect 0 rows. Consider guarding the event on updated count.

Example:

const { count } = await this.db.member.updateMany({ /* ... */ });
if (count > 0) {
  await this.db.memberEvent.create({ /* leave event */ });
}
prisma/schema/message-event.prisma (1)

8-16: Add Channel relation and analytics indexes; review cascade semantics.

  • channelId has no FK. Consider linking to Channel to prevent orphans:
    channel Channel? @relation(fields: [channelId], references: [id], onDelete: SetNull)
  • Add indexes used by stats/timeline:
    @@index([guildId, timestamp])
    @@index([guildId, userId, timestamp])
  • onDelete: Cascade on User will wipe historical analytics if a user row is removed. Confirm intended retention; SetNull + userId optional may be preferable.

Also applies to: 18-19

prisma/schema/member-event.prisma (1)

5-16: Tighten data types and add indexes for query patterns.

  • roles as String containing JSON is brittle. Prefer Json (if provider allows) or document strict encoding/decoding path centrally.
  • action should be an enum (e.g., JOIN, LEAVE, UPDATE) to avoid free‑form values.
  • Add analytics indexes:
    @@index([guildId, timestamp])
    @@index([guildId, userId, timestamp])

Review onDelete: Cascade for User as it impacts historical analytics.

prisma/schema/voice-event.prisma (1)

8-17: Add Channel relation and performance indexes; validate duration semantics.

  • Link channelId to Channel to avoid orphans:
    channel Channel? @relation(fields: [channelId], references: [id], onDelete: SetNull)
  • Add:
    @@index([guildId, timestamp])
    @@index([guildId, userId, timestamp])
  • Ensure only 'leave' events carry non-null, non-negative duration; enforce in write path and tests.
prisma/schema/member.prisma (1)

4-18: Add composite unique and indexes; consider Json for roles.

  • If Member is the current state, enforce uniqueness:
    @@unique([guildId, userId])
  • Add:
    @@index([guildId])
    @@index([guildId, userId])
  • roles as JSON is preferable to serialized String for queryability/validation.
prisma/schema/presence-event.prisma (1)

5-18: Constrain status to an enum and add analytics indexes.

  • Define an enum PresenceStatus { ONLINE, IDLE, DND, OFFLINE } and set status to that enum.
  • Add:
    @@index([guildId, timestamp])
    @@index([guildId, userId, timestamp])
  • Reconfirm onDelete: Cascade on User aligns with retention expectations.
prisma/schema/guild.prisma (1)

5-12: Add supporting indexes for common access patterns.

Given frequent filtering/joining by ownerId and createdAt, consider:

  • @@index([ownerId])
  • @@index([createdAt])

Also applies to: 22-23

src/stats/queries/user.ts (1)

21-23: Quote table name "user" for portability.

"user" is reserved in some SQL dialects (e.g., Postgres). Quote it to avoid future breakage.

-        INNER JOIN user u ON m.userId = u.id
+        INNER JOIN "user" u ON m.userId = u.id

Apply similarly to the voice query.

src/processors/base.ts (2)

107-120: Upsert channel should refresh mutable fields.

Empty update {} leaves stale name/type/parentId after renames. Update safely.

-        update: {},
+        update: {
+          name: (channelData.name as string) || 'Unknown Channel',
+          type: (channelData.type as number) || 0,
+          parentId: (channelData.parent_id as string | null) ?? null,
+        },

24-29: Prefer nullish coalescing over || for defaults.

'||' treats '' and 0 as falsy; '??' only defaults for null/undefined.

-          username: (userData.username as string) || 'Unknown',
-          discriminator: (userData.discriminator as string) || '0000',
-          avatar: userData.avatar as string | null,
-          bot: (userData.bot as boolean) || false,
+          username: (userData.username as string) ?? 'Unknown',
+          discriminator: (userData.discriminator as string) ?? '0000',
+          avatar: (userData.avatar as string | null) ?? null,
+          bot: Boolean(userData.bot),
@@
-          name: (guildData.name as string) || 'Unknown Guild',
-          icon: guildData.icon as string | null,
+          name: (guildData.name as string) ?? 'Unknown Guild',
+          icon: (guildData.icon as string | null) ?? null,
@@
-          name: (channelData.name as string) || 'Unknown Channel',
+          name: (channelData.name as string) ?? 'Unknown Channel',

Also applies to: 68-73, 113-118

src/export/exporter.ts (2)

172-203: Type the voice map callback and avoid implicit any.

Fix TS7006 at Line 195 by typing v.

-    const voice = await this.db.voiceEvent.findMany({
+    type VoiceRow = {
+      id: string;
+      userId: string;
+      channelId: string | null;
+      action: string;
+      duration: number | null;
+      timestamp: Date;
+    };
+    const voice = await this.db.voiceEvent.findMany({
@@
-    data.voice = voice.map((v) => ({
+    data.voice = voice.map((v: VoiceRow) => ({
       id: v.id,
       userId: v.userId,
       channelId: v.channelId || '',
       action: v.action,
       duration: v.duration || undefined,
       timestamp: v.timestamp,
     }));

150-170: Consider pagination instead of hard take=10000.

Hard caps risk truncation for large exports. Use cursor/skip-take batching to stream results.

src/db/factory.ts (2)

23-33: Avoid insecure default MySQL password and encode credentials.

Defaulting to 'password' is risky; prefer empty or require explicit config. Also URI‑encode.

-  const username = config.username || 'root';
-  const password = config.password || 'password';
-  return `mysql://${username}:${password}@${host}:${port}/${database}`;
+  const username = encodeURIComponent(config.username || 'root');
+  const password = encodeURIComponent(config.password || '');
+  return `mysql://${username}:${password}@${host}:${port}/${database}`;

55-75: Harden connection lifecycle (optional).

Wrap $connect in try/catch to surface clearer errors and add optional Prisma logs in dev.

-  await prisma.$connect();
+  try {
+    await prisma.$connect();
+  } catch (e) {
+    await prisma.$disconnect().catch(() => {});
+    throw new Error(`Failed to connect to database at ${datasourceUrl}: ${String(e)}`);
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3bd7d60 and f9334db.

⛔ Files ignored due to path filters (7)
  • bun.lock is excluded by !**/*.lock
  • src/db/generated/mysql/index.ts is excluded by !**/generated/**
  • src/db/generated/mysql/schema.ts is excluded by !**/generated/**
  • src/db/generated/postgres/index.ts is excluded by !**/generated/**
  • src/db/generated/postgres/schema.ts is excluded by !**/generated/**
  • src/db/generated/sqlite/index.ts is excluded by !**/generated/**
  • src/db/generated/sqlite/schema.ts is excluded by !**/generated/**
📒 Files selected for processing (83)
  • drizzle.mysql.config.ts (0 hunks)
  • drizzle.postgres.config.ts (0 hunks)
  • drizzle.sqlite.config.ts (0 hunks)
  • drizzle/mysql/0000_short_mercury.sql (0 hunks)
  • drizzle/mysql/0001_giant_dreadnoughts.sql (0 hunks)
  • drizzle/mysql/0002_square_rhino.sql (0 hunks)
  • drizzle/mysql/meta/0000_snapshot.json (0 hunks)
  • drizzle/mysql/meta/0001_snapshot.json (0 hunks)
  • drizzle/mysql/meta/0002_snapshot.json (0 hunks)
  • drizzle/mysql/meta/_journal.json (0 hunks)
  • drizzle/postgres/0000_fast_peter_parker.sql (0 hunks)
  • drizzle/postgres/0001_burly_molly_hayes.sql (0 hunks)
  • drizzle/postgres/0002_oval_pyro.sql (0 hunks)
  • drizzle/postgres/0003_cheerful_blacklash.sql (0 hunks)
  • drizzle/postgres/meta/0000_snapshot.json (0 hunks)
  • drizzle/postgres/meta/0001_snapshot.json (0 hunks)
  • drizzle/postgres/meta/0002_snapshot.json (0 hunks)
  • drizzle/postgres/meta/0003_snapshot.json (0 hunks)
  • drizzle/postgres/meta/_journal.json (0 hunks)
  • drizzle/sqlite/0000_good_purple_man.sql (0 hunks)
  • drizzle/sqlite/0001_tough_tag.sql (0 hunks)
  • drizzle/sqlite/0002_salty_layla_miller.sql (0 hunks)
  • drizzle/sqlite/0003_spooky_roughhouse.sql (0 hunks)
  • drizzle/sqlite/0004_melted_darwin.sql (0 hunks)
  • drizzle/sqlite/meta/0000_snapshot.json (0 hunks)
  • drizzle/sqlite/meta/0001_snapshot.json (0 hunks)
  • drizzle/sqlite/meta/0002_snapshot.json (0 hunks)
  • drizzle/sqlite/meta/0003_snapshot.json (0 hunks)
  • drizzle/sqlite/meta/0004_snapshot.json (0 hunks)
  • drizzle/sqlite/meta/_journal.json (0 hunks)
  • package.json (1 hunks)
  • prisma.config.ts (1 hunks)
  • prisma/schema/base.prisma (1 hunks)
  • prisma/schema/channel.prisma (1 hunks)
  • prisma/schema/guild.prisma (1 hunks)
  • prisma/schema/member-event.prisma (1 hunks)
  • prisma/schema/member.prisma (1 hunks)
  • prisma/schema/message-event.prisma (1 hunks)
  • prisma/schema/presence-event.prisma (1 hunks)
  • prisma/schema/reaction-event.prisma (1 hunks)
  • prisma/schema/user.prisma (1 hunks)
  • prisma/schema/voice-event.prisma (1 hunks)
  • scripts/generate-schemas.ts (0 hunks)
  • src/analytics/insights.ts (2 hunks)
  • src/db/connection.ts (2 hunks)
  • src/db/factory.ts (1 hunks)
  • src/db/index.ts (1 hunks)
  • src/db/interface.ts (1 hunks)
  • src/db/postgres/connection.ts (0 hunks)
  • src/db/schema/index.ts (0 hunks)
  • src/db/sqlite/connection.ts (0 hunks)
  • src/db/types.ts (2 hunks)
  • src/db/utils.ts (0 hunks)
  • src/export/exporter.ts (2 hunks)
  • src/generators/base.ts (0 hunks)
  • src/generators/generator.ts (0 hunks)
  • src/generators/index.ts (0 hunks)
  • src/generators/mysql.ts (0 hunks)
  • src/generators/postgres.ts (0 hunks)
  • src/generators/sqlite.ts (0 hunks)
  • src/handlers/guildHandler.ts (1 hunks)
  • src/migrations/index.ts (0 hunks)
  • src/migrations/manager.ts (0 hunks)
  • src/processors/base.ts (6 hunks)
  • src/processors/guild.ts (2 hunks)
  • src/processors/member.ts (3 hunks)
  • src/processors/message.ts (2 hunks)
  • src/processors/presence.ts (2 hunks)
  • src/processors/reaction.ts (4 hunks)
  • src/processors/voice.ts (4 hunks)
  • src/schemas/base.ts (0 hunks)
  • src/schemas/events.ts (0 hunks)
  • src/schemas/guild.ts (0 hunks)
  • src/schemas/index.ts (0 hunks)
  • src/stats/aggregator.ts (1 hunks)
  • src/stats/helpers.ts (0 hunks)
  • src/stats/queries/channel.ts (1 hunks)
  • src/stats/queries/member.ts (1 hunks)
  • src/stats/queries/message.ts (1 hunks)
  • src/stats/queries/reaction.ts (1 hunks)
  • src/stats/queries/user.ts (1 hunks)
  • src/stats/queries/voice.ts (1 hunks)
  • src/types/api/message/mention.ts (1 hunks)
💤 Files with no reviewable changes (48)
  • drizzle/sqlite/0002_salty_layla_miller.sql
  • drizzle.mysql.config.ts
  • src/schemas/index.ts
  • drizzle/sqlite/0001_tough_tag.sql
  • src/generators/mysql.ts
  • src/schemas/guild.ts
  • drizzle.postgres.config.ts
  • src/db/postgres/connection.ts
  • src/migrations/index.ts
  • drizzle/sqlite/0000_good_purple_man.sql
  • scripts/generate-schemas.ts
  • drizzle/postgres/0001_burly_molly_hayes.sql
  • src/generators/sqlite.ts
  • src/schemas/events.ts
  • src/db/sqlite/connection.ts
  • drizzle/postgres/meta/_journal.json
  • drizzle/mysql/meta/0002_snapshot.json
  • src/generators/base.ts
  • drizzle/postgres/0002_oval_pyro.sql
  • drizzle/postgres/0003_cheerful_blacklash.sql
  • drizzle/mysql/meta/0001_snapshot.json
  • drizzle/sqlite/meta/0003_snapshot.json
  • drizzle/postgres/meta/0000_snapshot.json
  • drizzle/sqlite/0004_melted_darwin.sql
  • drizzle/sqlite/meta/_journal.json
  • drizzle/sqlite/0003_spooky_roughhouse.sql
  • drizzle/sqlite/meta/0004_snapshot.json
  • drizzle/sqlite/meta/0000_snapshot.json
  • drizzle/postgres/0000_fast_peter_parker.sql
  • drizzle/postgres/meta/0002_snapshot.json
  • src/schemas/base.ts
  • drizzle/sqlite/meta/0001_snapshot.json
  • src/db/utils.ts
  • drizzle/mysql/0000_short_mercury.sql
  • drizzle/sqlite/meta/0002_snapshot.json
  • src/generators/postgres.ts
  • drizzle/mysql/0001_giant_dreadnoughts.sql
  • src/generators/generator.ts
  • src/stats/helpers.ts
  • src/migrations/manager.ts
  • drizzle/mysql/meta/_journal.json
  • src/generators/index.ts
  • drizzle.sqlite.config.ts
  • drizzle/postgres/meta/0003_snapshot.json
  • drizzle/mysql/meta/0000_snapshot.json
  • src/db/schema/index.ts
  • drizzle/postgres/meta/0001_snapshot.json
  • drizzle/mysql/0002_square_rhino.sql
🧰 Additional context used
🧬 Code graph analysis (12)
src/db/interface.ts (1)
src/db/index.ts (1)
  • CommonDatabase (6-6)
src/stats/queries/member.ts (1)
src/utils/date.ts (1)
  • createDateSince (1-2)
src/stats/queries/channel.ts (1)
src/utils/date.ts (1)
  • createDateSince (1-2)
src/processors/base.ts (2)
src/db/index.ts (1)
  • CommonDatabase (6-6)
src/db/interface.ts (1)
  • CommonDatabase (6-6)
src/analytics/insights.ts (1)
src/utils/date.ts (1)
  • createDateSince (1-2)
src/stats/queries/message.ts (1)
src/utils/date.ts (1)
  • createDateSince (1-2)
src/db/factory.ts (2)
src/db/types.ts (4)
  • PostgresConfig (10-22)
  • MysqlConfig (24-33)
  • DatabaseConfig (35-35)
  • DatabaseInstance (49-49)
src/db/index.ts (2)
  • createDatabaseConnection (3-3)
  • createDatabaseConnection (4-4)
src/handlers/guildHandler.ts (2)
src/types/event/index.ts (1)
  • GuildUpdate (65-65)
src/types/event/guilds.ts (1)
  • GuildUpdate (39-39)
src/export/exporter.ts (1)
src/export/formats.ts (2)
  • ExportData (15-60)
  • ExportOptions (1-13)
src/stats/queries/user.ts (1)
src/utils/date.ts (1)
  • createDateSince (1-2)
src/stats/queries/reaction.ts (1)
src/utils/date.ts (1)
  • createDateSince (1-2)
src/stats/queries/voice.ts (1)
src/utils/date.ts (1)
  • createDateSince (1-2)
🪛 GitHub Actions: TypeScript Check
src/db/interface.ts

[error] 1-1: TS2307: Cannot find module '@prisma/client' or its corresponding type declarations.

src/stats/queries/member.ts

[error] 36-36: TS7006: Parameter 'r' implicitly has an 'any' type.


[error] 37-37: TS7006: Parameter 'r' implicitly has an 'any' type.

src/stats/queries/channel.ts

[error] 34-34: TS7006: Parameter 'r' implicitly has an 'any' type.

src/analytics/insights.ts

[error] 32-32: TS2345: Argument of type '{ hour: number; activity: {}; }[]' is not assignable to parameter of type '{ hour: number; activity: number; }[]'. Type '{}' is not assignable to number.


[error] 33-33: TS2345: Argument of type '{ hour: number; activity: {}; }[]' is not assignable to parameter of type '{ hour: number; activity: number; }[]'. Type '{}' is not assignable to number.


[error] 80-80: TS7006: Parameter 'r' implicitly has an 'any' type.

src/stats/queries/message.ts

[error] 55-55: TS7006: Parameter 'r' implicitly has an 'any' type.

src/db/factory.ts

[error] 1-1: TS2307: Cannot find module '@prisma/client' or its corresponding type declarations.

src/export/exporter.ts

[error] 104-104: TS7006: Parameter 'row' implicitly has an 'any' type.


[error] 138-138: TS7006: Parameter 'c' implicitly has an 'any' type.


[error] 195-195: TS7006: Parameter 'v' implicitly has an 'any' type.

src/stats/queries/user.ts

[error] 30-30: TS7006: Parameter 'r' implicitly has an 'any' type.


[error] 54-54: TS7006: Parameter 'r' implicitly has an 'any' type.

src/stats/queries/reaction.ts

[error] 24-24: TS7006: Parameter 'r' implicitly has an 'any' type.


[error] 58-58: TS7006: Parameter 'r' implicitly has an 'any' type.


[error] 93-93: TS7006: Parameter 'r' implicitly has an 'any' type.


[error] 125-125: TS7006: Parameter 'r' implicitly has an 'any' type.

src/db/types.ts

[error] 1-1: TS2307: Cannot find module '@prisma/client' or its corresponding type declarations.

🔇 Additional comments (19)
src/types/api/message/mention.ts (2)

1-1: LGTM! Import consolidation aligns with the migration.

The import statement cleanly consolidates both APIGuildMember and GuildMember from a single source, which improves maintainability and aligns with the Prisma migration's type reorganization.


4-6: LGTM! Type definitions correctly updated for the migration.

The Mention type has been properly updated to use GuildMember instead of Member, maintaining consistency with the import changes and the broader Prisma migration. Since TypeScript compilation passed, type compatibility is confirmed.

prisma/schema/base.prisma (1)

1-8: Base schema looks good.

Generator and SQLite datasource setup are correct for a split schema. Ensure DATABASE_URL points to your desired file path in all environments.

Confirm your Prisma version supports multi-file schemas with prisma.config.ts (it should if generation passed).

src/handlers/guildHandler.ts (1)

42-61: Remove dead event listener registrations in constructor.

The dispatcher.on() calls (lines 30–39) register handlers that are never invoked. EventDispatcher uses internal routing via switch/processEvent() and never emits events with these listener names. The handlers are unreachable dead code; remove all 11 registrations:

export class GuildHandler extends Handler {
  public constructor(database: CommonDatabase, dispatcher: EventDispatcher) {
    super(database, [Intents.GUILDS]);
-   dispatcher.on(Events.GUILD_CREATE, this.handleGuildCreate);
-   dispatcher.on(Events.GUILD_UPDATE, this.handleGuildUpdate);
-   dispatcher.on(Events.GUILD_DELETE, this.handleGuildDelete);
-   dispatcher.on(Events.GUILD_ROLE_CREATE, this.handleGuildRoleCreate);
-   dispatcher.on(Events.GUILD_ROLE_UPDATE, this.handleGuildRoleUpdate);
-   dispatcher.on(Events.GUILD_ROLE_DELETE, this.handleGuildRoleDelete);
-   dispatcher.on(Events.CHANNEL_CREATE, this.handleChannelCreate);
-   dispatcher.on(Events.CHANNEL_UPDATE, this.handleChannelUpdate);
-   dispatcher.on(Events.CHANNEL_DELETE, this.handleChannelDelete);
-   dispatcher.on(Events.CHANNEL_PINS_UPDATE, this.handleChannelPinsUpdate);
  }

The handler methods themselves are not called and should either be removed or refactored into the dispatcher's processor classes if their logic is needed.

Likely an incorrect or invalid review comment.

src/processors/guild.ts (1)

4-6: Docs LGTM. Brief method JSDoc improves readability.

src/processors/presence.ts (1)

10-12: Docs LGTM.

src/processors/reaction.ts (4)

20-22: Docs LGTM.


32-44: Event write LGTM. Payload shape looks consistent; null-coalesce for emojiId is fine.

If ReactionEvent has FKs to user/channel, ensure upstream creation succeeds or use a transaction with the create below.


50-52: Docs LGTM.


62-74: Event write LGTM. Mirrors add-case; consistent fields.

src/processors/voice.ts (1)

7-9: Docs LGTM.

src/db/index.ts (1)

7-7: Export LGTM. Adding DatabaseManager aligns with the new DB management flow.

src/processors/member.ts (1)

4-6: Docs LGTM.

src/stats/queries/voice.ts (1)

13-31: LGTM with tiny nits.

  • Logic is sound; Prisma aggregate is appropriate.
  • Prefer nullish coalescing: result._sum.duration ?? 0.
  • Verify action values exactly match stored VoiceEvent.action (e.g., 'leave' vs 'LEFT').
src/stats/queries/message.ts (2)

13-26: Aggregate path looks good; confirm provider-specific SQL below.

  • getStats aggregate is fine.
  • The timeline SQL uses SQLite strftime(). If you may switch providers (e.g., PostgreSQL), this will break; consider a provider-specific switch or a query builder.

Also applies to: 28-34


55-58: Type annotation already provided via generic parameter—proposed diff is unnecessary.

The code does not have a TS7006 issue. The results variable at line 43 is explicitly typed with a generic parameter: this.db.$queryRaw<Array<{ hour: number; count: bigint }>>. TypeScript infers the parameter type r in the map callback from this generic declaration, making the proposed explicit annotation on the callback parameter redundant.

Likely an incorrect or invalid review comment.

prisma/schema/guild.prisma (1)

13-13: Confirm cascade semantics for owner relation.

onDelete: Cascade means deleting a User cascades and deletes its Guild(s). Is that intended? Many systems prefer SetNull on ownerId (then reassign) to avoid accidental guild loss. If you want SetNull, make ownerId optional and change the referential action.

src/analytics/insights.ts (1)

151-161: Guard against zero/undefined memberCount and ensure number type.

If guild.memberCount is null/undefined, default to 1 to avoid division by zero; ensure numeric.

Please confirm schema has memberCount default or that nulls won’t occur. Current fallback is fine for safety.

src/stats/queries/reaction.ts (1)

1-6: Prisma model names verified—no issues found.

The models "ReactionEvent" and "User" both exist in the Prisma schema with the correct identifiers, confirming that raw SQL references match actual table/model names.

- Add bigint types for COUNT and SUM aggregations
- Add proper type annotations for map callbacks
- Fix type compatibility issues in queries and exports
- Resolve all TypeScript compilation errors
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (7)
src/analytics/insights.ts (1)

66-87: Add explicit types for getHourlyActivity.

TypeScript still infers {} for activity, producing the TS7006/assignment error reported in the pipeline. Please spell out the return type, give the Map explicit generics, and use ?? so the number stays a number.

-  private async getHourlyActivity(guildId: string, days: number) {
+  private async getHourlyActivity(
+    guildId: string,
+    days: number
+  ): Promise<Array<{ hour: number; activity: number }>> {
@@
-    const activityMap = new Map(
-      result.map((r: { hour: number; activity: bigint }) => [r.hour, Number(r.activity)])
-    );
+    const activityMap = new Map<number, number>(
+      result.map((r: { hour: number; activity: bigint }) => [r.hour, Number(r.activity)])
+    );
@@
-      activity: activityMap.get(hour) || 0,
+      activity: activityMap.get(hour) ?? 0,
src/stats/queries/member.ts (1)

13-38: Order growth timelines deterministically.

The joins/leaves queries still omit ORDER BY, so result order depends on the engine and can shuffle between runs—breaking charting/analytics. Please add sorting on the date column to both queries so consumers get a stable chronological sequence.

       WHERE guildId = ${guildId}
         AND action = 'join'
         AND timestamp >= ${since}
       GROUP BY date(timestamp)
+      ORDER BY date(timestamp)
@@
       WHERE guildId = ${guildId}
         AND action = 'leave'
         AND timestamp >= ${since}
       GROUP BY date(timestamp)
+      ORDER BY date(timestamp)
src/stats/queries/message.ts (1)

43-53: Eliminate string interpolation in the raw SQL filter.

${userId ? \AND userId = ${userId}` : ''}splices an ad-hoc SQL fragment into$queryRaw, bypassing parameterization and reopening the injection hole the earlier review called out. Please compose the optional clause with Prisma.sql` instead.

-    const results = await this.db.$queryRaw<
-      Array<{ hour: number; count: bigint }>
-    >`
-      SELECT
-        CAST(strftime('%H', timestamp) AS INTEGER) as hour,
-        COUNT(*) as count
-      FROM messageevent
-      WHERE guildId = ${guildId}
-        AND timestamp >= ${since}
-        ${userId ? `AND userId = ${userId}` : ''}
-      GROUP BY strftime('%H', timestamp)
-      ORDER BY strftime('%H', timestamp)
-    `;
+    const userClause = userId ? Prisma.sql`AND userId = ${userId}` : Prisma.sql``;
+    const results = await this.db.$queryRaw<Array<{ hour: number; count: bigint }>>(
+      Prisma.sql`
+        SELECT
+          CAST(strftime('%H', timestamp) AS INTEGER) as hour,
+          COUNT(*) as count
+        FROM messageevent
+        WHERE guildId = ${guildId}
+          AND timestamp >= ${since}
+          ${userClause}
+        GROUP BY strftime('%H', timestamp)
+        ORDER BY strftime('%H', timestamp)
+      `
+    );
import { Prisma } from '@prisma/client';
src/stats/queries/reaction.ts (4)

13-22: Fix SQL injection vulnerability and use portable table names.

The nested template literal at line 20 bypasses Prisma's parameterization and creates an SQL injection risk. Additionally, the unquoted table name reactionevent will fail on Postgres (which expects "ReactionEvent").

Apply this diff to use Prisma.sql fragments for safe conditional SQL and quoted identifiers:

+import { Prisma } from '@prisma/client';
 
 export class ReactionQueries {
   constructor(private db: CommonDatabase) {}
@@
   async getReactionStats(guildId: string, userId?: string, days = 30) {
     const since = createDateSince(days);
 
+    const userFilter = userId ? Prisma.sql`AND "userId" = ${userId}` : Prisma.empty;
     const results = await this.db.$queryRaw<Array<{ action: string; count: bigint }>>`
       SELECT
-        action,
+        "action",
         COUNT(*) as count
-      FROM reactionevent
-      WHERE guildId = ${guildId}
-        AND timestamp >= ${since}
-        ${userId ? `AND userId = ${userId}` : ''}
-      GROUP BY action
+      FROM "ReactionEvent"
+      WHERE "guildId" = ${guildId}
+        AND "timestamp" >= ${since}
+        ${userFilter}
+      GROUP BY "action"
     `;

36-56: Add emojiAnimated to GROUP BY and quote identifiers for Postgres compatibility.

The query selects emojiAnimated (line 47) but omits it from the GROUP BY clause (line 53), which violates SQL standards and will fail on Postgres/MySQL with ONLY_FULL_GROUP_BY. Additionally, unquoted identifiers will cause table/column resolution issues on Postgres.

Apply this diff:

     const results = await this.db.$queryRaw<
       Array<{
         emojiId: string | null;
         emojiName: string;
-        emojiAnimated: number;
+        emojiAnimated: number | boolean;
         count: bigint;
       }>
     >`
       SELECT
-        emojiId,
-        emojiName,
-        emojiAnimated,
+        "emojiId",
+        "emojiName",
+        "emojiAnimated",
         COUNT(*) as count
-      FROM reactionevent
-      WHERE guildId = ${guildId}
-        AND action = 'add'
-        AND timestamp >= ${since}
-      GROUP BY emojiId, emojiName
+      FROM "ReactionEvent"
+      WHERE "guildId" = ${guildId}
+        AND "action" = 'add'
+        AND "timestamp" >= ${since}
+      GROUP BY "emojiId", "emojiName", "emojiAnimated"
       ORDER BY count DESC
       LIMIT ${limit}
     `;

72-91: Add u.username to GROUP BY and quote all identifiers.

The query selects u.username (line 81) but only groups by r.userId (line 88). This violates SQL standards and will fail on Postgres/MySQL with ONLY_FULL_GROUP_BY enabled. Additionally, unquoted identifiers cause portability issues.

Apply this diff:

     const results = await this.db.$queryRaw<
       Array<{
         userId: string;
         username: string;
         reactions: bigint;
       }>
     >`
       SELECT
-        r.userId,
-        u.username,
+        r."userId",
+        u."username",
         COUNT(*) as reactions
-      FROM reactionevent r
-      LEFT JOIN user u ON u.id = r.userId
-      WHERE r.guildId = ${guildId}
-        AND r.action = 'add'
-        AND r.timestamp >= ${since}
-      GROUP BY r.userId
+      FROM "ReactionEvent" r
+      LEFT JOIN "User" u ON u."id" = r."userId"
+      WHERE r."guildId" = ${guildId}
+        AND r."action" = 'add'
+        AND r."timestamp" >= ${since}
+      GROUP BY r."userId", u."username"
       ORDER BY reactions DESC
       LIMIT ${limit}
     `;

106-123: Replace SQLite-specific date functions with portable solution.

The date() and strftime() functions (lines 114-115) are SQLite-specific and will fail on Postgres/MySQL. Since the codebase supports multiple database backends through src/db/factory.ts, this breaks portability.

Consider computing time buckets in JavaScript to avoid dialect-specific SQL:

   async getReactionTimeline(guildId: string, days = 7) {
     const since = createDateSince(days);
 
-    const results = await this.db.$queryRaw<
-      Array<{
-        date: string;
-        hour: number;
-        reactions: bigint;
-      }>
-    >`
-      SELECT
-        date(timestamp) as date,
-        CAST(strftime('%H', timestamp) AS INTEGER) as hour,
-        COUNT(*) as reactions
-      FROM reactionevent
-      WHERE guildId = ${guildId}
-        AND action = 'add'
-        AND timestamp >= ${since}
-      GROUP BY date(timestamp), strftime('%H', timestamp)
-      ORDER BY date, hour
-    `;
-
-    return results.map((r: { date: string; hour: number; reactions: bigint }) => ({
-      date: r.date,
-      hour: r.hour,
-      reactions: Number(r.reactions),
-    }));
+    const rows = await this.db.reactionEvent.findMany({
+      where: { guildId, action: 'add', timestamp: { gte: since } },
+      select: { timestamp: true },
+    });
+    
+    const buckets = new Map<string, number>();
+    for (const { timestamp } of rows) {
+      const d = new Date(timestamp);
+      const date = d.toISOString().slice(0, 10);
+      const hour = d.getUTCHours();
+      const key = `${date}|${hour}`;
+      buckets.set(key, (buckets.get(key) ?? 0) + 1);
+    }
+    
+    return Array.from(buckets.entries())
+      .map(([k, count]) => {
+        const [date, hourStr] = k.split('|');
+        return { date, hour: Number(hourStr), reactions: count };
+      })
+      .sort((a, b) => (a.date === b.date ? a.hour - b.hour : a.date < b.date ? -1 : 1));
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9334db and eb99aa1.

📒 Files selected for processing (7)
  • src/analytics/insights.ts (2 hunks)
  • src/export/exporter.ts (2 hunks)
  • src/stats/queries/channel.ts (1 hunks)
  • src/stats/queries/member.ts (1 hunks)
  • src/stats/queries/message.ts (1 hunks)
  • src/stats/queries/reaction.ts (1 hunks)
  • src/stats/queries/user.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
src/stats/queries/channel.ts (1)
src/utils/date.ts (1)
  • createDateSince (1-2)
src/stats/queries/member.ts (1)
src/utils/date.ts (1)
  • createDateSince (1-2)
src/stats/queries/user.ts (1)
src/utils/date.ts (1)
  • createDateSince (1-2)
src/export/exporter.ts (1)
src/export/formats.ts (2)
  • ExportData (15-60)
  • ExportOptions (1-13)
src/stats/queries/reaction.ts (1)
src/utils/date.ts (1)
  • createDateSince (1-2)
src/stats/queries/message.ts (1)
src/utils/date.ts (1)
  • createDateSince (1-2)
src/analytics/insights.ts (1)
src/utils/date.ts (1)
  • createDateSince (1-2)
🪛 GitHub Actions: Biome Check
src/stats/queries/channel.ts

[error] 32-46: Formatter would have reformatted code in channel.ts. Run the formatter to apply style fixes.

src/stats/queries/member.ts

[error] 34-43: Formatter would have reformatted code in member.ts. Run the formatter to apply style fixes.

src/export/exporter.ts

[error] 102-111: Formatter would have reformatted code in exporter.ts. Run the formatter to apply style fixes.

src/stats/queries/reaction.ts

[error] 56-70: Formatter would have reformatted code in reaction.ts. Run the formatter to apply style fixes.

🪛 GitHub Actions: Format Check
src/stats/queries/channel.ts

[error] 34-46: Formatter would reformat return mapping in channel query. Code would be rewritten by the formatter.


[error] 34-46: TOOLNAME: Formatter detected formatting issues. Run the formatter again to apply fixes.

src/stats/queries/member.ts

[error] 34-43: Formatter would reformat joins/leaves mapping in member query. Code would be rewritten by the formatter.

src/export/exporter.ts

[error] 102-110: Formatter would reformat data.users mapping. Code would be rewritten by the formatter.


[error] 136-147: Formatter would reformat data.channels mapping. Code would be rewritten by the formatter.


[error] 193-215: Formatter would reformat data.voice mapping. Code would be rewritten by the formatter.


[error] 102-110: TOOLNAME: Formatter detected formatting issues. Run the formatter again to apply fixes.

src/stats/queries/reaction.ts

[error] 58-70: Formatter would reformat reaction count mapping. Code would be rewritten by the formatter.


[error] 58-70: TOOLNAME: Formatter detected formatting issues. Run the formatter again to apply fixes.

🪛 GitHub Actions: TypeScript Check
src/analytics/insights.ts

[error] 32-32: Type '({ hour: number; activity: {}; } | { hour: number; activity: {}; }[])' is not assignable to type '{ hour: number; activity: number; }[]'. Property 'activity' has type '{}' which is not assignable to 'number'.


[error] 33-33: Type '{ hour: number; activity: {}; }[]' is not assignable to type '{ hour: number; activity: number; }[]'. Property 'activity' has type '{}' which is not assignable to 'number'.

@madeline-xs madeline-xs self-assigned this Oct 29, 2025
taco added 2 commits October 29, 2025 18:33
- Add Promise<Array<{ hour: number; activity: number }>> return type
- Resolve TypeScript type inference issue
- Regenerate Prisma Client
- Add Map<number, number> type annotation to activityMap
- Use nullish coalescing operator (??) instead of logical OR
- Resolve TypeScript type inference issue with Map.get()
@WxTaco
Copy link
Collaborator Author

WxTaco commented Oct 29, 2025

beanie update minor

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/analytics/insights.ts (2)

72-81: Use createdAt (not timestamp) and address cross‑DB portability.

  • Column mismatch: other queries use createdAt; using timestamp will 404 at runtime if the column doesn’t exist. This was previously flagged.
  • strftime() is SQLite‑only; this query will break on PostgreSQL/MySQL. Provide provider‑specific SQL or an abstraction.

Apply for SQLite (and align on createdAt):

-      SELECT
-        CAST(strftime('%H', timestamp) AS INTEGER) as hour,
+      SELECT
+        CAST(strftime('%H', createdAt) AS INTEGER) as hour,
         COUNT(*) as activity
       FROM messageevent
       WHERE guildId = ${guildId}
-        AND timestamp >= ${since}
-      GROUP BY strftime('%H', timestamp)
+        AND createdAt >= ${since}
+      GROUP BY strftime('%H', createdAt)
       ORDER BY hour

Suggested approach for portability:

  • SQLite: strftime('%H', createdAt)
  • PostgreSQL: EXTRACT(HOUR FROM createdAt)
  • MySQL: HOUR(createdAt)

Centralize this in a small helper that switches on provider. I can draft it if helpful.

#!/bin/bash
# Verify schema/usage consistency
rg -nP --glob '!**/node_modules/**' -C2 '\bmessageevent\b.*\b(timestamp|createdAt)\b' src prisma
rg -nP --glob '!**/node_modules/**' -C2 '\bmessageevent\b' prisma/schema

138-160: Message stats: createdAt vs timestamp again; portability; remove unused avgDuration; add indexes.

  • Use createdAt consistently (duplicate of earlier concern).
  • strftime/DATE functions must be provider‑specific (SQLite/Postgres/MySQL). See earlier comment.
  • avgDuration is selected but never used; drop it to avoid wasted work.

Apply:

-      this.db.$queryRaw<Array<{ totalMessages: bigint; activeUsers: bigint }>>`
+      this.db.$queryRaw<Array<{ totalMessages: bigint; activeUsers: bigint }>>`
         SELECT
           COUNT(*) as totalMessages,
           COUNT(DISTINCT userId) as activeUsers
         FROM messageevent
         WHERE guildId = ${guildId}
-          AND timestamp >= ${since}
+          AND createdAt >= ${since}
       `,
-      this.db.$queryRaw<Array<{ voiceUsers: bigint; avgDuration: number | null }>>`
+      this.db.$queryRaw<Array<{ voiceUsers: bigint }>>`
         SELECT
           COUNT(DISTINCT userId) as voiceUsers
-          AVG(duration) as avgDuration
         FROM voiceevent
         WHERE guildId = ${guildId}
           AND createdAt >= ${since}
           AND action = 'leave'
       `,

Operational advice:

  • Ensure composite indexes: messageevent(guildId, createdAt), voiceevent(guildId, createdAt, action) to keep COUNT/filters fast.
🧹 Nitpick comments (1)
src/analytics/insights.ts (1)

162-166: Prefer nullish coalescing with BigInt defaults; guard memberCount explicitly.

Avoid falsy pitfalls and make the divide‑by‑zero guard clear.

-    const totalMessages = Number(messageStats[0]?.totalMessages || 0);
-    const activeUsers = Number(messageStats[0]?.activeUsers || 0);
-    const voiceUsers = Number(voiceStats[0]?.voiceUsers || 0);
-    const totalMembers = guild?.memberCount || 1;
+    const totalMessages = Number(messageStats[0]?.totalMessages ?? 0n);
+    const activeUsers = Number(messageStats[0]?.activeUsers ?? 0n);
+    const voiceUsers = Number(voiceStats[0]?.voiceUsers ?? 0n);
+    const totalMembers = Math.max(1, guild?.memberCount ?? 0);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be0a650 and b9b89d0.

📒 Files selected for processing (2)
  • package.json (2 hunks)
  • src/analytics/insights.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/analytics/insights.ts (1)
src/utils/date.ts (1)
  • createDateSince (1-2)
🔇 Additional comments (4)
package.json (1)

3-3: Version bump is reasonable.

The minor version bump from 2.2.2 to 2.3.0 aligns with the significant infrastructure change (Drizzle → Prisma migration), though this could also justify a patch bump if no user-facing changes were introduced. No concerns here.

src/analytics/insights.ts (3)

66-69: Types for getHourlyActivity — LGTM.

Explicit return type and typed raw result resolve prior TS issues.


83-85: Typed Map and nullish coalescing — good.

BigInt→number conversion and defaulting via ?? keep types safe.

Also applies to: 89-90


97-119: Review comment is based on incorrect assumption about the action field.

The action field in the MemberEvent model is typed as String, not a Prisma enum. Therefore:

  • Using action: 'join' as a string literal is correct and appropriate
  • There is no MemberAction enum to use (and one shouldn't be added for a string field)

The suggestion to "prefer enum" is incorrect for this schema design.

The optional suggestion about adding an explicit return type to getMemberGrowthTrend is valid (the function currently has no return type annotation, only inferred typing), but this is a minor code clarity preference, not a correctness issue.

Likely an incorrect or invalid review comment.

@WxTaco
Copy link
Collaborator Author

WxTaco commented Oct 29, 2025

Will address failing type check when I get back to my computer later today.

…rrors

- Create main prisma/schema.prisma file that consolidates all model definitions
- Rename split schema files in prisma/schema/ to .prisma.bak to prevent Prisma validation conflicts
- This resolves TS2339 errors where CommonDatabase properties were not recognized during CI/CD
- Prisma client now generates correctly with all models (User, Guild, Channel, Member, MessageEvent, VoiceEvent, MemberEvent, PresenceEvent, ReactionEvent)
WxTaco added 11 commits October 29, 2025 17:52
- Add npm install -g @biomejs/biome to all three Biome workflows
- Remove bun pm rebuild @biomejs/biome step from format-check and lint-check
- Ensures Biome binary is available on all runners (self-hosted and cloud)
- Replace Bun setup with Node.js 20 setup
- Use npm install instead of bun install
- Use npx biome commands instead of bun run scripts
- Remove global biome installation
- Simplify workflow configuration
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
package.json (1)

27-28: Remove @prisma/internals from dependencies or pin version to match.

The critical issue—missing Prisma dependencies—is resolved. However:

  1. @prisma/internals is unused: No explicit imports of this package were found in the codebase. It should be removed from dependencies unless it's required by a specific build or CLI tool.

  2. Version mismatch: If retained, pin @prisma/internals to exact version 6.18.0 to match @prisma/client and prisma, preventing potential divergence from caret versioning (^6.18.0).

Recommendation: Remove @prisma/internals from dependencies entirely—it will remain available transitively if genuinely needed by Prisma tooling.

Also applies to: 40-40

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 460b496 and a7829db.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • dummy.md (1 hunks)
  • package.json (4 hunks)
✅ Files skipped from review due to trivial changes (1)
  • dummy.md

WxTaco added 14 commits October 29, 2025 18:15
- Update getHourlyActivity query to use createdAt column
- Update getEngagementMetrics messageStats query to use createdAt
- Ensure consistency with schema and other queries throughout codebase
- Prevents runtime errors from missing timestamp column
- Use encodeURIComponent for username and password in buildPostgresUrl
- Use encodeURIComponent for username and password in buildMysqlUrl
- Safely handle special characters like @, :, %, & in credentials
- Prevents DSN parsing errors from special characters in credentials
- Remove updatedAt from user upsert update payload
- Remove updatedAt from guild upsert update payload
- Let Prisma manage updatedAt automatically via @updatedat directive
- Prevents runtime errors from manual timestamp assignment
- Add id field to create block in member upsert operation
- Use same composite id format (guildId-userId) as where clause
- Prevents duplicate records from being created on subsequent upserts
- Ensures created record matches future upsert lookups
- Add id field to member upsert create block using composite key
- Prevents duplicate records from being created on subsequent upserts
- Tighten validateJoinData to check u.id exists and is a string
- Avoid undefined runtime errors when accessing userData.id
- Ensures created record matches upsert where clause
- Extract userId and validate it is a non-empty string before DB insert
- Skip processing if userId is missing or empty (trim check)
- Tighten validatePresence to check u.id exists and is a string
- Pass validated userId directly to presenceEvent.create
- Prevents storing empty or invalid userId values in database
- Add ORDER BY date ASC to joinsResults query for deterministic ordering
- Add ORDER BY date ASC to leavesResults query for deterministic ordering
- Annotate map callback parameters with proper types (bigint)
- Ensures stable timelines and satisfies TypeScript type checking
- Maintains Number() conversion for bigint to number
- Replace unsafe string interpolation with Prisma.sql fragments
- Build userIdFragment using Prisma.sql for proper parameterization
- Use empty Prisma.sql fragment when userId is not provided
- Ensures all values are passed as parameters, not raw strings
- Prevents SQL injection and maintains query safety
- Replace unsafe string interpolation with Prisma.sql for userFilter
- Use Prisma.empty for empty conditional fragments
- Quote all table and column identifiers (ReactionEvent, User, etc.)
- Update all queries to use parameterized conditions
- Ensures SQL injection prevention and database portability
- Maintains type safety with proper parameter binding
- Include username in GROUP BY clause alongside userId
- Ensures SQL compliance when selecting non-aggregated columns
- Map callback has explicit type annotation
- All identifiers remain properly quoted
…lity

- Remove SQLite-specific date() and strftime() functions from query
- Query now selects raw timestamp without database-specific functions
- Aggregate by date and hour in JavaScript using Map for bucketing
- Extract YYYY-MM-DD date and UTC hour from timestamp in JS
- Sort results by date then hour for consistent ordering
- Works with any database backend (PostgreSQL, MySQL, SQLite, etc.)
- Maintains same output shape: { date, hour, reactions }
- Add WHERE clause to filter users by guild activity
- Only include users with MessageEvent or VoiceEvent in target guild
- Prevents data leak of all database users in multi-guild deployments
- Ensures export contains only guild-relevant user statistics
- Maintains accurate guild-scoped analytics
@WxTaco
Copy link
Collaborator Author

WxTaco commented Oct 29, 2025

Beanie is already updating this PR to start 2.3.x

This PR has made me question all of my decisions up to this point in my life.

Copy link
Contributor

@madeline-xs madeline-xs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good<3

@madeline-xs madeline-xs merged commit e16460d into master Nov 6, 2025
8 checks passed
@madeline-xs madeline-xs deleted the database-to-prisma branch November 6, 2025 02:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants