Changing `id` type from serial to uuid, causing error.

I'm sorry, I'm new to Drizzle, but I've read through the documentation and discord and cannot find a solution to my problem. I'm using Next.js 14, Postgresql through Vercel. When I first started, I built a test UsersTable but now I want to change the id from a serial to a uuid. Everytime I try to generate:pg it works, but push:pg fails, giving the following error:
error: column "id" cannot be cast automatically to type uuid
code: '42804',
hint: 'You might need to specify "USING id::uuid".',
error: column "id" cannot be cast automatically to type uuid
code: '42804',
hint: 'You might need to specify "USING id::uuid".',
Could you please tell me how I'm supposed to update my table?
import { pgTable, uuid, varchar, timestamp } from "drizzle-orm/pg-core"
import { sql } from "drizzle-orm"

export const UsersTable = pgTable("users", {
id: uuid("id").defaultRandom(),
firstName: varchar("first_name", { length: 50 }).notNull(),
lastName: varchar("last_name", { length: 50 }).notNull(),
email: varchar("email", { length: 100 }).notNull().unique(),
password: varchar("password", { length: 100 }).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull()
})
import { pgTable, uuid, varchar, timestamp } from "drizzle-orm/pg-core"
import { sql } from "drizzle-orm"

export const UsersTable = pgTable("users", {
id: uuid("id").defaultRandom(),
firstName: varchar("first_name", { length: 50 }).notNull(),
lastName: varchar("last_name", { length: 50 }).notNull(),
email: varchar("email", { length: 100 }).notNull().unique(),
password: varchar("password", { length: 100 }).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull()
})
54 Replies
Cold River
Cold River•8mo ago
it's not well documented in Drizzle yet, but I think you're expected to modify the migrations if there's no way to do them automatically. In this case, a serial is an int, which cannot be automatically converted to UUID. The error you're getting seesm to be trying to convert "id", which was a serial/int, to a type uuid. You'll need to specify how to convert old values: https://stackoverflow.com/a/7162961 ALTER TABLE tbl_name ALTER COLUMN id TYPE uuid USING some_expression;
Stack Overflow
How to alter a column's data type in a PostgreSQL table?
Entering the following command into a PostgreSQL interactive terminal results in an error: ALTER TABLE tbl_name ALTER COLUMN col_name varchar (11); What is the correct command to alter the data t...
Cold River
Cold River•8mo ago
i don't know how foreign keys will work though
samson
samson•8mo ago
serial is a different type than uuid - as the error message suggests you could cast it. Like @ColdRiver suggests you could just change the migration assuming you're not in production yet? I would probably just reset the db schema if you're still in dev.
David L. Bowman
David L. BowmanOP•8mo ago
How am I supposed to push the change? I was trying bunx drizzle-kit push:pg but this didn't work. I was able to just drop the table directly from Vercel and push a new migration, but at some point, it's not going to be easy. So, how do I use the SQL? I can write the sql code, where am I supposed to put it?
David L. Bowman
David L. BowmanOP•8mo ago
No description
David L. Bowman
David L. BowmanOP•8mo ago
if i put it in here, i was just getting "No changes made" or something like this when I ran bunx drizzle-kit push:pg.
David L. Bowman
David L. BowmanOP•8mo ago
No description
David L. Bowman
David L. BowmanOP•8mo ago
here's what I'd get if I edited the sql file.
samson
samson•8mo ago
I would drop all migrations, and start from scratch drizzle-kit drop (multiple times if you have multiple migration files) then drizzle-kit generate:pg to get a fresh migration file lastly you want push that change up to your database, ie run all migration files-- that kind of depends on what service you're using to host your DB.
David L. Bowman
David L. BowmanOP•8mo ago
I'm using Vercel/Postgresql, so it's bunx drizzle-kit push:pg but, when I drop migrations, it only drops it from my repository, not from the actual db itself. the issue is if I drop the migration, then generate:pg / push:pg that's where I encounter the error of mismatching types. I'm wondering where I'm supposed to put the ALTER TABLE "users" ALTER COLUMN "id" uuid code. I was able to do it directly on vercel by "querying" that 😄 but, I was wondering where I can run SQL on drizzle. or, maybe that's not the scope of drizzle and my issue is w/ vercel.
Cold River
Cold River•8mo ago
as samson said, there's two options: 1. reset the whole migrations and lose all your data 2. edit the migration to work and then run it If you're doing 1, that's what dropping the migrations and then generating the new one, and then pushing the change would look like. If you're doing 2, that's when you would edit the migration file and put the ALTER TABLE ... USING ... statement. You'd put it in the last migration file, replacing the ALTER TABLE statement in there. It would have the largest number as a preifx.
David L. Bowman
David L. BowmanOP•8mo ago
got it, so after I generate:pg I should edit the .sql file before using push:pg? by the way, i appreciate the help a lot.
Cold River
Cold River•8mo ago
im not sure about push:pg because im using the beta which includes a migrate command. but yes, you edit the .sql file immediately after generate:pg
David L. Bowman
David L. BowmanOP•8mo ago
migth be this one
David L. Bowman
David L. BowmanOP•8mo ago
Drizzle ORM - List of commands
Drizzle ORM is a lightweight and performant TypeScript ORM with developer experience in mind.
David L. Bowman
David L. BowmanOP•8mo ago
bunx drizzle-kit up:pg
samson
samson•8mo ago
to give you some context, what happens under the hood is that Drizzle executes all your migration files one by one, from start to finish. You probably just have one file right now. And if you did what I suggested (drop all migrations, then generate one fresh one), then there's no need to add any SQL to it, since the file will have the correct type for your id. For your change to be reflected in your production database, you need to probably tell Vercel to re-run your migration files -- I'm not familiar with how to trigger this on Vercel's end, it might be that you just connect your Github account or whatever and it detects any changes.
David L. Bowman
David L. BowmanOP•8mo ago
ALTER TABLE "users" ALTER COLUMN "id" SET DATA TYPE uuid;

error: column "id" cannot be cast automatically to type uuid
at /home/dlb/c0de/reminder-app/node_modules/drizzle-kit/bin.cjs:43518:21
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async PgPostgres.query (/home/dlb/c0de/reminder-app/node_modules/drizzle-kit/bin.cjs:62584:21)
at async Command.<anonymous> (/home/dlb/c0de/reminder-app/node_modules/drizzle-kit/bin.cjs:66267:9) {
length: 169,
severity: 'ERROR',
code: '42804',
detail: undefined,
hint: 'You might need to specify "USING id::uuid".',
position: undefined,
internalPosition: undefined,
internalQuery: undefined,
where: undefined,
schema: undefined,
table: undefined,
column: undefined,
dataType: undefined,
constraint: undefined,
file: 'tablecmds.c',
line: '12336',
routine: 'ATPrepAlterColumnType'
}
ALTER TABLE "users" ALTER COLUMN "id" SET DATA TYPE uuid;

error: column "id" cannot be cast automatically to type uuid
at /home/dlb/c0de/reminder-app/node_modules/drizzle-kit/bin.cjs:43518:21
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async PgPostgres.query (/home/dlb/c0de/reminder-app/node_modules/drizzle-kit/bin.cjs:62584:21)
at async Command.<anonymous> (/home/dlb/c0de/reminder-app/node_modules/drizzle-kit/bin.cjs:66267:9) {
length: 169,
severity: 'ERROR',
code: '42804',
detail: undefined,
hint: 'You might need to specify "USING id::uuid".',
position: undefined,
internalPosition: undefined,
internalQuery: undefined,
where: undefined,
schema: undefined,
table: undefined,
column: undefined,
dataType: undefined,
constraint: undefined,
file: 'tablecmds.c',
line: '12336',
routine: 'ATPrepAlterColumnType'
}
the issue is I was getting this error when trying to push:pg the fresh and only migration.
Cold River
Cold River•8mo ago
huh that's weird
samson
samson•8mo ago
I think ALTERING ids is not a good idea in general
Cold River
Cold River•8mo ago
i wouldnt expect any ALTER statements in the first migration file
David L. Bowman
David L. BowmanOP•8mo ago
The issue is i'm somewhat new to dbs 😄 so I'm going to make mistakes. This is for a proejct to learn on.
samson
samson•8mo ago
all good- as ColdRiver said, check your migration file
David L. Bowman
David L. BowmanOP•8mo ago
Is the real strategy, get it right the first time?
samson
samson•8mo ago
Does it have "ALTER..." in it?
David L. Bowman
David L. BowmanOP•8mo ago
hmmm , let me recreate the problem, 1s. no i droped the table on vercel, and started over. but, i was wondering if i had the issue how to fix 😄
samson
samson•8mo ago
Changing the type of an ID is kind of a complicated thing -- you might reference that same ID in other tables and be using the wrong type when you reference it. That may cause an error. So I don't think this is something you need to know "how to fix"
David L. Bowman
David L. BowmanOP•8mo ago
got it, so what should i do in cases like this. let's say i set the wrong type, do i just create a new type and copy over the data and ignroe the old type?
samson
samson•8mo ago
depends on what the old vs new type is. Imagine you change from string to number -- that won't work I generally 1. create a new column (myColumn_NEW) 2. copy over all the data (may need to cast/transform the data) 3. then drop the old column 4.then rename the new column. 1, 3, 4 are each migrations
David L. Bowman
David L. BowmanOP•8mo ago
hmmm, this is going to require a new and improved way of thinking. i'll have to be more careful. ahh, very good. okay, this makes sense.
Cold River
Cold River•8mo ago
@samson , why not use the USING clause in Postgres? to make 1-step instead of 4
samson
samson•8mo ago
yeah totally
David L. Bowman
David L. BowmanOP•8mo ago
i'm learnign i need to relearn postgres
samson
samson•8mo ago
just trying to lay out the logical steps
David L. Bowman
David L. BowmanOP•8mo ago
i appreciate it, thank you for explaining the thoughts. i also now better understand the dangers of not doing this.
Cold River
Cold River•8mo ago
ah right. i think what you said is needed in other databases anyway for reference, to change the type of an ID when using PostgreSQL i think you should be able to: 0. get a lock on the target table and all tables that reference it 1. disable foreign key checks 2. alter the column, with a USING clause if needed 3. update all tables that have a foreign key relationship that references that ID column to use the new ID values 4. re-enable foreign key checks 5. release lock on the tables in other words, try not to do it. just delete all the data and start over, if you're still in development.
David L. Bowman
David L. BowmanOP•8mo ago
haha kk 😄
Cold River
Cold River•8mo ago
and if you do need to do this in production, make sure to do it in a staging environment first because I and you probably missed something in that playbook 😛
David L. Bowman
David L. BowmanOP•8mo ago
well, how's my schema look, did i make any terrible mistakes?
import { sql } from "drizzle-orm"
import {
check,
index,
pgTable,
smallint,
timestamp,
uniqueIndex,
uuid,
varchar
} from "drizzle-orm/pg-core"

export const UsersTable = pgTable(
"users",
{
uuid: uuid("uuid").defaultRandom().primaryKey(),
firstName: varchar("first_name", { length: 50 }).notNull(),
lastName: varchar("last_name", { length: 50 }).notNull(),
email: varchar("email", { length: 100 }).notNull().unique(),
password: varchar("password", { length: 100 }).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull()
},
(table) => {
return {
emailIdx: uniqueIndex("email_idx").on(table.email)
}
}
)

export const RemindersTable = pgTable(
"reminders",
{
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id")
.references(() => UsersTable.uuid)
.notNull(),
message: varchar("message", { length: 500 }).notNull(),
reminderInterval: smallint("reminder_interval").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull()
},
(reminders) => ({
userIdIndex: index("user_id_index").on(reminders.userId),
userReminderCountConstraint: check(
"user_reminder_count_constraint",
sql`(SELECT COUNT(*) FROM reminders WHERE user_id = ${reminders.userId}) <= 5`
)
})
)
import { sql } from "drizzle-orm"
import {
check,
index,
pgTable,
smallint,
timestamp,
uniqueIndex,
uuid,
varchar
} from "drizzle-orm/pg-core"

export const UsersTable = pgTable(
"users",
{
uuid: uuid("uuid").defaultRandom().primaryKey(),
firstName: varchar("first_name", { length: 50 }).notNull(),
lastName: varchar("last_name", { length: 50 }).notNull(),
email: varchar("email", { length: 100 }).notNull().unique(),
password: varchar("password", { length: 100 }).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull()
},
(table) => {
return {
emailIdx: uniqueIndex("email_idx").on(table.email)
}
}
)

export const RemindersTable = pgTable(
"reminders",
{
id: uuid("id").defaultRandom().primaryKey(),
userId: uuid("user_id")
.references(() => UsersTable.uuid)
.notNull(),
message: varchar("message", { length: 500 }).notNull(),
reminderInterval: smallint("reminder_interval").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull()
},
(reminders) => ({
userIdIndex: index("user_id_index").on(reminders.userId),
userReminderCountConstraint: check(
"user_reminder_count_constraint",
sql`(SELECT COUNT(*) FROM reminders WHERE user_id = ${reminders.userId}) <= 5`
)
})
)
Cold River
Cold River•8mo ago
you probably want to use $updateFn instead of (or in addition to) default for updatedAt
David L. Bowman
David L. BowmanOP•8mo ago
updatedAt: timestamp("updated_at")
.$onUpdateFn(() => sql`current_timestamp`)
.notNull()
updatedAt: timestamp("updated_at")
.$onUpdateFn(() => sql`current_timestamp`)
.notNull()
Cold River
Cold River•8mo ago
i think the check is invalid, checks can only take into account the row being inserted or updated. From the documentation (https://www.postgresql.org/docs/current/ddl-constraints.html): Note PostgreSQL does not support CHECK constraints that reference table data other than the new or updated row being checked. While a CHECK constraint that violates this rule may appear to work in simple tests, it cannot guarantee that the database will not reach a state in which the constraint condition is false (due to subsequent changes of the other row(s) involved). This would cause a database dump and restore to fail. The restore could fail even when the complete database state is consistent with the constraint, due to rows not being loaded in an order that will satisfy the constraint. If possible, use UNIQUE, EXCLUDE, or FOREIGN KEY constraints to express cross-row and cross-table restrictions. If what you desire is a one-time check against other rows at row insertion, rather than a continuously-maintained consistency guarantee, a custom trigger can be used to implement that. (This approach avoids the dump/restore problem because pg_dump does not reinstall triggers until after restoring data, so that the check will not be enforced during a dump/restore.)
David L. Bowman
David L. BowmanOP•8mo ago
got it, so should I just have a counter for the user which counts up, and rejects when a sixth comes?
Cold River
Cold River•8mo ago
and it's fine for learning if you're not doing this already, but you'll want to make sure you don't store passwords in plain text, but rather a cryptographic hash
David L. Bowman
David L. BowmanOP•8mo ago
I was going to try this 😄
Cold River
Cold River•8mo ago
ok great good
David L. Bowman
David L. BowmanOP•8mo ago
it's just a stupid terminal app which plays a sound at an interval to remind you to do something
David L. Bowman
David L. BowmanOP•8mo ago
so far, the only commands are "help" "hi" and "bye" 😄 i was going to add the features after i figured out the orm/db part, which was harder than i thought. i bought a domain b/c i figured i'd share w/ others 😄 i built a CLI tool for myself, which I use to do this 😄 i thought it would be good practice to turn it into an app.
Cold River
Cold River•8mo ago
you would normally do something like this: 1. start transaction 2. check how many reminders. if more than allowed, abort transaction 3. insert reminder 4. commit transaction
David L. Bowman
David L. BowmanOP•8mo ago
No description
David L. Bowman
David L. BowmanOP•8mo ago
ahhh, i got you. easy enough then. thhank you for your help @samson and @ColdRiver you both taught me a lot today.
Cold River
Cold River•8mo ago
you're welcome! good luck in your project!
Want results from more Discord servers?
Add your server