From e06214ae9fe4b5e06771de242013b702348d73f2 Mon Sep 17 00:00:00 2001 From: Valerii Strilets Date: Wed, 10 Jun 2026 19:00:54 +0300 Subject: [PATCH] add replica identity --- .../src/dialects/cockroach/convertor.ts | 15 +++ drizzle-kit/src/dialects/cockroach/ddl.ts | 7 +- drizzle-kit/src/dialects/cockroach/diff.ts | 26 ++++ drizzle-kit/src/dialects/cockroach/drizzle.ts | 10 ++ .../src/dialects/cockroach/introspect.ts | 18 +++ .../src/dialects/cockroach/statements.ts | 9 ++ .../src/dialects/cockroach/typescript.ts | 8 +- .../src/dialects/postgres/aws-introspect.ts | 18 +++ .../src/dialects/postgres/commutativity.ts | 10 ++ .../src/dialects/postgres/convertor.ts | 17 +++ drizzle-kit/src/dialects/postgres/ddl.ts | 7 +- drizzle-kit/src/dialects/postgres/diff.ts | 26 ++++ drizzle-kit/src/dialects/postgres/drizzle.ts | 10 ++ .../dialects/postgres/duckdb-introspect.ts | 1 + .../src/dialects/postgres/introspect.ts | 18 +++ .../src/dialects/postgres/serializer.ts | 7 + .../src/dialects/postgres/statements.ts | 9 ++ .../src/dialects/postgres/typescript.ts | 8 +- drizzle-kit/src/dialects/postgres/versions.ts | 1 + drizzle-kit/src/ext/studio-postgres.ts | 2 + .../tests/cockroach/replica-identity.test.ts | 71 ++++++++++ .../tests/other/hints-probe-skip.test.ts | 5 +- .../tests/postgres/commutativity.test.ts | 124 +++++++++--------- drizzle-kit/tests/postgres/pg-tables.test.ts | 108 +++++++++++++++ drizzle-kit/tests/postgres/pull.test.ts | 9 ++ drizzle-orm/src/cockroach-core/table.ts | 38 ++++++ drizzle-orm/src/cockroach-core/utils.ts | 2 + drizzle-orm/src/pg-core/table.ts | 42 ++++++ drizzle-orm/src/pg-core/utils.ts | 2 + integration-tests/tests/pg/utils.test.ts | 23 ++++ 30 files changed, 583 insertions(+), 68 deletions(-) create mode 100644 drizzle-kit/tests/cockroach/replica-identity.test.ts diff --git a/drizzle-kit/src/dialects/cockroach/convertor.ts b/drizzle-kit/src/dialects/cockroach/convertor.ts index 4250f4e2c2..cb08b5db53 100644 --- a/drizzle-kit/src/dialects/cockroach/convertor.ts +++ b/drizzle-kit/src/dialects/cockroach/convertor.ts @@ -715,6 +715,20 @@ const toggleRlsConvertor = convertor('alter_rls', (st) => { return `ALTER TABLE ${tableNameWithSchema} ${isRlsEnabled ? 'ENABLE' : 'DISABLE'} ROW LEVEL SECURITY;`; }); +const replicaIdentityConvertor = convertor('alter_replica_identity', (st) => { + const { schema, name, replicaIdentity } = st; + + const tableNameWithSchema = schema !== 'public' ? `"${schema}"."${name}"` : `"${name}"`; + + const clause = !replicaIdentity + ? 'DEFAULT' + : replicaIdentity.type === 'index' + ? `USING INDEX "${replicaIdentity.index}"` + : replicaIdentity.type.toUpperCase(); + + return `ALTER TABLE ${tableNameWithSchema} REPLICA IDENTITY ${clause};`; +}); + const convertors = [ createSchemaConvertor, dropSchemaConvertor, @@ -767,6 +781,7 @@ const convertors = [ alterPolicyConvertor, recreatePolicy, toggleRlsConvertor, + replicaIdentityConvertor, alterPrimaryKeyConvertor, alterColumnAddNotNullConvertor, alterColumnDropNotNullConvertor, diff --git a/drizzle-kit/src/dialects/cockroach/ddl.ts b/drizzle-kit/src/dialects/cockroach/ddl.ts index 5318b5bb35..b779efbe68 100644 --- a/drizzle-kit/src/dialects/cockroach/ddl.ts +++ b/drizzle-kit/src/dialects/cockroach/ddl.ts @@ -6,7 +6,11 @@ import { defaults } from './grammar'; export const createDDL = () => { return create({ schemas: {}, - tables: { schema: 'required', isRlsEnabled: 'boolean' }, + tables: { + schema: 'required', + isRlsEnabled: 'boolean', + replicaIdentity: { type: ['full', 'nothing', 'index'], index: 'string?' }, + }, enums: { schema: 'required', values: 'string[]', @@ -138,6 +142,7 @@ export type Table = { checks: CheckConstraint[]; policies: Policy[]; isRlsEnabled: boolean; + replicaIdentity: CockroachEntities['tables']['replicaIdentity']; }; export type InterimColumn = Omit & { diff --git a/drizzle-kit/src/dialects/cockroach/diff.ts b/drizzle-kit/src/dialects/cockroach/diff.ts index fef8035614..84c9474feb 100644 --- a/drizzle-kit/src/dialects/cockroach/diff.ts +++ b/drizzle-kit/src/dialects/cockroach/diff.ts @@ -764,6 +764,30 @@ export const ddlDiff = async ( } }); + // replica identity: set for newly created tables (after indexes are created, see ordering below) + // and altered for existing tables when it changes + const jsonAlterReplicaIdentityStatements = [ + ...createdTables + .filter((it) => it.replicaIdentity) + .map((it) => + prepareStatement('alter_replica_identity', { + schema: it.schema, + name: it.name, + replicaIdentity: it.replicaIdentity, + }) + ), + ...alters + .filter((it) => it.entityType === 'tables') + .filter((it) => it.replicaIdentity) + .map((it) => + prepareStatement('alter_replica_identity', { + schema: it.schema, + name: it.name, + replicaIdentity: it.replicaIdentity?.to ?? null, + }) + ), + ]; + // explicit rls alters const rlsAlters = alters.filter((it) => it.entityType === 'tables').filter((it) => it.isRlsEnabled); @@ -1063,6 +1087,8 @@ export const ddlDiff = async ( jsonStatements.push(...jsonRecreateFKs); jsonStatements.push(...jsonCreateIndexes); + jsonStatements.push(...jsonAlterReplicaIdentityStatements); // after indexes for `USING INDEX` to resolve + jsonStatements.push(...jsonDropColumnsStatemets); jsonStatements.push(...jsonCreatedCheckConstraints); diff --git a/drizzle-kit/src/dialects/cockroach/drizzle.ts b/drizzle-kit/src/dialects/cockroach/drizzle.ts index 781c2cfa52..d166234c16 100644 --- a/drizzle-kit/src/dialects/cockroach/drizzle.ts +++ b/drizzle-kit/src/dialects/cockroach/drizzle.ts @@ -62,6 +62,15 @@ import { typeFor, } from './grammar'; +export const replicaIdentityFrom = ( + replicaIdentity: ReturnType['replicaIdentity'], +): CockroachEntities['tables']['replicaIdentity'] => { + if (!replicaIdentity || replicaIdentity === 'default') return null; + if (replicaIdentity === 'full') return { type: 'full', index: null }; + if (replicaIdentity === 'nothing') return { type: 'nothing', index: null }; + return { type: 'index', index: replicaIdentity.usingIndex }; +}; + export const policyFrom = (policy: CockroachPolicy, dialect: CockroachDialect) => { const mappedTo = !policy.to ? ['public'] @@ -280,6 +289,7 @@ export const fromDrizzleSchema = ( schema, name: config.name, isRlsEnabled, + replicaIdentity: replicaIdentityFrom(config.replicaIdentity), } satisfies CockroachEntities['tables']; }); diff --git a/drizzle-kit/src/dialects/cockroach/introspect.ts b/drizzle-kit/src/dialects/cockroach/introspect.ts index 49c6a505e2..f481c96e3e 100644 --- a/drizzle-kit/src/dialects/cockroach/introspect.ts +++ b/drizzle-kit/src/dialects/cockroach/introspect.ts @@ -128,6 +128,9 @@ export const fromDatabase = async ( accessMethod: number; options: string[] | null; rlsEnabled: boolean; + /* d - default, n - nothing, f - full, i - index */ + replicaIdentity: 'd' | 'n' | 'f' | 'i'; + replicaIdentityIndex: string | null; tablespaceid: number; definition: string | null; }>( @@ -141,6 +144,14 @@ export const fromDatabase = async ( reloptions::text[] as "options", reltablespace as "tablespaceid", relrowsecurity AS "rlsEnabled", + relreplident::text AS "replicaIdentity", + ( + SELECT ci.relname + FROM pg_catalog.pg_index i + JOIN pg_catalog.pg_class ci ON ci.oid OPERATOR(pg_catalog.=) i.indexrelid + WHERE i.indrelid OPERATOR(pg_catalog.=) pg_class.oid AND i.indisreplident + LIMIT 1 + ) AS "replicaIdentityIndex", CASE WHEN relkind OPERATOR(pg_catalog.=) 'v' OR relkind OPERATOR(pg_catalog.=) 'm' THEN pg_catalog.pg_get_viewdef(pg_class.oid, true) @@ -191,6 +202,13 @@ export const fromDatabase = async ( schema: table.schema, name: table.name, isRlsEnabled: table.rlsEnabled, + replicaIdentity: table.replicaIdentity === 'f' + ? { type: 'full', index: null } + : table.replicaIdentity === 'n' + ? { type: 'nothing', index: null } + : table.replicaIdentity === 'i' + ? { type: 'index', index: table.replicaIdentityIndex } + : null, }); } diff --git a/drizzle-kit/src/dialects/cockroach/statements.ts b/drizzle-kit/src/dialects/cockroach/statements.ts index 9344cdc56f..152c5ecffd 100644 --- a/drizzle-kit/src/dialects/cockroach/statements.ts +++ b/drizzle-kit/src/dialects/cockroach/statements.ts @@ -1,6 +1,7 @@ import type { Simplify } from '../../utils'; import type { CheckConstraint, + CockroachEntities, Column, DiffEntities, Enum, @@ -182,6 +183,13 @@ export interface JsonAlterRLS { isRlsEnabled: boolean; } +export interface JsonAlterReplicaIdentity { + type: 'alter_replica_identity'; + schema: string; + name: string; + replicaIdentity: CockroachEntities['tables']['replicaIdentity']; +} + export interface JsonAlterPolicy { type: 'alter_policy'; diff: DiffEntities['policies']; @@ -423,6 +431,7 @@ export type JsonStatement = | JsonRecreatePolicy | JsonRenamePolicy | JsonAlterRLS + | JsonAlterReplicaIdentity | JsonRenameRole | JsonCreateRole | JsonDropRole diff --git a/drizzle-kit/src/dialects/cockroach/typescript.ts b/drizzle-kit/src/dialects/cockroach/typescript.ts index a8f062c47b..0373ecf9ed 100644 --- a/drizzle-kit/src/dialects/cockroach/typescript.ts +++ b/drizzle-kit/src/dialects/cockroach/typescript.ts @@ -377,7 +377,13 @@ export const ddlToTypeScript = (ddl: CockroachDDL, columnsForViews: ViewColumn[] statement += createTableChecks(table.checks, casing); statement += ']'; } - statement += ');'; + statement += ')'; + if (table.replicaIdentity) { + statement += table.replicaIdentity.type === 'index' + ? `.replicaIdentity({ usingIndex: "${table.replicaIdentity.index}" })` + : `.replicaIdentity("${table.replicaIdentity.type}")`; + } + statement += ';'; return statement; }); diff --git a/drizzle-kit/src/dialects/postgres/aws-introspect.ts b/drizzle-kit/src/dialects/postgres/aws-introspect.ts index 6462039b5b..e854482a41 100644 --- a/drizzle-kit/src/dialects/postgres/aws-introspect.ts +++ b/drizzle-kit/src/dialects/postgres/aws-introspect.ts @@ -166,6 +166,9 @@ export const fromDatabase = async ( accessMethod: string; options: string[] | null; rlsEnabled: boolean; + /* d - default, n - nothing, f - full, i - index */ + replicaIdentity: 'd' | 'n' | 'f' | 'i'; + replicaIdentityIndex: string | null; tablespaceid: string; definition: string | null; }; @@ -181,6 +184,14 @@ export const fromDatabase = async ( reloptions::text[] as "options", reltablespace as "tablespaceid", relrowsecurity AS "rlsEnabled", + relreplident::text AS "replicaIdentity", + ( + SELECT ci.relname + FROM pg_catalog.pg_index i + JOIN pg_catalog.pg_class ci ON ci.oid OPERATOR(pg_catalog.=) i.indexrelid + WHERE i.indrelid OPERATOR(pg_catalog.=) pg_class.oid AND i.indisreplident + LIMIT 1 + ) AS "replicaIdentityIndex", CASE WHEN relkind OPERATOR(pg_catalog.=) 'v' OR relkind OPERATOR(pg_catalog.=) 'm' THEN pg_catalog.pg_get_viewdef(pg_class.oid, true) @@ -225,6 +236,13 @@ export const fromDatabase = async ( schema: trimChar(table.schema, "'"), name: table.name, isRlsEnabled: table.rlsEnabled, + replicaIdentity: table.replicaIdentity === 'f' + ? { type: 'full', index: null } + : table.replicaIdentity === 'n' + ? { type: 'nothing', index: null } + : table.replicaIdentity === 'i' + ? { type: 'index', index: table.replicaIdentityIndex } + : null, }); } diff --git a/drizzle-kit/src/dialects/postgres/commutativity.ts b/drizzle-kit/src/dialects/postgres/commutativity.ts index dc01967a4f..e386c9b9be 100644 --- a/drizzle-kit/src/dialects/postgres/commutativity.ts +++ b/drizzle-kit/src/dialects/postgres/commutativity.ts @@ -165,6 +165,7 @@ class PostgresCommutativity extends AbstractCommutativity< 'alter_policy', 'recreate_policy', 'alter_rls', + 'alter_replica_identity', 'grant_privilege', 'revoke_privilege', 'regrant_privilege', @@ -599,6 +600,15 @@ class PostgresCommutativity extends AbstractCommutativity< }), }, + // Replica identity operations + alter_replica_identity: { + conflicts: ['alter_replica_identity'], + buildInfo: (statement) => ({ + primary: makeTableTarget((statement as any).schema, (statement as any).name), + ancestors: [], + }), + }, + // Role operations create_role: { conflicts: ['create_role', 'drop_role', 'rename_role', 'alter_role'], diff --git a/drizzle-kit/src/dialects/postgres/convertor.ts b/drizzle-kit/src/dialects/postgres/convertor.ts index 85cf5a7829..89b1042529 100644 --- a/drizzle-kit/src/dialects/postgres/convertor.ts +++ b/drizzle-kit/src/dialects/postgres/convertor.ts @@ -1038,6 +1038,22 @@ const toggleRlsConvertor = convertor('alter_rls', (st) => { return `ALTER TABLE ${tableNameWithSchema} ${isRlsEnabled ? 'ENABLE' : 'DISABLE'} ROW LEVEL SECURITY;`; }); +const replicaIdentityConvertor = convertor('alter_replica_identity', (st) => { + const { schema, name, replicaIdentity } = st; + + const tableNameWithSchema = schema !== 'public' + ? `"${schema}"."${name}"` + : `"${name}"`; + + const clause = !replicaIdentity + ? 'DEFAULT' + : replicaIdentity.type === 'index' + ? `USING INDEX "${replicaIdentity.index}"` + : replicaIdentity.type.toUpperCase(); + + return `ALTER TABLE ${tableNameWithSchema} REPLICA IDENTITY ${clause};`; +}); + const convertors = [ createSchemaConvertor, dropSchemaConvertor, @@ -1097,6 +1113,7 @@ const convertors = [ alterPolicyConvertor, recreatePolicy, toggleRlsConvertor, + replicaIdentityConvertor, ]; export function fromJson( diff --git a/drizzle-kit/src/dialects/postgres/ddl.ts b/drizzle-kit/src/dialects/postgres/ddl.ts index c95adb1ad0..11870f6969 100644 --- a/drizzle-kit/src/dialects/postgres/ddl.ts +++ b/drizzle-kit/src/dialects/postgres/ddl.ts @@ -5,7 +5,11 @@ import { defaultNameForPK, defaultNameForUnique } from './grammar'; export const createDDL = () => { return create({ schemas: {}, - tables: { schema: 'required', isRlsEnabled: 'boolean' }, + tables: { + schema: 'required', + isRlsEnabled: 'boolean', + replicaIdentity: { type: ['full', 'nothing', 'index'], index: 'string?' }, + }, enums: { schema: 'required', values: 'string[]', @@ -201,6 +205,7 @@ export type Table = { checks: CheckConstraint[]; policies: Policy[]; isRlsEnabled: boolean; + replicaIdentity: PostgresEntities['tables']['replicaIdentity']; }; export type InterimColumn = Omit & { diff --git a/drizzle-kit/src/dialects/postgres/diff.ts b/drizzle-kit/src/dialects/postgres/diff.ts index 692b5eae63..6f6df47a9c 100644 --- a/drizzle-kit/src/dialects/postgres/diff.ts +++ b/drizzle-kit/src/dialects/postgres/diff.ts @@ -937,6 +937,30 @@ export const ddlDiff = async ( }, ); + // replica identity: set for newly created tables (after indexes are created, see ordering below) + // and altered for existing tables when it changes + const jsonAlterReplicaIdentityStatements = [ + ...createdTables + .filter((it) => it.replicaIdentity) + .map((it) => + prepareStatement('alter_replica_identity', { + schema: it.schema, + name: it.name, + replicaIdentity: it.replicaIdentity, + }) + ), + ...alters + .filter((it) => it.entityType === 'tables') + .filter((it) => it.replicaIdentity) + .map((it) => + prepareStatement('alter_replica_identity', { + schema: it.schema, + name: it.name, + replicaIdentity: it.replicaIdentity?.to ?? null, + }) + ), + ]; + // explicit rls alters const rlsAlters = alters.filter((it) => it.entityType === 'tables').filter((it) => it.isRlsEnabled); @@ -1267,6 +1291,8 @@ export const ddlDiff = async ( jsonStatements.push(...jsonAlteredUniqueConstraints); jsonStatements.push(...jsonCreateIndexes); // above fks for uniqueness constraint to come first + jsonStatements.push(...jsonAlterReplicaIdentityStatements); // after indexes for `USING INDEX` to resolve + jsonStatements.push(...jsonCreateFKs); jsonStatements.push(...jsonRecreateFKs); diff --git a/drizzle-kit/src/dialects/postgres/drizzle.ts b/drizzle-kit/src/dialects/postgres/drizzle.ts index 9734308179..8148741f9e 100644 --- a/drizzle-kit/src/dialects/postgres/drizzle.ts +++ b/drizzle-kit/src/dialects/postgres/drizzle.ts @@ -71,6 +71,15 @@ import { typeFor, } from './grammar'; +export const replicaIdentityFrom = ( + replicaIdentity: ReturnType['replicaIdentity'], +): PostgresEntities['tables']['replicaIdentity'] => { + if (!replicaIdentity || replicaIdentity === 'default') return null; + if (replicaIdentity === 'full') return { type: 'full', index: null }; + if (replicaIdentity === 'nothing') return { type: 'nothing', index: null }; + return { type: 'index', index: replicaIdentity.usingIndex }; +}; + export const policyFrom = (policy: PgPolicy, dialect: PgDialect) => { const mappedTo = !policy.to ? ['public'] @@ -307,6 +316,7 @@ export const fromDrizzleSchema = ( schema, name: config.name, isRlsEnabled, + replicaIdentity: replicaIdentityFrom(config.replicaIdentity), } satisfies PostgresEntities['tables']; }); diff --git a/drizzle-kit/src/dialects/postgres/duckdb-introspect.ts b/drizzle-kit/src/dialects/postgres/duckdb-introspect.ts index f2da726de6..efd22c9006 100644 --- a/drizzle-kit/src/dialects/postgres/duckdb-introspect.ts +++ b/drizzle-kit/src/dialects/postgres/duckdb-introspect.ts @@ -182,6 +182,7 @@ export const fromDatabase = async ( schema: trimChar(table.schema, "'"), name: table.name, isRlsEnabled: false, + replicaIdentity: null, }); } diff --git a/drizzle-kit/src/dialects/postgres/introspect.ts b/drizzle-kit/src/dialects/postgres/introspect.ts index bf375d9bff..8b236d1d4f 100644 --- a/drizzle-kit/src/dialects/postgres/introspect.ts +++ b/drizzle-kit/src/dialects/postgres/introspect.ts @@ -174,6 +174,9 @@ export const fromDatabase = async ( accessMethod: number | string; options: string[] | null; rlsEnabled: boolean; + /* d - default, n - nothing, f - full, i - index */ + replicaIdentity: 'd' | 'n' | 'f' | 'i'; + replicaIdentityIndex: string | null; tablespaceid: number | string; definition: string | null; }; @@ -190,6 +193,14 @@ export const fromDatabase = async ( reloptions::text[] as "options", reltablespace as "tablespaceid", relrowsecurity AS "rlsEnabled", + relreplident::text AS "replicaIdentity", + ( + SELECT ci.relname + FROM pg_catalog.pg_index i + JOIN pg_catalog.pg_class ci ON ci.oid OPERATOR(pg_catalog.=) i.indexrelid + WHERE i.indrelid OPERATOR(pg_catalog.=) pg_class.oid AND i.indisreplident + LIMIT 1 + ) AS "replicaIdentityIndex", CASE WHEN relkind OPERATOR(pg_catalog.=) 'v' OR relkind OPERATOR(pg_catalog.=) 'm' THEN pg_catalog.pg_get_viewdef(pg_class.oid, true) @@ -234,6 +245,13 @@ export const fromDatabase = async ( schema: trimChar(table.schema, "'"), name: table.name, isRlsEnabled: table.rlsEnabled, + replicaIdentity: table.replicaIdentity === 'f' + ? { type: 'full', index: null } + : table.replicaIdentity === 'n' + ? { type: 'nothing', index: null } + : table.replicaIdentity === 'i' + ? { type: 'index', index: table.replicaIdentityIndex } + : null, }); } progressCallback('tables', tables.length, 'done'); diff --git a/drizzle-kit/src/dialects/postgres/serializer.ts b/drizzle-kit/src/dialects/postgres/serializer.ts index b4e9d87e0c..0ac0e79e0c 100644 --- a/drizzle-kit/src/dialects/postgres/serializer.ts +++ b/drizzle-kit/src/dialects/postgres/serializer.ts @@ -159,6 +159,7 @@ export function generateLatestSnapshot( name: table.name, schema: table.schema, isRlsEnabled: table.isRlsEnabled, + replicaIdentity: table.replicaIdentity, }); for (const column of table.columns) { ddl.columns.push(column); @@ -647,6 +648,12 @@ export function generateLatestSnapshot( set: { isRlsEnabled: statement.isRlsEnabled }, }); break; + case 'alter_replica_identity': + ddl.tables.update({ + where: { schema: statement.schema, name: statement.name }, + set: { replicaIdentity: statement.replicaIdentity }, + }); + break; case 'create_role': push(ddl.roles, statement.role); diff --git a/drizzle-kit/src/dialects/postgres/statements.ts b/drizzle-kit/src/dialects/postgres/statements.ts index 12bb6badf5..0868c028af 100644 --- a/drizzle-kit/src/dialects/postgres/statements.ts +++ b/drizzle-kit/src/dialects/postgres/statements.ts @@ -7,6 +7,7 @@ import type { ForeignKey, Index, Policy, + PostgresEntities, PrimaryKey, Privilege, Role, @@ -188,6 +189,13 @@ export interface JsonAlterRLS { isRlsEnabled: boolean; } +export interface JsonAlterReplicaIdentity { + type: 'alter_replica_identity'; + schema: string; + name: string; + replicaIdentity: PostgresEntities['tables']['replicaIdentity']; +} + export interface JsonAlterPolicy { type: 'alter_policy'; diff: DiffEntities['policies']; @@ -431,6 +439,7 @@ export type JsonStatement = | JsonRecreatePolicy | JsonRenamePolicy | JsonAlterRLS + | JsonAlterReplicaIdentity | JsonRenameRole | JsonCreateRole | JsonDropRole diff --git a/drizzle-kit/src/dialects/postgres/typescript.ts b/drizzle-kit/src/dialects/postgres/typescript.ts index 543d017635..de7440d2ad 100644 --- a/drizzle-kit/src/dialects/postgres/typescript.ts +++ b/drizzle-kit/src/dialects/postgres/typescript.ts @@ -396,7 +396,13 @@ export const ddlToTypeScript = ( statement += createTableChecks(table.checks, casing); statement += ']'; } - statement += ');'; + statement += ')'; + if (table.replicaIdentity) { + statement += table.replicaIdentity.type === 'index' + ? `.replicaIdentity({ usingIndex: "${table.replicaIdentity.index}" })` + : `.replicaIdentity("${table.replicaIdentity.type}")`; + } + statement += ';'; return statement; }); diff --git a/drizzle-kit/src/dialects/postgres/versions.ts b/drizzle-kit/src/dialects/postgres/versions.ts index ab59090865..ab324b9093 100644 --- a/drizzle-kit/src/dialects/postgres/versions.ts +++ b/drizzle-kit/src/dialects/postgres/versions.ts @@ -61,6 +61,7 @@ export const upToV8 = ( schema, name: table.name, isRlsEnabled: isRlsEnabled, + replicaIdentity: null, }); for (const column of Object.values(table.columns)) { diff --git a/drizzle-kit/src/ext/studio-postgres.ts b/drizzle-kit/src/ext/studio-postgres.ts index 4e478ac7b3..0e447b3515 100644 --- a/drizzle-kit/src/ext/studio-postgres.ts +++ b/drizzle-kit/src/ext/studio-postgres.ts @@ -36,6 +36,7 @@ export type InterimTable = { pks: Interim[]; fks: Interim[]; isRlsEnabled: boolean; + replicaIdentity?: PostgresEntities['tables']['replicaIdentity']; }; export type InterimView = { @@ -72,6 +73,7 @@ const fromInterims = ({ name: it.name, schema: it.schema, isRlsEnabled: it.isRlsEnabled, + replicaIdentity: it.replicaIdentity ?? null, })); const columns: InterimColumn[] = tables .map((table) => { diff --git a/drizzle-kit/tests/cockroach/replica-identity.test.ts b/drizzle-kit/tests/cockroach/replica-identity.test.ts new file mode 100644 index 0000000000..3cb09a1e72 --- /dev/null +++ b/drizzle-kit/tests/cockroach/replica-identity.test.ts @@ -0,0 +1,71 @@ +import { cockroachTable, int4 } from 'drizzle-orm/cockroach-core'; +import { expect, test } from 'vitest'; +import { diff } from './mocks'; + +// These tests only exercise the pure (DB-free) diff path, so they don't require a CockroachDB instance. + +test('create table with replica identity full', async () => { + const to = { + users: cockroachTable('users', { + id: int4('id').primaryKey(), + }).replicaIdentity('full'), + }; + + const { sqlStatements: st } = await diff({}, to, []); + + expect(st.at(-1)).toBe('ALTER TABLE "users" REPLICA IDENTITY FULL;'); +}); + +test('alter replica identity default -> full', async () => { + const from = { + users: cockroachTable('users', { + id: int4('id').primaryKey(), + }), + }; + + const to = { + users: cockroachTable('users', { + id: int4('id').primaryKey(), + }).replicaIdentity('full'), + }; + + const { sqlStatements: st } = await diff(from, to, []); + + expect(st).toStrictEqual(['ALTER TABLE "users" REPLICA IDENTITY FULL;']); +}); + +test('alter replica identity full -> nothing', async () => { + const from = { + users: cockroachTable('users', { + id: int4('id').primaryKey(), + }).replicaIdentity('full'), + }; + + const to = { + users: cockroachTable('users', { + id: int4('id').primaryKey(), + }).replicaIdentity('nothing'), + }; + + const { sqlStatements: st } = await diff(from, to, []); + + expect(st).toStrictEqual(['ALTER TABLE "users" REPLICA IDENTITY NOTHING;']); +}); + +test('alter replica identity full -> default (reset)', async () => { + const from = { + users: cockroachTable('users', { + id: int4('id').primaryKey(), + }).replicaIdentity('full'), + }; + + const to = { + users: cockroachTable('users', { + id: int4('id').primaryKey(), + }), + }; + + const { sqlStatements: st } = await diff(from, to, []); + + expect(st).toStrictEqual(['ALTER TABLE "users" REPLICA IDENTITY DEFAULT;']); +}); diff --git a/drizzle-kit/tests/other/hints-probe-skip.test.ts b/drizzle-kit/tests/other/hints-probe-skip.test.ts index 70d9f135e5..58d3385a5a 100644 --- a/drizzle-kit/tests/other/hints-probe-skip.test.ts +++ b/drizzle-kit/tests/other/hints-probe-skip.test.ts @@ -38,6 +38,7 @@ const table = (name: string, schema = 'public') => checks: [], policies: [], isRlsEnabled: false, + replicaIdentity: null, }) satisfies Table; const column = (tableName: string, name: string, schema = 'public', overrides: Partial = {}) => @@ -325,10 +326,10 @@ test('rename hints resolve orders to orders1 before probes so no stale orders1 s ] satisfies readonly Hint[]; const ddlFrom = createDDL(); - ddlFrom.tables.push({ schema: 'public', name: 'orders', isRlsEnabled: false }); + ddlFrom.tables.push({ schema: 'public', name: 'orders', isRlsEnabled: false, replicaIdentity: null }); const ddlTo = createDDL(); - ddlTo.tables.push({ schema: 'public', name: 'orders1', isRlsEnabled: false }); + ddlTo.tables.push({ schema: 'public', name: 'orders1', isRlsEnabled: false, replicaIdentity: null }); const diffResult = await runPushDiff(ddlFrom, ddlTo, hintsInput); const statementTypes = diffResult.statements.map((statement) => statement.type); diff --git a/drizzle-kit/tests/postgres/commutativity.test.ts b/drizzle-kit/tests/postgres/commutativity.test.ts index f1bacf16aa..63aba1694e 100644 --- a/drizzle-kit/tests/postgres/commutativity.test.ts +++ b/drizzle-kit/tests/postgres/commutativity.test.ts @@ -38,7 +38,7 @@ function mkTmp(): { tmp: string; fs: any; path: any; os: any } { describe('commutativity integration (postgres)', () => { test('Parent not empty: detects conflict when first migration of branch A has a conflict with the last migration of branch B', async () => { const parentDDL = createDDL(); - parentDDL.tables.push({ schema: 'public', isRlsEnabled: false, name: 'users' }); + parentDDL.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'users' }); parentDDL.columns.push({ schema: 'public', table: 'users', @@ -55,7 +55,7 @@ describe('commutativity integration (postgres)', () => { const parent = makeSnapshot('p1', [baseId], parentDDL.entities.list()); const A = createDDL(); - A.tables.push({ schema: 'public', isRlsEnabled: false, name: 'users' }); + A.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'users' }); A.columns.push({ schema: 'public', table: 'users', @@ -72,7 +72,7 @@ describe('commutativity integration (postgres)', () => { const leafA = makeSnapshot('a1', ['p1'], A.entities.list()); const A2 = createDDL(); - A2.tables.push({ schema: 'public', isRlsEnabled: false, name: 'users' }); + A2.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'users' }); A2.columns.push({ schema: 'public', table: 'users', @@ -89,7 +89,7 @@ describe('commutativity integration (postgres)', () => { const leafA2 = makeSnapshot('a2', ['a1'], A2.entities.list()); const B = createDDL(); - B.tables.push({ schema: 'public', isRlsEnabled: false, name: 'users' }); + B.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'users' }); B.columns.push({ schema: 'public', table: 'users', @@ -103,7 +103,7 @@ describe('commutativity integration (postgres)', () => { generated: null, identity: null, } as any); - B.tables.push({ schema: 'public', isRlsEnabled: false, name: 'posts' }); + B.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'posts' }); B.columns.push({ schema: 'public', table: 'posts', @@ -120,7 +120,7 @@ describe('commutativity integration (postgres)', () => { const leafB = makeSnapshot('b1', ['p1'], B.entities.list()); const B2 = createDDL(); - B2.tables.push({ schema: 'public', isRlsEnabled: false, name: 'users' }); + B2.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'users' }); B2.columns.push({ schema: 'public', table: 'users', @@ -134,7 +134,7 @@ describe('commutativity integration (postgres)', () => { generated: null, identity: null, } as any); - B2.tables.push({ schema: 'public', isRlsEnabled: false, name: 'posts' }); + B2.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'posts' }); B2.columns.push({ schema: 'public', table: 'posts', @@ -151,7 +151,7 @@ describe('commutativity integration (postgres)', () => { const leafB2 = makeSnapshot('b2', ['b1'], B2.entities.list()); const B3 = createDDL(); - B3.tables.push({ schema: 'public', isRlsEnabled: false, name: 'posts' }); + B3.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'posts' }); B3.columns.push({ schema: 'public', table: 'posts', @@ -185,7 +185,7 @@ describe('commutativity integration (postgres)', () => { const parent = makeSnapshot('p1', [baseId], createDDL().entities.list()); const A = createDDL(); - A.tables.push({ schema: 'public', isRlsEnabled: false, name: 'users' }); + A.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'users' }); A.columns.push({ schema: 'public', table: 'users', @@ -202,7 +202,7 @@ describe('commutativity integration (postgres)', () => { const leafA = makeSnapshot('a1', ['p1'], A.entities.list()); const A2 = createDDL(); - A2.tables.push({ schema: 'public', isRlsEnabled: false, name: 'posts' }); + A2.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'posts' }); A2.columns.push({ schema: 'public', table: 'posts', @@ -219,7 +219,7 @@ describe('commutativity integration (postgres)', () => { const leafA2 = makeSnapshot('a2', ['a1'], A2.entities.list()); const B = createDDL(); - B.tables.push({ schema: 'public', isRlsEnabled: false, name: 'posts' }); + B.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'posts' }); B.columns.push({ schema: 'public', table: 'users', @@ -236,7 +236,7 @@ describe('commutativity integration (postgres)', () => { const leafB = makeSnapshot('b1', ['p1'], B.entities.list()); const B2 = createDDL(); - B2.tables.push({ schema: 'public', isRlsEnabled: false, name: 'posts' }); + B2.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'posts' }); B2.columns.push({ schema: 'public', table: 'users', @@ -253,7 +253,7 @@ describe('commutativity integration (postgres)', () => { const leafB2 = makeSnapshot('b2', ['b1'], B2.entities.list()); const B3 = createDDL(); - B3.tables.push({ schema: 'public', isRlsEnabled: false, name: 'posts' }); + B3.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'posts' }); B3.columns.push({ schema: 'public', table: 'users', @@ -267,7 +267,7 @@ describe('commutativity integration (postgres)', () => { generated: null, identity: null, } as any); - B3.tables.push({ schema: 'public', isRlsEnabled: false, name: 'media' }); + B3.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'media' }); B3.columns.push({ schema: 'public', table: 'media', @@ -299,7 +299,7 @@ describe('commutativity integration (postgres)', () => { test('detects conflict when drop table in one branch and add column in other', async () => { const parentDDL = createDDL(); - parentDDL.tables.push({ schema: 'public', isRlsEnabled: false, name: 'users' }); + parentDDL.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'users' }); parentDDL.columns.push({ schema: 'public', table: 'users', @@ -316,7 +316,7 @@ describe('commutativity integration (postgres)', () => { const parent = makeSnapshot('p1', [baseId], parentDDL.entities.list()); const A = createDDL(); - A.tables.push({ schema: 'public', isRlsEnabled: false, name: 'users' }); + A.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'users' }); A.columns.push({ schema: 'public', table: 'users', @@ -349,7 +349,7 @@ describe('commutativity integration (postgres)', () => { const parent = makeSnapshot('p1', [baseId], createDDL().entities.list()); const A = createDDL(); - A.tables.push({ schema: 'public', isRlsEnabled: false, name: 'users' }); + A.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'users' }); A.columns.push({ schema: 'public', table: 'users', @@ -366,7 +366,7 @@ describe('commutativity integration (postgres)', () => { const leafA = makeSnapshot('a1', ['p1'], A.entities.list()); const B = createDDL(); - B.tables.push({ schema: 'public', isRlsEnabled: false, name: 'users' }); + B.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'users' }); B.columns.push({ schema: 'public', table: 'users', @@ -397,11 +397,11 @@ describe('commutativity integration (postgres)', () => { const parent = makeSnapshot('p2', [baseId], createDDL().entities.list()); const A = createDDL(); - A.tables.push({ schema: 'public', isRlsEnabled: false, name: 'users' }); + A.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'users' }); const leafA = makeSnapshot('a2', ['p2'], A.entities.list()); const B = createDDL(); - B.tables.push({ schema: 'public', isRlsEnabled: false, name: 'posts' }); + B.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'posts' }); const leafB = makeSnapshot('b2', ['p2'], B.entities.list()); const os = require('os'); @@ -419,11 +419,11 @@ describe('commutativity integration (postgres)', () => { const files: string[] = []; const parent = createDDL(); - parent.tables.push({ schema: 'public', isRlsEnabled: false, name: 'users' }); + parent.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'users' }); const p = makeSnapshot('p_col', [ORIGIN], parent.entities.list()); const a = createDDL(); - a.tables.push({ schema: 'public', isRlsEnabled: false, name: 'users' }); + a.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'users' }); a.columns.push( { schema: 'public', @@ -440,7 +440,7 @@ describe('commutativity integration (postgres)', () => { } as any, ); const b = createDDL(); - b.tables.push({ schema: 'public', isRlsEnabled: false, name: 'users' }); + b.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'users' }); b.columns.push( { schema: 'public', @@ -472,7 +472,7 @@ describe('commutativity integration (postgres)', () => { const files: string[] = []; const parent = createDDL(); - parent.tables.push({ schema: 'public', isRlsEnabled: false, name: 't1' }); + parent.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't1' }); parent.columns.push( { schema: 'public', @@ -492,7 +492,7 @@ describe('commutativity integration (postgres)', () => { const a = createDDL(); // dropping table in branch A (no t1) const b = createDDL(); - b.tables.push({ schema: 'public', isRlsEnabled: false, name: 't1' }); + b.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't1' }); b.columns.push( { schema: 'public', @@ -532,7 +532,7 @@ describe('commutativity integration (postgres)', () => { const parent = createDDL(); parent.schemas.push({ name: 'app' } as any); - parent.tables.push({ schema: 'app', isRlsEnabled: false, name: 'users' } as any); + parent.tables.push({ schema: 'app', isRlsEnabled: false, replicaIdentity: null, name: 'users' } as any); parent.columns.push({ schema: 'app', table: 'users', @@ -552,8 +552,8 @@ describe('commutativity integration (postgres)', () => { const b = createDDL(); b.schemas.push({ name: 'app' } as any); - b.tables.push({ schema: 'app', isRlsEnabled: false, name: 'users' } as any); - b.tables.push({ schema: 'app', isRlsEnabled: false, name: 'profiles' } as any); + b.tables.push({ schema: 'app', isRlsEnabled: false, replicaIdentity: null, name: 'users' } as any); + b.tables.push({ schema: 'app', isRlsEnabled: false, replicaIdentity: null, name: 'profiles' } as any); b.columns.push( { schema: 'app', @@ -598,8 +598,8 @@ describe('commutativity integration (postgres)', () => { const files: string[] = []; const parent = createDDL(); - parent.tables.push({ schema: 'public', isRlsEnabled: false, name: 'orders' }); - parent.tables.push({ schema: 'public', isRlsEnabled: false, name: 'invoices' }); + parent.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'orders' }); + parent.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'invoices' }); parent.columns.push( { schema: 'public', @@ -631,8 +631,8 @@ describe('commutativity integration (postgres)', () => { const p = makeSnapshot('p_idx', [ORIGIN], parent.entities.list()); const a = createDDL(); - a.tables.push({ schema: 'public', isRlsEnabled: false, name: 'orders' }); - a.tables.push({ schema: 'public', isRlsEnabled: false, name: 'invoices' }); + a.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'orders' }); + a.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'invoices' }); a.columns.push( { schema: 'public', @@ -675,8 +675,8 @@ describe('commutativity integration (postgres)', () => { } as any); const b = createDDL(); - b.tables.push({ schema: 'public', isRlsEnabled: false, name: 'orders' }); - b.tables.push({ schema: 'public', isRlsEnabled: false, name: 'invoices' }); + b.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'orders' }); + b.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'invoices' }); b.columns.push( { schema: 'public', @@ -733,11 +733,11 @@ describe('commutativity integration (postgres)', () => { const files: string[] = []; const parent = createDDL(); - parent.tables.push({ schema: 'public', isRlsEnabled: false, name: 't2' }); + parent.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't2' }); const p = makeSnapshot('p_uq', [ORIGIN], parent.entities.list()); const a = createDDL(); - a.tables.push({ schema: 'public', isRlsEnabled: false, name: 't2' }); + a.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't2' }); a.uniques.push( { schema: 'public', @@ -749,7 +749,7 @@ describe('commutativity integration (postgres)', () => { } as any, ); const b = createDDL(); - b.tables.push({ schema: 'public', isRlsEnabled: false, name: 't2' }); + b.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't2' }); b.uniques.push( { schema: 'public', @@ -880,11 +880,11 @@ describe('commutativity integration (postgres)', () => { const files: string[] = []; const parent = createDDL(); - parent.tables.push({ schema: 'public', isRlsEnabled: false, name: 't3' }); + parent.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't3' }); const p = makeSnapshot('p_pol', [ORIGIN], parent.entities.list()); const a = createDDL(); - a.tables.push({ schema: 'public', isRlsEnabled: false, name: 't3' }); + a.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't3' }); a.policies.push( { schema: 'public', @@ -898,7 +898,7 @@ describe('commutativity integration (postgres)', () => { } as any, ); const b = createDDL(); - b.tables.push({ schema: 'public', isRlsEnabled: false, name: 't3' }); + b.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't3' }); b.policies.push( { schema: 'public', @@ -927,11 +927,11 @@ describe('commutativity integration (postgres)', () => { const files: string[] = []; const parent = createDDL(); - parent.tables.push({ schema: 'public', isRlsEnabled: false, name: 't_rls' }); + parent.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't_rls' }); const p = makeSnapshot('p_rls', [ORIGIN], parent.entities.list()); const a = createDDL(); - a.tables.push({ schema: 'public', isRlsEnabled: true, name: 't_rls' }); + a.tables.push({ schema: 'public', isRlsEnabled: true, replicaIdentity: null, name: 't_rls' }); a.policies.push( { schema: 'public', @@ -962,11 +962,11 @@ describe('commutativity integration (postgres)', () => { const files: string[] = []; const parent = createDDL(); - parent.tables.push({ schema: 'public', isRlsEnabled: false, name: 't' }); + parent.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't' }); const p = makeSnapshot('p_three', [ORIGIN], parent.entities.list()); const a = createDDL(); - a.tables.push({ schema: 'public', isRlsEnabled: false, name: 't' }); + a.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't' }); a.columns.push( { schema: 'public', @@ -983,7 +983,7 @@ describe('commutativity integration (postgres)', () => { } as any, ); const b = createDDL(); - b.tables.push({ schema: 'public', isRlsEnabled: false, name: 't' }); + b.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't' }); b.columns.push( { schema: 'public', @@ -1000,7 +1000,7 @@ describe('commutativity integration (postgres)', () => { } as any, ); const c = createDDL(); - c.tables.push({ schema: 'public', isRlsEnabled: false, name: 't' }); + c.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't' }); c.columns.push( { schema: 'public', @@ -1034,11 +1034,11 @@ describe('commutativity integration (postgres)', () => { const files: string[] = []; const root = createDDL(); - root.tables.push({ schema: 'public', isRlsEnabled: false, name: 't' }); + root.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't' }); const p = makeSnapshot('p_nested', [ORIGIN], root.entities.list()); const A = createDDL(); - A.tables.push({ schema: 'public', isRlsEnabled: false, name: 't' }); + A.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't' }); A.columns.push( { schema: 'public', @@ -1055,7 +1055,7 @@ describe('commutativity integration (postgres)', () => { } as any, ); const A1 = createDDL(); - A1.tables.push({ schema: 'public', isRlsEnabled: false, name: 't' }); + A1.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't' }); A1.columns.push( { schema: 'public', @@ -1072,7 +1072,7 @@ describe('commutativity integration (postgres)', () => { } as any, ); const B = createDDL(); - B.tables.push({ schema: 'public', isRlsEnabled: false, name: 't' }); + B.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 't' }); B.columns.push( { schema: 'public', @@ -1106,13 +1106,13 @@ describe('commutativity integration (postgres)', () => { const files: string[] = []; const base = createDDL(); - base.tables.push({ schema: 'public', isRlsEnabled: false, name: 'u' }); - base.tables.push({ schema: 'public', isRlsEnabled: false, name: 'p' }); + base.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'u' }); + base.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'p' }); const p = makeSnapshot('p_mix', [ORIGIN], base.entities.list()); // Branch X: alter u.email, create view v_users, enum e1 const X = createDDL(); - X.tables.push({ schema: 'public', isRlsEnabled: false, name: 'u' }); + X.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'u' }); X.columns.push( { schema: 'public', @@ -1144,7 +1144,7 @@ describe('commutativity integration (postgres)', () => { // Branch Y: drop table u (conflicts with X's column/view touching u), policy on p const Y = createDDL(); - Y.tables.push({ schema: 'public', isRlsEnabled: false, name: 'p' }); + Y.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'p' }); Y.policies.push( { schema: 'public', @@ -1175,28 +1175,28 @@ describe('commutativity integration (postgres)', () => { const base = createDDL(); base.schemas.push({ name: 's1' } as any); - base.tables.push({ schema: 's1', isRlsEnabled: false, name: 't1' } as any); - base.tables.push({ schema: 'public', isRlsEnabled: false, name: 'common_table' } as any); + base.tables.push({ schema: 's1', isRlsEnabled: false, replicaIdentity: null, name: 't1' } as any); + base.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'common_table' } as any); const p = makeSnapshot('p_schema_move', [ORIGIN], base.entities.list()); // Branch A: rename schema s1 to s2, move t1 from s1 to s2.t1 const A = createDDL(); A.schemas.push({ name: 's2' } as any); - A.tables.push({ schema: 's2', isRlsEnabled: false, name: 't1' } as any); - A.tables.push({ schema: 'public', isRlsEnabled: false, name: 'common_table' } as any); + A.tables.push({ schema: 's2', isRlsEnabled: false, replicaIdentity: null, name: 't1' } as any); + A.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'common_table' } as any); // Branch B: drop schema s1, create table in public schema const B = createDDL(); - B.tables.push({ schema: 'public', isRlsEnabled: false, name: 'new_table_in_public' } as any); - B.tables.push({ schema: 'public', isRlsEnabled: false, name: 'common_table' } as any); + B.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'new_table_in_public' } as any); + B.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'common_table' } as any); // implicitly drops schema s1 and t1 within it // Branch C: alter common_table in public, create new schema s3 const C = createDDL(); C.schemas.push({ name: 's1' } as any); C.schemas.push({ name: 's3' } as any); - C.tables.push({ schema: 's1', isRlsEnabled: false, name: 't1' } as any); - C.tables.push({ schema: 'public', isRlsEnabled: false, name: 'common_table' } as any); + C.tables.push({ schema: 's1', isRlsEnabled: false, replicaIdentity: null, name: 't1' } as any); + C.tables.push({ schema: 'public', isRlsEnabled: false, replicaIdentity: null, name: 'common_table' } as any); C.columns.push({ schema: 'public', table: 'common_table', name: 'new_col', type: 'text' } as any); files.push( diff --git a/drizzle-kit/tests/postgres/pg-tables.test.ts b/drizzle-kit/tests/postgres/pg-tables.test.ts index 80aa943d3d..49b3a0cc8a 100644 --- a/drizzle-kit/tests/postgres/pg-tables.test.ts +++ b/drizzle-kit/tests/postgres/pg-tables.test.ts @@ -1568,3 +1568,111 @@ test('rename table with identity column', async () => { expect(st).toStrictEqual(expectedSt); expect(pst).toStrictEqual(expectedSt); }); + +test('create table with replica identity full', async () => { + const to = { + users: pgTable('users', { + id: integer().primaryKey(), + }).replicaIdentity('full'), + }; + + const { sqlStatements: st } = await diff({}, to, []); + const { sqlStatements: pst } = await push({ db, to }); + + const st0 = [ + 'CREATE TABLE "users" (\n\t"id" integer PRIMARY KEY\n);\n', + 'ALTER TABLE "users" REPLICA IDENTITY FULL;', + ]; + expect(st).toStrictEqual(st0); + expect(pst).toStrictEqual(st0); +}); + +test('alter replica identity default -> full', async () => { + const from = { + users: pgTable('users', { + id: integer().primaryKey(), + }), + }; + + const to = { + users: pgTable('users', { + id: integer().primaryKey(), + }).replicaIdentity('full'), + }; + + const { sqlStatements: st } = await diff(from, to, []); + + await push({ db, to: from }); + const { sqlStatements: pst } = await push({ db, to }); + + const st0 = ['ALTER TABLE "users" REPLICA IDENTITY FULL;']; + expect(st).toStrictEqual(st0); + expect(pst).toStrictEqual(st0); +}); + +test('alter replica identity full -> nothing', async () => { + const from = { + users: pgTable('users', { + id: integer().primaryKey(), + }).replicaIdentity('full'), + }; + + const to = { + users: pgTable('users', { + id: integer().primaryKey(), + }).replicaIdentity('nothing'), + }; + + const { sqlStatements: st } = await diff(from, to, []); + + await push({ db, to: from }); + const { sqlStatements: pst } = await push({ db, to }); + + const st0 = ['ALTER TABLE "users" REPLICA IDENTITY NOTHING;']; + expect(st).toStrictEqual(st0); + expect(pst).toStrictEqual(st0); +}); + +test('alter replica identity full -> default (reset)', async () => { + const from = { + users: pgTable('users', { + id: integer().primaryKey(), + }).replicaIdentity('full'), + }; + + const to = { + users: pgTable('users', { + id: integer().primaryKey(), + }), + }; + + const { sqlStatements: st } = await diff(from, to, []); + + await push({ db, to: from }); + const { sqlStatements: pst } = await push({ db, to }); + + const st0 = ['ALTER TABLE "users" REPLICA IDENTITY DEFAULT;']; + expect(st).toStrictEqual(st0); + expect(pst).toStrictEqual(st0); +}); + +test('create table with replica identity using index', async () => { + const to = { + users: pgTable('users', { + id: integer().primaryKey(), + email: text().notNull(), + }, (t) => [uniqueIndex('users_email_idx').on(t.email)]).replicaIdentity({ usingIndex: 'users_email_idx' }), + }; + + const { sqlStatements: st } = await diff({}, to, []); + const { sqlStatements: pst } = await push({ db, to }); + + const alterSql = 'ALTER TABLE "users" REPLICA IDENTITY USING INDEX "users_email_idx";'; + const indexIdx = st.findIndex((s) => /CREATE UNIQUE INDEX "users_email_idx"/.test(s)); + + // the ALTER ... USING INDEX statement must come after the index is created + expect(st.at(-1)).toBe(alterSql); + expect(indexIdx).toBeGreaterThanOrEqual(0); + expect(indexIdx).toBeLessThan(st.indexOf(alterSql)); + expect(pst).toStrictEqual(st); +}); diff --git a/drizzle-kit/tests/postgres/pull.test.ts b/drizzle-kit/tests/postgres/pull.test.ts index d7d6f3aed3..ef2e131408 100644 --- a/drizzle-kit/tests/postgres/pull.test.ts +++ b/drizzle-kit/tests/postgres/pull.test.ts @@ -1633,6 +1633,7 @@ test('introspect partitioned tables', async () => { schema: 'public', entityType: 'tables', isRlsEnabled: false, + replicaIdentity: null, } satisfies (typeof tables)[number], ]); }); @@ -1752,6 +1753,7 @@ test('introspect view with table filter', async () => { schema: 'public', name: 'table1', isRlsEnabled: false, + replicaIdentity: null, }, ]; expect(tables).toStrictEqual(expectedTables); @@ -1833,6 +1835,7 @@ test.skipIf(Date.now() < +new Date('2026-06-20'))('introspect sequences with tab schema: 'public', name: 'table1', isRlsEnabled: false, + replicaIdentity: null, }, ]); expect(sequences).toBe([ @@ -2024,6 +2027,7 @@ test('introspect enum within schema', async () => { schema: 'public', name: 'table1', isRlsEnabled: false, + replicaIdentity: null, }, ]); expect(enums).toStrictEqual([]); @@ -2206,6 +2210,7 @@ test('pull after migrate with custom migrations table #1', async () => { isRlsEnabled: false, name: 'users', schema: 'drizzle', + replicaIdentity: null, }, { columns: ['id'], @@ -2261,6 +2266,7 @@ test('pull after migrate with custom migrations table #2', async () => { isRlsEnabled: false, name: 'users', schema: 'public', + replicaIdentity: null, }, { columns: ['id'], @@ -2326,12 +2332,14 @@ test('pull after migrate with custom migrations table #3', async () => { isRlsEnabled: false, name: 'users', schema: 'custom', + replicaIdentity: null, }, { entityType: 'tables', isRlsEnabled: false, name: 'users', schema: 'public', + replicaIdentity: null, }, { columns: ['id'], @@ -2809,6 +2817,7 @@ test('non-admin', async () => { schema: 'public', name: 'users', isRlsEnabled: false, + replicaIdentity: null, }, ]); }); diff --git a/drizzle-orm/src/cockroach-core/table.ts b/drizzle-orm/src/cockroach-core/table.ts index c74177e4e4..7245bf5c58 100644 --- a/drizzle-orm/src/cockroach-core/table.ts +++ b/drizzle-orm/src/cockroach-core/table.ts @@ -36,10 +36,22 @@ export type CockroachTableExtraConfig = Record< export type TableConfig = TableConfigBase; +/** + * Configures the `REPLICA IDENTITY` of a table. + * + * - `'default'` - records the old values of the columns of the primary key, if any (the database default). + * - `'full'` - records the old values of all columns in the row. + * - `'nothing'` - records no information about the old row. + * - `{ usingIndex: string }` - records the old values of the columns covered by the named index (`USING INDEX`). + */ +export type CockroachReplicaIdentity = 'default' | 'full' | 'nothing' | { usingIndex: string }; + /** @internal */ export const InlineForeignKeys = Symbol.for('drizzle:CockroachInlineForeignKeys'); /** @internal */ export const EnableRLS = Symbol.for('drizzle:EnableRLS'); +/** @internal */ +export const ReplicaIdentity = Symbol.for('drizzle:ReplicaIdentity'); export class CockroachTable extends Table { static override readonly [entityKind]: string = 'CockroachTable'; @@ -48,6 +60,7 @@ export class CockroachTable extends Table extends Table) => CockroachTableExtraConfig) @@ -79,6 +95,19 @@ export type CockroachTableWithColumns = CockroachTableWithColumns, 'enableRLS' >; + } + & { + /** + * Sets the `REPLICA IDENTITY` for the table. + * + * @example + * ```ts + * export const users = cockroachTable('users', { + * id: integer().primaryKey(), + * }).replicaIdentity('full'); + * ``` + */ + replicaIdentity: (identity: CockroachReplicaIdentity) => CockroachTableWithColumns; }; /** @internal */ @@ -151,6 +180,15 @@ export function cockroachTableWithSchema< dialect: 'cockroach'; }>; }, + replicaIdentity: (identity: CockroachReplicaIdentity) => { + table[CockroachTable.Symbol.ReplicaIdentity] = identity; + return table as CockroachTableWithColumns<{ + name: TTableName; + schema: TSchemaName; + columns: BuildColumns; + dialect: 'cockroach'; + }>; + }, }) as any; } diff --git a/drizzle-orm/src/cockroach-core/utils.ts b/drizzle-orm/src/cockroach-core/utils.ts index 2d6d137cd3..fe13eabaaf 100644 --- a/drizzle-orm/src/cockroach-core/utils.ts +++ b/drizzle-orm/src/cockroach-core/utils.ts @@ -23,6 +23,7 @@ export function getTableConfig(table: TTable) { const schema = table[Table.Symbol.Schema]; const policies: CockroachPolicy[] = []; const enableRLS: boolean = table[CockroachTable.Symbol.EnableRLS]; + const replicaIdentity = table[CockroachTable.Symbol.ReplicaIdentity]; const extraConfigBuilder = table[CockroachTable.Symbol.ExtraConfigBuilder]; @@ -57,6 +58,7 @@ export function getTableConfig(table: TTable) { schema, policies, enableRLS, + replicaIdentity, }; } diff --git a/drizzle-orm/src/pg-core/table.ts b/drizzle-orm/src/pg-core/table.ts index 8522fb2562..aaad868bca 100644 --- a/drizzle-orm/src/pg-core/table.ts +++ b/drizzle-orm/src/pg-core/table.ts @@ -31,10 +31,22 @@ export type PgTableExtraConfig = Record; export type TableConfig = TableConfigBase; +/** + * Configures the `REPLICA IDENTITY` of a Postgres table. + * + * - `'default'` - records the old values of the columns of the primary key, if any (the database default). + * - `'full'` - records the old values of all columns in the row. + * - `'nothing'` - records no information about the old row. + * - `{ usingIndex: string }` - records the old values of the columns covered by the named index (`USING INDEX`). + */ +export type PgReplicaIdentity = 'default' | 'full' | 'nothing' | { usingIndex: string }; + /** @internal */ export const InlineForeignKeys = Symbol.for('drizzle:PgInlineForeignKeys'); /** @internal */ export const EnableRLS = Symbol.for('drizzle:EnableRLS'); +/** @internal */ +export const ReplicaIdentity = Symbol.for('drizzle:ReplicaIdentity'); export class PgTable extends Table { static override readonly [entityKind]: string = 'PgTable'; @@ -43,6 +55,7 @@ export class PgTable extends Table { static override readonly Symbol = Object.assign({}, Table.Symbol, { InlineForeignKeys: InlineForeignKeys as typeof InlineForeignKeys, EnableRLS: EnableRLS as typeof EnableRLS, + ReplicaIdentity: ReplicaIdentity as typeof ReplicaIdentity, }); /**@internal */ @@ -51,6 +64,9 @@ export class PgTable extends Table { /** @internal */ [EnableRLS]: boolean = false; + /** @internal */ + [ReplicaIdentity]: PgReplicaIdentity | undefined = undefined; + /** @internal */ override [Table.Symbol.ExtraConfigBuilder]: ((self: Record) => PgTableExtraConfig) | undefined = undefined; @@ -74,6 +90,23 @@ export type PgTableWithColumns = PgTableWithColumns, 'enableRLS' >; + } + & { + /** + * Sets the `REPLICA IDENTITY` for the table. + * + * @example + * ```ts + * export const users = pgTable('users', { + * id: integer().primaryKey(), + * }).replicaIdentity('full'); + * + * export const posts = pgTable('posts', { + * id: integer().primaryKey(), + * }).replicaIdentity({ usingIndex: 'posts_some_index' }); + * ``` + */ + replicaIdentity: (identity: PgReplicaIdentity) => PgTableWithColumns; }; /** @internal */ @@ -144,6 +177,15 @@ export function pgTableWithSchema< dialect: 'pg'; }>; }, + replicaIdentity: (identity: PgReplicaIdentity) => { + table[PgTable.Symbol.ReplicaIdentity] = identity; + return table as PgTableWithColumns<{ + name: TTableName; + schema: TSchemaName; + columns: PgBuildColumns; + dialect: 'pg'; + }>; + }, }) as any; } export interface PgTableFnInternal { diff --git a/drizzle-orm/src/pg-core/utils.ts b/drizzle-orm/src/pg-core/utils.ts index de36464740..4908baccba 100644 --- a/drizzle-orm/src/pg-core/utils.ts +++ b/drizzle-orm/src/pg-core/utils.ts @@ -29,6 +29,7 @@ export function getTableConfig(table: TTable) { const schema = table[Table.Symbol.Schema]; const policies: PgPolicy[] = []; const enableRLS: boolean = table[PgTable.Symbol.EnableRLS]; + const replicaIdentity = table[PgTable.Symbol.ReplicaIdentity]; const extraConfigBuilder = table[PgTable.Symbol.ExtraConfigBuilder]; @@ -63,6 +64,7 @@ export function getTableConfig(table: TTable) { schema, policies, enableRLS, + replicaIdentity, }; } diff --git a/integration-tests/tests/pg/utils.test.ts b/integration-tests/tests/pg/utils.test.ts index fb0591fea1..7fc06675d2 100644 --- a/integration-tests/tests/pg/utils.test.ts +++ b/integration-tests/tests/pg/utils.test.ts @@ -452,3 +452,26 @@ test('Enable RLS function', () => { expect(config1.enableRLS).toBeTruthy(); expect(config2.enableRLS).toBeFalsy(); }); + +test('replicaIdentity', () => { + const usersFull = pgTable('users', { + id: integer(), + }).replicaIdentity('full'); + + const usersNothing = pgTable('users', { + id: integer(), + }).replicaIdentity('nothing'); + + const usersIndex = pgTable('users', { + id: integer(), + }).replicaIdentity({ usingIndex: 'users_some_idx' }); + + const usersDefault = pgTable('users', { + id: integer(), + }); + + expect(getTableConfig(usersFull).replicaIdentity).toBe('full'); + expect(getTableConfig(usersNothing).replicaIdentity).toBe('nothing'); + expect(getTableConfig(usersIndex).replicaIdentity).toStrictEqual({ usingIndex: 'users_some_idx' }); + expect(getTableConfig(usersDefault).replicaIdentity).toBeUndefined(); +});