Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE]: Prisma's updatedAt behavior. onUpdate API suggestion #956

Closed
danulqua opened this issue Jul 31, 2023 · 42 comments · Fixed by #1509
Closed

[FEATURE]: Prisma's updatedAt behavior. onUpdate API suggestion #956

danulqua opened this issue Jul 31, 2023 · 42 comments · Fixed by #1509
Labels
enhancement New feature or request

Comments

@danulqua
Copy link

Describe what you want

Prisma has awesome feature such as declaring a column with @updatedAt which updates that column with a new timestamp every time the record updates.

Instead of suggesting to add something similar like just updatedAt alternative, what if you provide a more flexible onUpdate method when declaring columns in schema so we can do something like this:

const table = pgTable('table', {
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').defaultNow().onUpdate(() => Date.now()),
});

The idea is that this method accepts a callback function that returns a new value for the column. The callback function is invoked every time the record updates. The basic example is to use it for timestamps on updatedAt columns, but it can be used for anything since the new value will be the one returned by the callback.

In general, when the record updates, we can find out which columns were updated, old values and new values. Maybe, this data can be accepted by onUpdate callback like

onUpdate((columns, oldValues, newValues) => ...)

Or any other better API for this.

@danulqua danulqua added the enhancement New feature or request label Jul 31, 2023
@zivtamary
Copy link

What is the current approach to implementing updatedAt?

@danulqua
Copy link
Author

@zivtamary either manually when updating records by providing new value for updatedAt column, or by creating a custom SQL migration and defining a trigger.

@MatanYadaev
Copy link

Related: #843

@laurent512
Copy link

Would really be interested by this feature !

@Pentiado
Copy link

Pentiado commented Aug 12, 2023

You can ask the database to update the timestamp for you

const table = pgTable('table', {
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').default(sql`CURRENT_TIMESTAMP(3) on update CURRENT_TIMESTAMP(3)`),
});

Or, you can do it in your application code

// some db/utils.ts file with methods like updateUser, deleteUser, etc. 
export const updateUser = (data: Partial<InferModel<typeof users, 'insert'>>) => db.update(users).set({
  updatedAt: new Date(),
  ...data,
});
// api/user.ts
const yourApiOrStuffHandler = async (req, res) => {
  await updateUser({ firstName: req.query.firstName }).where(eq(users.id, req.query.id));
  
  res.send('ok!!');
};

Right now Drizzle is a lightweight ORM with no learning curve. You read through the docs in no time. In Prisma docs, you can get lost for days, and SQL knowledge won't help you much. I hope Drizzle will stay like this and will leave application code for developers to implement in their apps.

@Amar-Gill
Copy link

I would love to use the above approach using the sql statement but it does not appear to be possible for sqlite.

For me, the benefit of defining triggers in theschema.ts file is to keep my local db synchronized with generated migrations when I run drizzle-kit push:sqlite for local dev.

@Gerschtli
Copy link

@Pentiado That does not work in postgres either, looks like only mysql knows this on update notation, see also https://stackoverflow.com/questions/1035980/update-timestamp-when-row-is-updated-in-postgresql

@Angelelz
Copy link
Collaborator

Angelelz commented Sep 6, 2023

This can now be achieved with the new .$default() / .$defaultFn() functions introduced in v0.28.3.
Can this issue be closed?

@danulqua
Copy link
Author

danulqua commented Sep 6, 2023

@Angelelz How it's related to the ON UPDATE? Or could you provide an example using .$default() / .defaultFn()?

@Angelelz
Copy link
Collaborator

Angelelz commented Sep 6, 2023

Nope, I was mistaken. Based on the shape of the API in the initial feature request, it looked a lot like $default(). But it would only run on inserts, not on updates.

@danulqua
Copy link
Author

danulqua commented Sep 6, 2023

@Angelelz Exactly.

@statusunknown418
Copy link

any updates on this? I saw a PR pending merge

@MunifTanjim
Copy link

MunifTanjim commented Sep 23, 2023

For PostgreSQL, until this is implemented, this is what I'm using for the time being:

create or replace function on_row_update_set_updated_at() returns trigger
    language plpgsql as
$$
begin
    NEW."updated_at" = now();
    return NEW;
end;
$$;

create or replace function on_create_table_add_trigger_on_row_update_set_updated_at() returns event_trigger
    language plpgsql as
$$
declare
    obj      record;
    tbl_name text;
begin
    for obj in select * from pg_event_trigger_ddl_commands() where object_type = 'table'
        loop
            tbl_name := obj.objid::regclass;
            if exists(select 1
                      from information_schema.columns
                      where table_schema = obj.schema_name
                        and table_name = tbl_name
                        and column_name = 'updated_at') then
                execute format(
                        'create or replace trigger on_row_update_set_updated_at before update on %I for each row execute procedure on_row_update_set_updated_at();',
                        tbl_name);
            end if;
        end loop;

end
$$;

create event trigger
    on_create_table_add_trigger_on_row_update_set_updated_at on ddl_command_end
    when tag in ('CREATE TABLE', 'CREATE TABLE AS')
execute procedure on_create_table_add_trigger_on_row_update_set_updated_at();

Create your tables with

"updated_at" timestamp with time zone DEFAULT now() NOT NULL

column, and it'll be updated automatically whenever any row changes.

@Angelelz
Copy link
Collaborator

Angelelz commented Sep 23, 2023

@AlvaroAquijeDiaz

any updates on this? I saw a PR pending merge

What is that PR? I did a quick search and couldn't find it. I might open one if no one else has one open.

@statusunknown418
Copy link

@Angelelz here #1114 , I think it's actually yours lol

@Angelelz
Copy link
Collaborator

@Angelelz here #1114 , I think it's actually yours lol

That PR is for MySQL, postgres doesn't have an on update clause.

@statusunknown418
Copy link

@Angelelz here #1114 , I think it's actually yours lol

That PR is for MySQL, postgres doesn't have an on update clause.

Right, but could it be merged for MySQL though?

@pascalmtts
Copy link

We would also love to have this in drizzle. Currently, this is not really possible in a meaningful way with drizzle.

@ashishpandey001
Copy link

This is a core schema creation feature, at least for MySQL. Why is this labelled as an enhancement?

@Angelelz
Copy link
Collaborator

This is a core schema creation feature, at least for MySQL. Why is this labelled as an enhancement?

Postgres doesn't have an onUpdate. We're looking into this one. We're thinking that an API like the .$default() and .$defaultFn() but for updates would close this issue.

@ashishpandey001
Copy link

@Angelelz understood, thank you. I checked out the linked PR. A concern that I have is that implementing this functionality at the library level would be counter-productive for the MySQL dialect which has native support at the schema level for the trigger. Could we possibly implement this in such a way that this utilizes the schema definition for MySQL whereas use the lib's internal implementation or some custom plpgsql method for pg and other dialects?

@Angelelz
Copy link
Collaborator

How would it be counterproductive for MySql? We already have a similar feature with the .$default() method.

@ashishpandey001
Copy link

I'm saying that we should be replicating the behaviour described in this reference manual specifically for the DATETIME and TIMESTAMP data types via the MySQL dialect for drizzle rather than a generic .$defaultFn() or .$updateFn(). I hope I'm making some sense.

@Angelelz
Copy link
Collaborator

This is implemented already in mysql. We have in MySql the method .onUpdateNow() on all datetime and timestamp columns. The feature being proposed goes beyond updating timestamps, it could allow for example, implementing automatic counter at the database for how many times a user has updated his profile like this:

updateQty: int("updateQty").$onUpdate(() => sql`updateQty + 1`)

Now I know this can be done with a generated column in MySql, but not the datetime stuff in Pg, and also this is opt-in behavior.

@ashishpandey001
Copy link

I had no idea about the .onUpdateNow(). Is this documented somewhere because I can't seem to find it or the search is broken.

@Angelelz
Copy link
Collaborator

It is not. Can you raise a documentation issue?

@205g0
Copy link

205g0 commented Dec 5, 2023

@Angelelz according to the docs there's no .$onUpdate()

@Angelelz
Copy link
Collaborator

Angelelz commented Dec 5, 2023

@Angelelz according to the docs there's no .$onUpdate()

I was referring to a feature being proposed, not a current feature.

@2coo
Copy link

2coo commented Dec 12, 2023

The functionality that developers can extend their drizzle client like prisma client extension.
It would improve the developer experience way better.

Scenarios that I have faced:

  • Hashing the password value before saving it into a database.
  • adding a new property to query results at runtime and type-level.
    e.g. to use casl ability it requires to __caslSubjectType__ property.
  • Implement soft delete

@gsavvidis96
Copy link

any news on this issue? I am using postgres and currently there is not a drizzle-integrated way to handle updated_at field.

@predragnikolic
Copy link

You can subscribe to this issue and the PR #1509
to get notifications.

@givenloyiso
Copy link

givenloyiso commented Jan 5, 2024

It seems like they removed that function or something.
image

@davidprokopec
Copy link

@givenloyiso It's currently possible only for MySql drivers, not for any others

@jassibacha
Copy link

jassibacha commented Jan 30, 2024

This is implemented already in mysql. We have in MySql the method .onUpdateNow() on all datetime and timestamp columns. The feature being proposed goes beyond updating timestamps, it could allow for example, implementing automatic counter at the database for how many times a user has updated his profile like this:
...
Now I know this can be done with a generated column in MySql, but not the datetime stuff in Pg, and also this is opt-in behavior.

Hmm, I don't seem to be able to use .onUpdateNow() on datetime in my current project (MySQL + PlanetScale). I tried changing my updatedAt from timestamp to datetime and got an error.

export const createTable = mysqlTableCreator((name) => `asdf_${name}`);

export const users = createTable(
  "user",
  {
    id: varchar("id", { length: 256 }).primaryKey(),
    createdAt: timestamp("created_at")
      .default(sql`CURRENT_TIMESTAMP`)
      .notNull(),
    updatedAt: datetime("updatedAt").onUpdateNow(),
  },
  (table) => ({}),
);

I get this error on updatedAt line: Property 'onUpdateNow' does not exist on type 'MySqlDateTimeBuilderInitial<"updatedAt">'.

I also see nothing in the documentation for Drizzle that even mentions onUpdateNow(), and the AI Assistant on the Docs also couldn't find anything.

UPDATE: After doing more googling it looks like it's a PlanetScale issue that prevents onUpdateNow(). Boo. Wish the docs explained this stuff.

@DannyFeliz
Copy link

This is what is needed

  updatedAt: timestamp('updated_at', { mode: 'date', precision: 3 }).$onUpdate(() => new Date()),

@tposch
Copy link

tposch commented May 8, 2024

This is what is needed

  updatedAt: timestamp('updated_at', { mode: 'date', precision: 3 }).$onUpdate(() => new Date()),

You probably do not want to use new Date() because that will use server time. You probably want to use

$onUpdate(() => sql`CURRENT_TIMESTAMP`),

@anthonyhoegberg
Copy link

This is what is needed

  updatedAt: timestamp('updated_at', { mode: 'date', precision: 3 }).$onUpdate(() => new Date()),

You probably do not want to use new Date() because that will use server time. You probably want to use

$onUpdate(() => sql`CURRENT_TIMESTAMP`),

This would cause issues with stuff like drizzle-graphql since it assumes its a date, is there another way do use Date with db server time?

@carloschneider
Copy link

This is what is needed

  updatedAt: timestamp('updated_at', { mode: 'date', precision: 3 }).$onUpdate(() => new Date()),

You probably do not want to use new Date() because that will use server time. You probably want to use

$onUpdate(() => sql`CURRENT_TIMESTAMP`),

I dont know the reason, but when I tried to update an entry I received this error "TypeError: value.toISOString is not a function" so I chaged to new Date and it works.

@trevorpfiz
Copy link

trevorpfiz commented Jun 29, 2024

This is what is needed

  updatedAt: timestamp('updated_at', { mode: 'date', precision: 3 }).$onUpdate(() => new Date()),

You probably do not want to use new Date() because that will use server time. You probably want to use

$onUpdate(() => sql`CURRENT_TIMESTAMP`),

I dont know the reason, but when I tried to update an entry I received this error "TypeError: value.toISOString is not a function" so I chaged to new Date and it works.

I am getting TypeError: value.toISOString is not a function as well with $onUpdateFn(() => sql`now()`)

@belkocik
Copy link

belkocik commented Jun 30, 2024

It works with postgres-js library and PostgreSQL database

export const users = pgTable('users', {
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at')
    .defaultNow()
    .$onUpdate(() => new Date()),
});

However, with

$onUpdate(() => sql`CURRENT_TIMESTAMP`),

I got an error that was mentioned by others:
TypeError: value.toISOString is not a function

Any solution how to use sql(`CURRENT_TIMESTAMP`) or sql`now()

@christophemarois
Copy link

@belkocik workaround here: #2212 (comment)

@ghost
Copy link

ghost commented Nov 12, 2024

My setup. It works flawlessly

const timestamps = {
	created_at: timestamp({ withTimezone: true, mode: 'string' })
		.default(sql`(now() AT TIME ZONE 'utc'::text)`)
		.notNull(),
	updated_at: timestamp({ withTimezone: true, mode: 'string' })
		.default(sql`(now() AT TIME ZONE 'utc'::text)`)
		.notNull()
		.$onUpdate(() => sql`(now() AT TIME ZONE 'utc'::text)`),
}

export const countries = pgTable("countries", {
	id: uuid().defaultRandom().primaryKey().notNull(),
	name: text(),
	short: text(),
	code: text(),
	flag: text(),
	currency: text().default(''),
	symbol: text(),
	...timestamps,
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.