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

Feat: Drizzle-ORM support for Generated columns #1471

Merged
merged 42 commits into from
Jun 27, 2024

Conversation

Angelelz
Copy link
Collaborator

@Angelelz Angelelz commented Nov 6, 2023

This PR will close #261, will close #579 and will close #1464.

The API for generated columns will look like this:

const users = sqliteTable(
  'users',
  {
    id: int('id').primaryKey(),
    firstName: text('first_name', { length: 255 }),
    lastName: text('last_name', { length: 255 }),
    email: text('email').notNull(),
    fullName: text('full_name').generatedAlwaysAs(sql`concat_ws(first_name, ' ', last_name)`, { mode: 'stored' }),
    upperName: text('upper_name').generatedAlwaysAs(
      sql` case when first_name is null then null else upper(first_name) end `,
      { mode: 'virtual' }
    ).$type<string | null>(), // There is no way for drizzle to detect nullability in these cases. This is how the user can work around it
  },
);

In Postgres there is also a new method on integer bigint and smallint columns called .generatedAsIdentity()

You can pass a configuration object that will be different depending on your dialect:

// MySql & SQLite:
interface config {
  mode?: 'virtual' | 'stored';  // virtual is default
}

// Pg only for .generatedAsIdentity() method
interface config {
  type?: 'always' | 'byDefault';  // always is the default
}

For this table, the inferred types will be as follows:

type User = typeof users.$inferSelect;
    // ^?  {
    //   id: number;
    //   firstName: string | null;
    //   lastName: string | null;
    //   email: string;
    //   fullName: string;
    //   upperName: string | null;
    // },

type NewUser = typeof users.$inferInsert;
    // ^?  {
    //   email: string;
    //   id?: number | undefined;
    //   firstName?: string | null | undefined;
    //   lastName?: string | null | undefined;
    // },

Please note that drizzle kit support for this is still pending.

@Angelelz Angelelz marked this pull request as ready for review November 6, 2023 06:53
@Angelelz Angelelz force-pushed the feat-generated-columns branch from 870b55a to 6061a63 Compare November 10, 2023 02:28
Copy link

@luukvhoudt luukvhoudt left a comment

Choose a reason for hiding this comment

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

LGTM.

However, you should probably get rid of the merge commit and the comments that I highlighted in the comments below.

drizzle-orm/src/mysql-core/query-builders/update.ts Outdated Show resolved Hide resolved
integration-tests/tests/libsql.test.ts Outdated Show resolved Hide resolved
Copy link

@luukvhoudt luukvhoudt left a comment

Choose a reason for hiding this comment

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

Super, let's merge this!

@Angelelz
Copy link
Collaborator Author

This will also close #1239

@AndriiSherman AndriiSherman self-requested a review December 15, 2023 12:34
@adicco
Copy link

adicco commented Dec 25, 2023

Any updates on this? Thank you!

@nikelborm
Copy link

nikelborm commented Jan 2, 2024

@Angelelz could you please show how this feature infers select or insert types for "generated always as identity" columns? It should forbid the user to insert values to a column of this type because postgres will always throw an error.
It should either be

type NewUser = typeof users.$inferInsert;
    // ^?  {
    //   email: string;
    //   firstName?: string | null | undefined;
    //   lastName?: string | null | undefined;
    // },

or be

type NewUser = typeof users.$inferInsert;
    // ^?  {
    //   email: string;
    //   id: never;
    //   firstName?: string | null | undefined;
    //   lastName?: string | null | undefined;
    // },

@davepar
Copy link

davepar commented Jan 31, 2024

This looks awesome! What's holding up merging this PR? Generated columns are currently very difficult, because of the custom type bug in drizzle-kit https://github.com/drizzle-team/drizzle-kit-mirror/issues/167.

@emmbm
Copy link

emmbm commented Feb 12, 2024

Is there anything the community can do to help this move forward? It's currently kind of cumbersome to create a custom type every time we need a generated column and this feature would make schema declarations much cleaner imho.

@kedarv
Copy link

kedarv commented Feb 17, 2024

Echoing the comments above on how we can help move this PR forward -- I think this PR adds some fairly necessary functionality!

@gfmio
Copy link

gfmio commented Mar 11, 2024

Hi!

Thanks for this PR! Exactly what I was looking for :)

I just made a fork of this change to try it out and installed it as an override for drizzle-orm in my test project.

I tried it using it with sqlite, mysql and postgres, but unfortunately I was not able to get it to work.

Here's the small test model I used for sqlite.

const urnPrefix = "urn:example:users:";
export const usersTable = sqliteTable("users", {
  id: integer("id").unique().notNull().primaryKey({ autoIncrement: true }),
  urn: text("urn").generatedAlwaysAs(sql`${urnPrefix} || id`, { mode: "virtual" }).notNull().unique(),
});

I ran drizzle-kit to generate the migrations but the urn field is generated as a regular text column and not as a generated column.

Is this meant to work with drizzle-kit or will this require a separate change?

I played around with it a bit more and I came up with the following solution that does work for me:

export interface GenerateCustomTypeOptions<T> {
  data: T;
  config: { sql: SQL | string; mode?: "stored" | "virtual" | undefined };
  notNull: true;
  default: true;
  configRequired: true;
}

export type GeneratedColumnType = <T>(
  ...args: Parameters<ReturnType<typeof customType<GenerateCustomTypeOptions<T>>>>
) => ReturnType<ReturnType<typeof customType<GenerateCustomTypeOptions<T>>>>;

const generated: GeneratedColumnType = <T>(
  ...args: Parameters<ReturnType<typeof customType<GenerateCustomTypeOptions<T>>>>
) => {
  const [_, config] = args;
  const asSql = sql`as (${config.sql})`;
  const asSqlString = asSql.toQuery({
    escapeName: (name) => `'${name}`,
    escapeParam: (_, value) => `'${value}'`,
    escapeString: (str) => `'${str}'`,
  }).sql;
  const joinedSqlString = `${asSqlString} ${config?.mode ?? "virtual"}`;

  return customType<GenerateCustomTypeOptions<T>>({
    dataType: () => joinedSqlString,
  })(...args).generatedAlwaysAs(asSql, { mode: config.mode });
};

Note: The generatedAlwaysAs(asSql, { mode: config.mode }) portion is only needed to prevent errors during inserts / updates since drizzle would otherwise attempt to set urn to null.

Do you think it would make sense to adapt this PR to provide a generated column type builder function like this and add, instead of the generated field in the column type add a writable field to indicate that the field can and should be written to? This would (in my opinion anyway) nicely mirror the rest of the drizzle API and for example also be useful if we want to prevent schema users from ever directly writing to a field.

@SiNONiMiTY
Copy link

It is time to ditch the PostgreSQL specific way of doing auto incremented columns with SERIAL.

@psygo
Copy link

psygo commented Apr 24, 2024

Maybe, in the meantime, leave an example in the docs with the necessary SQL code to make this happen?

@emmbm
Copy link

emmbm commented Apr 24, 2024

Maybe, in the meantime, leave an example in the docs with the necessary SQL code to make this happen?

The catch is that there's currently no way to implement this using a custom type in a manner that is compatible with drizzle-kit (#579 (comment)). So adding the workaround in the docs might just end up confusing more users who are going to face failing migrations.

@arjunyel
Copy link
Contributor

arjunyel commented May 3, 2024

For Postgres users here is a patch-package to fix this with drizzle-kit

diff --git a/node_modules/drizzle-kit/bin.cjs b/node_modules/drizzle-kit/bin.cjs
index 9b92321..d347342 100755
--- a/node_modules/drizzle-kit/bin.cjs
+++ b/node_modules/drizzle-kit/bin.cjs
@@ -15496,6 +15496,7 @@ var init_sqlgenerator = __esm({
       "interval minute to second"
     ]);
     isPgNativeType = (it) => {
+      return true
       if (pgNativeTypes.has(it))
         return true;
       const toCheck = it.replace(/ /g, "");

I've only tested this type, test other custom types as I have no clue if this will break them

import { customType } from "drizzle-orm/pg-core";
export const identity = (name: string) =>
  customType<{
    data: string;
    notNull: true;
    default: true;
  }>({
    dataType() {
      return "BIGINT GENERATED ALWAYS AS IDENTITY";
    },
  })(name);

@gediminastub
Copy link

Heya, thanks a lot for amazing work!
Are there any plans to finish this up soon? Thanks!

@dkmooers
Copy link

Adding a +1 for this - would love to use generated columns for post comment counts and such! It would get widespread use throughout the app I'm building. 🙏 😁

@modernmusician
Copy link

+1 for this!

@Julian-Hackenberg
Copy link

+1 for this, would be great for generated columns in fulltext search for types like arrays with tags for example!

@davepar
Copy link

davepar commented Jun 19, 2024

Back in March it sounded like this PR was set to be merged, and then progress stalled. I can't find the discussion now, but I remember reading in Discord that the Drizzle team was going to make some major changes to schema definition implying that it would require a different approach for generated columns. (Maybe this? #2316) So I wouldn't expect this PR to get merged soon.

Personally, after waiting several months for generated columns I couldn't wait any longer. Text and location search are a vital part of my project. I ended up ripping Drizzle out of everything, and just using the Postgres.js driver directly. Which also solved a lot of other issues (e.g. lack of query aggregations, a bug in migration generation for custom types, etc.). It turns out that writing my own TS type definitions was easier than I expected.

Not trying to bash Drizzle. It's excellent. It's just missing some features that I need right now.

@AndriiSherman
Copy link
Member

Back in March it sounded like this PR was set to be merged, and then progress stalled. I can't find the discussion now, but I remember reading in Discord that the Drizzle team was going to make some major changes to schema definition implying that it would require a different approach for generated columns. (Maybe this? #2316) So I wouldn't expect this PR to get merged soon.

Personally, after waiting several months for generated columns I couldn't wait any longer. Text and location search are a vital part of my project. I ended up ripping Drizzle out of everything, and just using the Postgres.js driver directly. Which also solved a lot of other issues (e.g. lack of query aggregations, a bug in migration generation for custom types, etc.). It turns out that writing my own TS type definitions was easier than I expected.

Not trying to bash Drizzle. It's excellent. It's just missing some features that I need right now.

sending this to beta release today, I'm finishing release notes, will update here

@AndriiSherman AndriiSherman changed the base branch from beta to generated June 27, 2024 09:25
@AndriiSherman AndriiSherman merged commit 3245641 into drizzle-team:generated Jun 27, 2024
@AndriiSherman
Copy link
Member

Release notes with all the detailed info: https://github.com/drizzle-team/drizzle-orm/releases/tag/v0.32.0-beta

GitHub discussions: #2564

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.