Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat(pg): add virtual generated columns support
- Add virtual mode support for PostgreSQL generated columns
- Update schema serializer to handle both virtual and stored generated columns
- Modify SQL generator to support VIRTUAL keyword in generated column statements
- Add comprehensive tests for virtual generated columns
- Update type definitions and interfaces for virtual generated column configuration
- Ensure backward compatibility with existing stored generated columns
  • Loading branch information
namhtpyn committed Sep 21, 2025
commit 00a27baa75b716169808776ab9da555d08a371d7
4 changes: 2 additions & 2 deletions drizzle-kit/src/serializer/pgSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ const column = object({
uniqueName: string().optional(),
nullsNotDistinct: boolean().optional(),
generated: object({
type: literal('stored'),
type: enumType(['virtual', 'stored']),
as: string(),
}).optional(),
identity: sequenceSchema
Expand All @@ -207,7 +207,7 @@ const columnSquashed = object({
uniqueName: string().optional(),
nullsNotDistinct: boolean().optional(),
generated: object({
type: literal('stored'),
type: enumType(['virtual', 'stored']),
as: string(),
}).optional(),
identity: string().optional(),
Expand Down
4 changes: 2 additions & 2 deletions drizzle-kit/src/serializer/pgSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export const generatePgSnapshot = (
: typeof generated.as === 'function'
? dialect.sqlToQuery(generated.as() as SQL).sql
: (generated.as as any),
type: 'stored',
type: generated.mode ?? 'stored',
}
: undefined,
identity: identity
Expand Down Expand Up @@ -780,7 +780,7 @@ export const generatePgSnapshot = (
: typeof generated.as === 'function'
? dialect.sqlToQuery(generated.as() as SQL).sql
: (generated.as as any),
type: 'stored',
type: generated.mode ?? 'stored',
}
: undefined,
identity: identity
Expand Down
8 changes: 6 additions & 2 deletions drizzle-kit/src/sqlgenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,9 @@ class PgCreateTableConvertor extends Convertor {
const type = parseType(schemaPrefix, column.type);
const generated = column.generated;

const generatedStatement = generated ? ` GENERATED ALWAYS AS (${generated?.as}) STORED` : '';
const generatedStatement = generated
? ` GENERATED ALWAYS AS (${generated?.as}) ${generated?.type.toUpperCase()}`
: '';

const unsquashedIdentity = column.identity
? PgSquasher.unsquashIdentity(column.identity)
Expand Down Expand Up @@ -1793,7 +1795,9 @@ class PgAlterTableAddColumnConvertor extends Convertor {
})`
: '';

const generatedStatement = generated ? ` GENERATED ALWAYS AS (${generated?.as}) STORED` : '';
const generatedStatement = generated
? ` GENERATED ALWAYS AS (${generated?.as}) ${generated?.type.toUpperCase()}`
: '';

return `ALTER TABLE ${tableNameWithSchema} ADD COLUMN "${name}" ${fixedType}${primaryKeyStatement}${defaultStatement}${generatedStatement}${notNullStatement}${identityStatement};`;
}
Expand Down
226 changes: 226 additions & 0 deletions drizzle-kit/tests/pg-generated.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,3 +527,229 @@ test('generated as string: change generated constraint', async () => {
'ALTER TABLE "users" ADD COLUMN "gen_name" text GENERATED ALWAYS AS ("users"."name" || \'hello\') STORED;',
]);
});

test('generated as callback: add column with virtual generated constraint', async () => {
const from = {
users: pgTable('users', {
id: integer('id'),
id2: integer('id2'),
name: text('name'),
}),
};
const to = {
users: pgTable('users', {
id: integer('id'),
id2: integer('id2'),
name: text('name'),
generatedName: text('gen_name').generatedAlwaysAs(
(): SQL => sql`${to.users.name} || 'hello'`,
{ mode: 'virtual' },
),
}),
};

const { statements, sqlStatements } = await diffTestSchemas(from, to, []);

expect(statements).toStrictEqual([
{
column: {
generated: {
as: '"users"."name" || \'hello\'',
type: 'virtual',
},
name: 'gen_name',
notNull: false,
primaryKey: false,
type: 'text',
},
schema: '',
tableName: 'users',
type: 'alter_table_add_column',
},
]);
expect(sqlStatements).toStrictEqual([
`ALTER TABLE "users" ADD COLUMN "gen_name" text GENERATED ALWAYS AS (\"users\".\"name\" || 'hello') VIRTUAL;`,
]);
});

test('generated as SQL: add column with virtual generated constraint', async () => {
const from = {
users: pgTable('users', {
id: integer('id'),
id2: integer('id2'),
name: text('name'),
}),
};
const to = {
users: pgTable('users', {
id: integer('id'),
id2: integer('id2'),
name: text('name'),
generatedName: text('gen_name').generatedAlwaysAs(
sql`concat("users"."name", 'hello')`,
{ mode: 'virtual' },
),
}),
};

const { statements, sqlStatements } = await diffTestSchemas(from, to, []);

expect(statements).toStrictEqual([
{
column: {
generated: {
as: 'concat("users"."name", \'hello\')',
type: 'virtual',
},
name: 'gen_name',
notNull: false,
primaryKey: false,
type: 'text',
},
schema: '',
tableName: 'users',
type: 'alter_table_add_column',
},
]);
expect(sqlStatements).toStrictEqual([
`ALTER TABLE "users" ADD COLUMN "gen_name" text GENERATED ALWAYS AS (concat("users"."name", 'hello')) VIRTUAL;`,
]);
});

test('generated as string: add column with virtual generated constraint', async () => {
const from = {
users: pgTable('users', {
id: integer('id'),
id2: integer('id2'),
name: text('name'),
}),
};
const to = {
users: pgTable('users', {
id: integer('id'),
id2: integer('id2'),
name: text('name'),
generatedName: text('gen_name').generatedAlwaysAs(
`\"users\".\"name\" || 'hello'`,
{ mode: 'virtual' },
),
}),
};

const { statements, sqlStatements } = await diffTestSchemas(from, to, []);

expect(statements).toStrictEqual([
{
column: {
generated: {
as: '"users"."name" || \'hello\'',
type: 'virtual',
},
name: 'gen_name',
notNull: false,
primaryKey: false,
type: 'text',
},
schema: '',
tableName: 'users',
type: 'alter_table_add_column',
},
]);
expect(sqlStatements).toStrictEqual([
`ALTER TABLE "users" ADD COLUMN "gen_name" text GENERATED ALWAYS AS ("users"."name" || 'hello') VIRTUAL;`,
]);
});

test('change from stored to virtual generated constraint', async () => {
const from = {
users: pgTable('users', {
id: integer('id'),
id2: integer('id2'),
name: text('name'),
generatedName: text('gen_name').generatedAlwaysAs(
sql`"users"."name" || 'hello'`,
{ mode: 'stored' },
),
}),
};
const to = {
users: pgTable('users', {
id: integer('id'),
id2: integer('id2'),
name: text('name'),
generatedName: text('gen_name').generatedAlwaysAs(
sql`"users"."name" || 'hello'`,
{ mode: 'virtual' },
),
}),
};

const { statements, sqlStatements } = await diffTestSchemas(from, to, []);

expect(statements).toStrictEqual([
{
columnAutoIncrement: undefined,
columnDefault: undefined,
columnGenerated: { as: '"users"."name" || \'hello\'', type: 'virtual' },
columnName: 'gen_name',
columnNotNull: false,
columnOnUpdate: undefined,
columnPk: false,
newDataType: 'text',
schema: '',
tableName: 'users',
type: 'alter_table_alter_column_alter_generated',
},
]);
expect(sqlStatements).toStrictEqual([
'ALTER TABLE "users" drop column "gen_name";',
'ALTER TABLE "users" ADD COLUMN "gen_name" text GENERATED ALWAYS AS ("users"."name" || \'hello\') VIRTUAL;',
]);
});

test('change from virtual to stored generated constraint', async () => {
const from = {
users: pgTable('users', {
id: integer('id'),
id2: integer('id2'),
name: text('name'),
generatedName: text('gen_name').generatedAlwaysAs(
sql`"users"."name" || 'hello'`,
{ mode: 'virtual' },
),
}),
};
const to = {
users: pgTable('users', {
id: integer('id'),
id2: integer('id2'),
name: text('name'),
generatedName: text('gen_name').generatedAlwaysAs(
sql`"users"."name" || 'hello'`,
{ mode: 'stored' },
),
}),
};

const { statements, sqlStatements } = await diffTestSchemas(from, to, []);

expect(statements).toStrictEqual([
{
columnAutoIncrement: undefined,
columnDefault: undefined,
columnGenerated: { as: '"users"."name" || \'hello\'', type: 'stored' },
columnName: 'gen_name',
columnNotNull: false,
columnOnUpdate: undefined,
columnPk: false,
newDataType: 'text',
schema: '',
tableName: 'users',
type: 'alter_table_alter_column_alter_generated',
},
]);
expect(sqlStatements).toStrictEqual([
'ALTER TABLE "users" drop column "gen_name";',
'ALTER TABLE "users" ADD COLUMN "gen_name" text GENERATED ALWAYS AS ("users"."name" || \'hello\') STORED;',
]);
});
8 changes: 6 additions & 2 deletions drizzle-orm/src/pg-core/columns/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export interface ReferenceConfig {
};
}

export interface PgGeneratedColumnConfig {
mode?: 'virtual' | 'stored';
}

export interface PgColumnBuilderBase<
T extends ColumnBuilderBaseConfig<ColumnDataType, string> = ColumnBuilderBaseConfig<ColumnDataType, string>,
TTypeConfig extends object = object,
Expand Down Expand Up @@ -83,13 +87,13 @@ export abstract class PgColumnBuilder<
return this;
}

generatedAlwaysAs(as: SQL | T['data'] | (() => SQL)): HasGenerated<this, {
generatedAlwaysAs(as: SQL | T['data'] | (() => SQL), config?: PgGeneratedColumnConfig): HasGenerated<this, {
type: 'always';
}> {
this.config.generated = {
as,
type: 'always',
mode: 'stored',
mode: config?.mode ?? 'stored',
};
return this as HasGenerated<this, {
type: 'always';
Expand Down
Loading