Repository navigation
implement database schema - #1
Conversation
📝 WalkthroughWalkthroughIntroduces Drizzle ORM database infrastructure with PostgreSQL configuration, schema definitions, and migrations. Establishes a Neon serverless database client, creates Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (6)
drizzle/0000_cynical_ultimates.sql (2)
2-2: ConsiderGENERATED BY DEFAULT AS IDENTITYinstead ofALWAYS.
GENERATED ALWAYSrejects any explicitidinsert unless the caller usesOVERRIDING SYSTEM VALUE, which complicates seed scripts, data imports, and test fixtures.GENERATED BY DEFAULTstill auto-generates when omitted but lets you specify an id when needed. Applies tosubjectsas well.Purely a preference — keep as-is if strictness is intentional.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@drizzle/0000_cynical_ultimates.sql` at line 2, The column definition uses "GENERATED ALWAYS AS IDENTITY" for the "id" column (sequence "departments_id_seq") which forbids explicit id inserts; change it to "GENERATED BY DEFAULT AS IDENTITY" for both the departments "id" and the subjects "id" definitions so the DB still auto-generates ids when omitted but allows explicit ids for seed scripts and imports, updating the identity clause for those fields accordingly.
11-22: Add an index onsubjects.department_id.PostgreSQL does not automatically index foreign-key columns. Without an index on
subjects.department_id:
ON DELETE RESTRICTforces a sequential scan ofsubjectsfor every attempted department delete.- Common queries like "list subjects for a department" will also seq-scan as the table grows.
Recommend adding an index via Drizzle (
index("subjects_department_id_idx").on(subjects.departmentId)insrc/db/schema/app.ts) and regenerating the migration.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@drizzle/0000_cynical_ultimates.sql` around lines 11 - 22, Add a b-tree index on the foreign-key column to avoid sequential scans: in your schema file (src/db/schema/app.ts) add an index named subjects_department_id_idx on the subjects.departmentId column (e.g., index("subjects_department_id_idx").on(subjects.departmentId)) and then regenerate the migration so the new migration includes the CREATE INDEX for subjects.department_id; ensure the index name matches subjects_department_id_idx and the table/column symbols used are subjects and departmentId.drizzle/0001_chubby_lady_bullseye.sql (1)
1-2: Consider squashing 0000 + 0001 before merging.This migration only exists to convert
descriptionfromvarchar(255)→texton tables that were created in the immediately preceding migration. Since this is the initial schema and nothing has been deployed, it's cleaner to delete both 0000/0001 SQL files (and their snapshots/journal entries) and regenerate a single initial migration that declaresdescriptionastextfrom the start. Keeps history minimal and avoids a column rewrite on first deploy.Not blocking — safe to keep as-is if you prefer preserving the authoring history.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@drizzle/0001_chubby_lady_bullseye.sql` around lines 1 - 2, The two migrations (0000 and 0001) currently create tables with description as varchar(255) then alter them to text (see ALTER TABLE "departments" ALTER COLUMN "description" SET DATA TYPE text and ALTER TABLE "subjects" ALTER COLUMN "description" SET DATA TYPE text); remove/squash these two files and their snapshots/journal entries and regenerate a single initial migration that defines the description columns as text for the departments and subjects tables to avoid an unnecessary column rewrite on first deploy.package.json (1)
10-27: Scripts and dependencies look correct.
db:generate/db:migratealign with the addeddrizzle.config.ts, and the runtime (@neondatabase/serverless,drizzle-orm,dotenv) and dev (drizzle-kit) dependency split is appropriate.One optional consideration:
@types/nodemajor version (25) should track the Node.js runtime major you deploy on. If you're running on an LTS (e.g. 22/24), pin@types/nodeto that major instead to avoid typing drift against unavailable runtime APIs.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` around lines 10 - 27, The `@types/node` devDependency is pinned to major 25 which may not match your deployed Node.js LTS; update the "@types/node" entry in package.json (under devDependencies) to the major that matches your runtime (e.g., ^24.0.0 or ^22.0.0) and re-run your package manager (npm/yarn) to install the matching types so TypeScript typings align with the deployed Node.js APIs.src/db/schema/app.ts (2)
40-45: Unusedmanydestructure.
subjectsRelationsdestructuresmanybut never uses it.-export const subjectsRelations = relations(subjects, ({ one, many }) => ({ +export const subjectsRelations = relations(subjects, ({ one }) => ({ department: one(departments, { fields: [subjects.departmentId], //foreign key field(s) in subject table references: [departments.id], //primary key field(s) in department table }), }));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/db/schema/app.ts` around lines 40 - 45, The relations call for subjectsRelations destructures both one and many but never uses many; update the lambda to only destructure the used helper (one) — e.g., change relations(subjects, ({ one, many }) => ({ ... })) to relations(subjects, ({ one }) => ({ department: one(departments, { fields: [subjects.departmentId], references: [departments.id] }) })) so the unused many identifier is removed.
21-23: Consider indexingsubjects.department_id.Foreign key columns are not automatically indexed in Postgres. Queries like "list all subjects for a department" and cascading integrity checks on department updates/deletes will benefit from an explicit index on
department_id, especially as the table grows.♻️ Suggested index
-import { integer, pgTable, text, timestamp, varchar } from "drizzle-orm/pg-core"; +import { index, integer, pgTable, text, timestamp, varchar } from "drizzle-orm/pg-core"; @@ -export const subjects = pgTable("subjects", { - id: integer("id").primaryKey().generatedAlwaysAsIdentity(), - - departmentId: integer("department_id") - .notNull() - .references(() => departments.id, { onDelete: "restrict" }), - - name: varchar("name", { length: 255 }).notNull(), - code: varchar("code", { length: 50 }).notNull().unique(), - description: text("description"), - - ...timestamps, -}); +export const subjects = pgTable( + "subjects", + { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + departmentId: integer("department_id") + .notNull() + .references(() => departments.id, { onDelete: "restrict" }), + name: varchar("name", { length: 255 }).notNull(), + code: varchar("code", { length: 50 }).notNull().unique(), + description: text("description"), + ...timestamps, + }, + (t) => ({ + subjectsDepartmentIdIdx: index("subjects_department_id_idx").on(t.departmentId), + }), +);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/db/schema/app.ts` around lines 21 - 23, Add an explicit index on subjects.department_id: locate the subjects table schema where departmentId (department_id) is defined and add an index on that column (e.g., using your schema builder's index API or by adding a migration that creates subjects_department_id_idx on department_id) so queries like "list all subjects for a department" and FK cascade checks are performant as the table grows.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/db/index.ts`:
- Around line 1-10: Current code uses drizzle-orm/neon-http which does not
support transactions; replace that setup by importing neon from
'drizzle-orm/neon-serverless' and creating a WebSocket-enabled Pool from
'@neondatabase/serverless' instead of neon(process.env.DATABASE_URL);
instantiate new Pool({ connectionString: process.env.DATABASE_URL }), pass that
Pool into neon(...) to create the sql client and then call drizzle(sql) to
export db (referencing the existing symbols: db, sql, neon, drizzle, Pool); keep
the DATABASE_URL check and ensure the Pool is created only when the env var
exists.
In `@src/db/schema/app.ts`:
- Line 26: The subjects table currently marks the code column as unique (code:
varchar("code", { length: 50 }).notNull().unique()), which enforces global
uniqueness; if you intend codes to be unique only within a department, remove
the column-level unique() from the "code" column and add a composite unique
constraint on (department_id, code) at the table level (i.e., define a unique
index/constraint referencing department_id and code in the subjects schema
instead of per-column unique).
- Around line 4-7: The timestamps object defines createdAt and updatedAt using
timestamp(...).defaultNow(), but defaultNow() only sets the value on INSERT so
updatedAt won't change on UPDATE; modify the timestamps.updatedAt property (in
the timestamps const) to use Drizzle's $onUpdate hook to set the value to the
current timestamp on every update (e.g., call the DB current timestamp via the
appropriate SQL expression) so updates done through Drizzle refresh updatedAt;
if your app performs raw SQL updates outside Drizzle, instead add a Postgres
trigger to set updated_at on each UPDATE to ensure consistency.
---
Nitpick comments:
In `@drizzle/0000_cynical_ultimates.sql`:
- Line 2: The column definition uses "GENERATED ALWAYS AS IDENTITY" for the "id"
column (sequence "departments_id_seq") which forbids explicit id inserts; change
it to "GENERATED BY DEFAULT AS IDENTITY" for both the departments "id" and the
subjects "id" definitions so the DB still auto-generates ids when omitted but
allows explicit ids for seed scripts and imports, updating the identity clause
for those fields accordingly.
- Around line 11-22: Add a b-tree index on the foreign-key column to avoid
sequential scans: in your schema file (src/db/schema/app.ts) add an index named
subjects_department_id_idx on the subjects.departmentId column (e.g.,
index("subjects_department_id_idx").on(subjects.departmentId)) and then
regenerate the migration so the new migration includes the CREATE INDEX for
subjects.department_id; ensure the index name matches subjects_department_id_idx
and the table/column symbols used are subjects and departmentId.
In `@drizzle/0001_chubby_lady_bullseye.sql`:
- Around line 1-2: The two migrations (0000 and 0001) currently create tables
with description as varchar(255) then alter them to text (see ALTER TABLE
"departments" ALTER COLUMN "description" SET DATA TYPE text and ALTER TABLE
"subjects" ALTER COLUMN "description" SET DATA TYPE text); remove/squash these
two files and their snapshots/journal entries and regenerate a single initial
migration that defines the description columns as text for the departments and
subjects tables to avoid an unnecessary column rewrite on first deploy.
In `@package.json`:
- Around line 10-27: The `@types/node` devDependency is pinned to major 25 which
may not match your deployed Node.js LTS; update the "@types/node" entry in
package.json (under devDependencies) to the major that matches your runtime
(e.g., ^24.0.0 or ^22.0.0) and re-run your package manager (npm/yarn) to install
the matching types so TypeScript typings align with the deployed Node.js APIs.
In `@src/db/schema/app.ts`:
- Around line 40-45: The relations call for subjectsRelations destructures both
one and many but never uses many; update the lambda to only destructure the used
helper (one) — e.g., change relations(subjects, ({ one, many }) => ({ ... })) to
relations(subjects, ({ one }) => ({ department: one(departments, { fields:
[subjects.departmentId], references: [departments.id] }) })) so the unused many
identifier is removed.
- Around line 21-23: Add an explicit index on subjects.department_id: locate the
subjects table schema where departmentId (department_id) is defined and add an
index on that column (e.g., using your schema builder's index API or by adding a
migration that creates subjects_department_id_idx on department_id) so queries
like "list all subjects for a department" and FK cascade checks are performant
as the table grows.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 30fbb007-6107-48ad-a161-ff69aa7eeac0
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (10)
drizzle.config.tsdrizzle/0000_cynical_ultimates.sqldrizzle/0001_chubby_lady_bullseye.sqldrizzle/meta/0000_snapshot.jsondrizzle/meta/0001_snapshot.jsondrizzle/meta/_journal.jsonpackage.jsonsrc/db/index.tssrc/db/schema/app.tssrc/db/schema/index.ts
| import 'dotenv/config'; | ||
| import { drizzle } from 'drizzle-orm/neon-http'; | ||
| import { neon } from '@neondatabase/serverless'; | ||
|
|
||
| if (!process.env.DATABASE_URL) { | ||
| throw new Error('DATABASE_URL is not defined'); | ||
| } | ||
|
|
||
| const sql = neon(process.env.DATABASE_URL); | ||
| export const db = drizzle(sql); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
drizzle-orm neon-http vs neon-serverless transaction support
💡 Result:
Drizzle ORM's neon-http driver does not support interactive transactions (db.transaction), throwing "No transactions support in neon-http driver". It is optimized for single, non-interactive queries over HTTP, faster for simple operations but lacks full session/transaction features. The neon-serverless driver (WebSocket-based) supports full interactive transactions via Pool or Client from @neondatabase/serverless, providing session support and pg compatibility. Use it for transactions: import { drizzle } from 'drizzle-orm/neon-serverless'; with a Pool. Official Drizzle docs recommend neon-http for speed in non-interactive cases, neon-serverless for interactive transactions. Neon's driver docs confirm HTTP for non-interactive tx (limited, all-or-nothing), WebSockets/Pool for full support. Errors and fixes in SO/posts confirm switching to neon-serverless resolves transaction issues. No evidence of neon-http gaining full tx support as of 2026.
Citations:
- 1: https://www.answeroverflow.com/m/1149370348593217619
- 2: [BUG]: Neon transactions not working in Node v20 drizzle-team/drizzle-orm#2200
- 3: https://orm.drizzle.team/docs/connect-neon
- 4: https://stackoverflow.com/questions/79215025/how-to-handle-transaction-with-nextjs-14-and-drizzle-orm
- 5: Error: No transactions support in neon-http driver - unable_to_create_user better-auth/better-auth#4747
- 6: https://neon.com/docs/serverless/serverless-driver
- 7: https://neon.tech/docs/serverless/serverless-driver
neon-http adapter does not support interactive transactions.
The drizzle-orm/neon-http driver uses Neon's HTTP endpoint, which only supports single-statement queries and db.batch([...]). Calling db.transaction(async (tx) => { ... }) will throw "No transactions support in neon-http driver" at runtime. If you anticipate multi-statement atomic writes (likely for department/subject mutations and future tables), switch to drizzle-orm/neon-serverless with the WebSocket Pool from @neondatabase/serverless, which supports full transactions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/db/index.ts` around lines 1 - 10, Current code uses drizzle-orm/neon-http
which does not support transactions; replace that setup by importing neon from
'drizzle-orm/neon-serverless' and creating a WebSocket-enabled Pool from
'@neondatabase/serverless' instead of neon(process.env.DATABASE_URL);
instantiate new Pool({ connectionString: process.env.DATABASE_URL }), pass that
Pool into neon(...) to create the sql client and then call drizzle(sql) to
export db (referencing the existing symbols: db, sql, neon, drizzle, Pool); keep
the DATABASE_URL check and ensure the Pool is created only when the env var
exists.
| const timestamps = { | ||
| createdAt: timestamp("created_at").defaultNow().notNull(), | ||
| updatedAt: timestamp("updated_at").defaultNow().notNull(), | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
drizzle-orm timestamp $onUpdate auto update column
💡 Result:
In Drizzle ORM, use the $onUpdate or $onUpdateFn method on timestamp columns to automatically set a value (like current timestamp) during UPDATE queries at runtime. This does not affect schema generation/migrations (drizzle-kit ignores it); it's handled by the ORM during query execution. Example for PostgreSQL/MySQL/SQLite (works across dialects since v0.30.5): import { timestamp, pgTable } from 'drizzle-orm/pg-core'; // or mysql-core/sqlite-core export const users = pgTable('users', { id: serial('id').primaryKey, updatedAt: timestamp('updated_at', { mode: 'date' }).defaultNow.$onUpdate( => new Date), }); - The callback runs on UPDATE if no value provided for the column. - If no .default or $defaultFn, it also runs on INSERT. - For DB server time (not app server): Use mode: 'string' and sqlnow: timestamp('updated_at', { mode: 'string' }).$onUpdate( => sqlnow). - MySQL-specific schema-level: .onUpdateCurrentTimestamp or .default(sqlCURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) for DB-handled updates (drizzle-kit supported). When updating, omit the column from .set: await db.update(users).set({ name: 'New' }).where(eq(users.id, 1)); The ORM injects the $onUpdate value automatically. Note: Official docs lack detail (mostly GitHub release/PRs/issues). Use latest version (beta as of 2026 has fixes for sql in $onUpdate).
Citations:
- 1: Feat: add an
$onUpdatemethod to columns drizzle-team/drizzle-orm#1509 - 2: [FEATURE]: Prisma's
updatedAtbehavior.onUpdateAPI suggestion drizzle-team/drizzle-orm#956 - 3: [FEATURE]: Prisma's
updatedAtbehavior.onUpdateAPI suggestion drizzle-team/drizzle-orm#956 - 4: https://orm.drizzle.team/docs/update
- 5: [BUG]: $onUpdate/$onUpdateFn doesn't work with sql`` drizzle-team/drizzle-orm#2212
- 6: Fix $onUpdate not handling sql values drizzle-team/drizzle-orm#2911
- 7: https://www.northwestautoparts.com
- 8: https://www.quilql.com
- 9: https://change8.dev/package/drizzle-orm
- 10: https://github.com/drizzle-team/drizzle-orm/releases/tag/0.30.5
🏁 Script executed:
# Check if Drizzle ORM is used in the project
rg "drizzle" package.json -A 5 -B 5Repository: AzrilFitrayana/classroom-backend
Length of output: 743
🏁 Script executed:
# Look for the specific file mentioned in the review
cat -n src/db/schema/app.tsRepository: AzrilFitrayana/classroom-backend
Length of output: 2661
🏁 Script executed:
# Search for any existing $onUpdate usage in the codebase to verify syntax
rg "\$onUpdate" --type ts --type js -B 2 -A 2Repository: AzrilFitrayana/classroom-backend
Length of output: 58
updatedAt will not auto-update on row modification.
defaultNow() only sets the value on INSERT. Subsequent UPDATEs will leave updated_at unchanged, so the column name becomes misleading. Use Drizzle's $onUpdate (application-side) or a Postgres trigger (DB-side) to refresh it on every update.
♻️ Suggested app-side fix
const timestamps = {
createdAt: timestamp("created_at").defaultNow().notNull(),
- updatedAt: timestamp("updated_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at")
+ .defaultNow()
+ .$onUpdate(() => new Date())
+ .notNull(),
};Note: $onUpdate only fires for updates performed through Drizzle; if you expect raw SQL updates from elsewhere, add a Postgres trigger instead.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/db/schema/app.ts` around lines 4 - 7, The timestamps object defines
createdAt and updatedAt using timestamp(...).defaultNow(), but defaultNow() only
sets the value on INSERT so updatedAt won't change on UPDATE; modify the
timestamps.updatedAt property (in the timestamps const) to use Drizzle's
$onUpdate hook to set the value to the current timestamp on every update (e.g.,
call the DB current timestamp via the appropriate SQL expression) so updates
done through Drizzle refresh updatedAt; if your app performs raw SQL updates
outside Drizzle, instead add a Postgres trigger to set updated_at on each UPDATE
to ensure consistency.
| .references(() => departments.id, { onDelete: "restrict" }), | ||
|
|
||
| name: varchar("name", { length: 255 }).notNull(), | ||
| code: varchar("code", { length: 50 }).notNull().unique(), |
There was a problem hiding this comment.
Confirm subjects.code should be globally unique.
code is declared unique() at the table level, so two departments cannot share the same subject code (e.g., both CS and MATH defining "101"). If codes are intended to be unique per department, replace the column-level unique with a composite unique on (department_id, code).
♻️ Alternative if per-department uniqueness is desired
-import { integer, pgTable, text, timestamp, varchar } from "drizzle-orm/pg-core";
+import { integer, pgTable, text, timestamp, unique, varchar } from "drizzle-orm/pg-core";
@@
-export const subjects = pgTable("subjects", {
- id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
-
- departmentId: integer("department_id")
- .notNull()
- .references(() => departments.id, { onDelete: "restrict" }),
-
- name: varchar("name", { length: 255 }).notNull(),
- code: varchar("code", { length: 50 }).notNull().unique(),
- description: text("description"),
-
- ...timestamps,
-});
+export const subjects = pgTable(
+ "subjects",
+ {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+ departmentId: integer("department_id")
+ .notNull()
+ .references(() => departments.id, { onDelete: "restrict" }),
+ name: varchar("name", { length: 255 }).notNull(),
+ code: varchar("code", { length: 50 }).notNull(),
+ description: text("description"),
+ ...timestamps,
+ },
+ (t) => ({
+ subjectsDeptCodeUnique: unique("subjects_department_id_code_unique").on(
+ t.departmentId,
+ t.code,
+ ),
+ }),
+);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/db/schema/app.ts` at line 26, The subjects table currently marks the code
column as unique (code: varchar("code", { length: 50 }).notNull().unique()),
which enforces global uniqueness; if you intend codes to be unique only within a
department, remove the column-level unique() from the "code" column and add a
composite unique constraint on (department_id, code) at the table level (i.e.,
define a unique index/constraint referencing department_id and code in the
subjects schema instead of per-column unique).
Summary by CodeRabbit
Release Notes
New Features
Chores
db:generateanddb:migratenpm scripts.