Skip to content

Commit 7c6fd61

Browse files
committed
commutative fixes
1 parent 1a01082 commit 7c6fd61

File tree

8 files changed

+196
-37
lines changed

8 files changed

+196
-37
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
More fixes for the drizzle-kit migration process and commutativity checks
2+
3+
- Adding a value to a PostgreSQL enum will no longer be treated as commutative
4+
- Enum values added in different leaves will now be merged properly

drizzle-kit/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "drizzle-kit",
3-
"version": "1.0.0-beta.20",
3+
"version": "1.0.0-beta.21",
44
"homepage": "https://orm.drizzle.team",
55
"keywords": [
66
"drizzle",

drizzle-kit/src/dialects/postgres/commutativity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ const footprintMap: Record<JsonStatement['type'], JsonStatement['type'][]> = {
145145
'create_enum',
146146
'drop_enum',
147147
'rename_enum',
148-
'alter_enum',
148+
// 'alter_enum',
149149
'recreate_enum',
150150
'move_enum',
151151
'alter_type_drop_value',

drizzle-kit/src/dialects/postgres/serializer.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -545,11 +545,47 @@ export function generateLatestSnapshot(
545545
},
546546
});
547547
break;
548-
case 'alter_enum':
549-
replace(ddl.enums, statement.from, statement.to);
548+
case 'alter_enum': {
549+
// When merging commutative branches, multiple alter_enum statements
550+
// for the same enum may be applied sequentially. Each statement's
551+
// `from`/`to` reflects the diff from the common parent, so the 2nd
552+
// statement's `to` wouldn't include values added by the 1st branch.
553+
// Instead of replacing the whole enum, we apply the diff incrementally:
554+
// find the current enum by identity (name + schema) and merge in the
555+
// added values from this statement's diff.
556+
const identity = {
557+
name: statement.from.name,
558+
schema: statement.from.schema,
559+
};
560+
const existing = ddl.enums.one(identity);
561+
if (!existing) {
562+
push(ddl.enums, statement.to);
563+
break;
564+
}
565+
566+
const values = [...existing.values];
567+
for (const d of statement.diff) {
568+
if (d.type !== 'added' || values.includes(d.value)) continue;
569+
570+
const insertAt = d.beforeValue !== undefined ? values.indexOf(d.beforeValue) : -1;
571+
if (insertAt !== -1) {
572+
values.splice(insertAt, 0, d.value);
573+
} else {
574+
values.push(d.value);
575+
}
576+
}
577+
ddl.enums.update({ where: identity, set: { values } });
550578
break;
579+
}
551580
case 'recreate_enum':
552-
replace(ddl.enums, statement.from, statement.to);
581+
// recreate_enum is used when values are removed, which requires a full
582+
// replacement. Delete by identity to handle the case where a prior
583+
// commutative branch may have already modified the enum values.
584+
ddl.enums.delete({
585+
name: statement.from.name,
586+
schema: statement.from.schema,
587+
});
588+
push(ddl.enums, statement.to);
553589
break;
554590
case 'alter_type_drop_value':
555591
ddl.enums.update({

drizzle-kit/tests/postgres/serializer-snapshot.test.ts

Lines changed: 148 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,7 @@ describe('transition tests', () => {
161161
age: integer('age'),
162162
test: integer('test'),
163163
},
164-
(
165-
t,
166-
) => [
164+
(t) => [
167165
check('age_check', sql`${t.age} > 18`),
168166
index('users_name_idx').on(t.name),
169167
unique().on(t.test),
@@ -189,9 +187,7 @@ describe('transition tests', () => {
189187
age: integer('age'),
190188
test: integer('test'),
191189
},
192-
(
193-
t,
194-
) => [
190+
(t) => [
195191
check('age_check', sql`${t.age} > 18`),
196192
index('users_name_idx').on(t.name),
197193
unique().on(t.test),
@@ -278,9 +274,7 @@ describe('transition tests', () => {
278274
age: integer('age'),
279275
test: integer('test'),
280276
},
281-
(
282-
t,
283-
) => [
277+
(t) => [
284278
check('age_check', sql`${t.age} > 18`),
285279
index('users_name_idx').on(t.name),
286280
unique().on(t.test),
@@ -307,9 +301,7 @@ describe('transition tests', () => {
307301
age: integer('age'),
308302
test: integer('test'),
309303
},
310-
(
311-
t,
312-
) => [
304+
(t) => [
313305
check('age_check', sql`${t.age} > 18`),
314306
index('users_name_idx').on(t.name),
315307
unique().on(t.test),
@@ -362,9 +354,7 @@ describe('transition tests', () => {
362354
age: integer('age'),
363355
test: integer('test'),
364356
},
365-
(
366-
t,
367-
) => [
357+
(t) => [
368358
check('age_check', sql`${t.age} > 18`),
369359
index('users_name_idx').on(t.name),
370360
unique().on(t.test),
@@ -391,9 +381,7 @@ describe('transition tests', () => {
391381
age: integer('age'),
392382
test: integer('test'),
393383
},
394-
(
395-
t,
396-
) => [
384+
(t) => [
397385
check('age_check', sql`${t.age} > 18`),
398386
index('users_name_idx').on(t.name),
399387
unique().on(t.test),
@@ -411,7 +399,10 @@ describe('transition tests', () => {
411399
usersSubs: usersSubsTo,
412400
};
413401

414-
const expectedTypes: JsonStatement['type'][] = ['rename_table', 'move_table'];
402+
const expectedTypes: JsonStatement['type'][] = [
403+
'rename_table',
404+
'move_table',
405+
];
415406

416407
const { afterPatch, fromTs, statements } = await applyTransition({
417408
from,
@@ -570,7 +561,9 @@ describe('transition tests', () => {
570561
});
571562

572563
test('recreate_column', async () => {
573-
const from = { users: pgTable('users', { id: integer('id').generatedAlwaysAs(sql`13`) }) };
564+
const from = {
565+
users: pgTable('users', { id: integer('id').generatedAlwaysAs(sql`13`) }),
566+
};
574567
const to = {
575568
users: pgTable('users', { id: integer('id').generatedAlwaysAs(sql`15`) }),
576569
};
@@ -945,7 +938,11 @@ describe('transition tests', () => {
945938
}),
946939
};
947940

948-
const expectedTypes: JsonStatement['type'][] = ['alter_column', 'alter_column', 'drop_pk'];
941+
const expectedTypes: JsonStatement['type'][] = [
942+
'alter_column',
943+
'alter_column',
944+
'drop_pk',
945+
];
949946

950947
const { afterPatch, fromTs, statements } = await applyTransition({
951948
from,
@@ -1788,7 +1785,9 @@ describe('transition tests', () => {
17881785
const from = { users };
17891786
const to = {
17901787
users,
1791-
policy: pgPolicy('users_policy', { for: 'select', to: 'public' }).link(users),
1788+
policy: pgPolicy('users_policy', { for: 'select', to: 'public' }).link(
1789+
users,
1790+
),
17921791
};
17931792

17941793
const expectedTypes: JsonStatement['type'][] = ['create_policy'];
@@ -1815,7 +1814,9 @@ describe('transition tests', () => {
18151814

18161815
const from = {
18171816
users,
1818-
policy: pgPolicy('users_policy', { for: 'select', to: 'public' }).link(users),
1817+
policy: pgPolicy('users_policy', { for: 'select', to: 'public' }).link(
1818+
users,
1819+
),
18191820
};
18201821
const to = { users };
18211822

@@ -1843,11 +1844,16 @@ describe('transition tests', () => {
18431844

18441845
const from = {
18451846
users,
1846-
policy: pgPolicy('users_policy', { for: 'select', to: 'public' }).link(users),
1847+
policy: pgPolicy('users_policy', { for: 'select', to: 'public' }).link(
1848+
users,
1849+
),
18471850
};
18481851
const to = {
18491852
users,
1850-
policy: pgPolicy('users_policy_renamed', { for: 'select', to: 'public' }).link(users),
1853+
policy: pgPolicy('users_policy_renamed', {
1854+
for: 'select',
1855+
to: 'public',
1856+
}).link(users),
18511857
};
18521858

18531859
const expectedTypes: JsonStatement['type'][] = ['rename_policy'];
@@ -1874,11 +1880,19 @@ describe('transition tests', () => {
18741880

18751881
const from = {
18761882
users,
1877-
policy: pgPolicy('users_policy', { for: 'select', to: 'public', using: sql`` }).link(users),
1883+
policy: pgPolicy('users_policy', {
1884+
for: 'select',
1885+
to: 'public',
1886+
using: sql``,
1887+
}).link(users),
18781888
};
18791889
const to = {
18801890
users,
1881-
policy: pgPolicy('users_policy', { for: 'select', to: 'public', using: sql`1` }).link(users),
1891+
policy: pgPolicy('users_policy', {
1892+
for: 'select',
1893+
to: 'public',
1894+
using: sql`1`,
1895+
}).link(users),
18821896
};
18831897

18841898
const expectedTypes: JsonStatement['type'][] = ['alter_policy'];
@@ -1905,7 +1919,9 @@ describe('transition tests', () => {
19051919

19061920
const from = {
19071921
users,
1908-
policy: pgPolicy('users_policy', { for: 'select', to: 'public' }).link(users),
1922+
policy: pgPolicy('users_policy', { for: 'select', to: 'public' }).link(
1923+
users,
1924+
),
19091925
};
19101926
const to = {
19111927
users,
@@ -2190,6 +2206,107 @@ describe('transition tests', () => {
21902206
expect(afterPatch).toStrictEqual(fromTs);
21912207
});
21922208

2209+
test('merge commutative branches with alter_enum (no duplicates)', async () => {
2210+
// Simulates 3 leaf branches forking from the same parent, each independently
2211+
// modifying the same enum. When merging, all their alter_enum statements are
2212+
// concatenated and applied to the parent snapshot. Previously this caused
2213+
// duplicate enum entries because the 2nd/3rd alter_enum's `from` didn't match
2214+
// the already-modified enum.
2215+
const parentEnum = pgEnum('status', ['a', 'b']);
2216+
const finalEnum = pgEnum('status', ['a', 'b', 'c', 'd', 'e']);
2217+
2218+
const parentSchema = {
2219+
statusEnum: parentEnum,
2220+
items: pgTable('items', {
2221+
id: integer('id'),
2222+
status: parentEnum('status'),
2223+
}),
2224+
};
2225+
2226+
// Branch A: adds 'c'
2227+
const branchAEnum = pgEnum('status', ['a', 'b', 'c']);
2228+
const branchA = {
2229+
statusEnum: branchAEnum,
2230+
items: pgTable('items', {
2231+
id: integer('id'),
2232+
status: branchAEnum('status'),
2233+
}),
2234+
};
2235+
2236+
// Branch B: adds 'd', 'e'
2237+
const branchBEnum = pgEnum('status', ['a', 'b', 'd', 'e']);
2238+
const branchB = {
2239+
statusEnum: branchBEnum,
2240+
items: pgTable('items', {
2241+
id: integer('id'),
2242+
status: branchBEnum('status'),
2243+
}),
2244+
};
2245+
2246+
// Get statements from each branch (parent -> branch)
2247+
const { statements: statementsA } = await diff(parentSchema, branchA, []);
2248+
const { statements: statementsB } = await diff(parentSchema, branchB, []);
2249+
2250+
// Combine all statements (simulates flatMap of leaf statements)
2251+
const combinedStatements = [...statementsA, ...statementsB];
2252+
2253+
const base: PostgresSnapshot = {
2254+
version: '8',
2255+
dialect: 'postgres',
2256+
id: 'snapshot-id',
2257+
prevIds: [originId],
2258+
ddl: drizzleToDDL(parentSchema).ddl.entities.list(),
2259+
renames: [],
2260+
};
2261+
2262+
const actual = generateLatestSnapshot(base, combinedStatements);
2263+
2264+
// The expected snapshot is what the final schema looks like
2265+
const expected = {
2266+
...base,
2267+
ddl: drizzleToDDL({
2268+
statusEnum: finalEnum,
2269+
items: pgTable('items', {
2270+
id: integer('id'),
2271+
status: finalEnum('status'),
2272+
}),
2273+
}).ddl.entities.list(),
2274+
};
2275+
2276+
const stable = (value: unknown): unknown => {
2277+
if (Array.isArray(value)) {
2278+
return value.map(stable);
2279+
}
2280+
if (value && typeof value === 'object') {
2281+
return Object.fromEntries(
2282+
Object.entries(value as Record<string, unknown>)
2283+
.sort(([left], [right]) => left.localeCompare(right))
2284+
.map(([key, entry]) => [key, stable(entry)]),
2285+
);
2286+
}
2287+
return value;
2288+
};
2289+
2290+
const ddlEntries = (ddl: PostgresEntity[]) =>
2291+
ddl
2292+
.map((entry) => JSON.stringify(stable(entry)))
2293+
.sort((left, right) => left.localeCompare(right));
2294+
2295+
const actualDdl = ddlEntries(actual.ddl);
2296+
const expectedDdl = ddlEntries(expected.ddl);
2297+
2298+
// Verify no duplicate enum entries
2299+
const enumEntries = actual.ddl.filter(
2300+
(e) => (e as any).entityType === 'enums',
2301+
);
2302+
expect(
2303+
enumEntries.length,
2304+
`Expected exactly 1 enum entry, but found ${enumEntries.length}. Duplicate enums were created during branch merge.`,
2305+
).toBe(1);
2306+
2307+
expect(actualDdl).toStrictEqual(expectedDdl);
2308+
});
2309+
21932310
test('alter view', async () => {
21942311
const from = {
21952312
users: pgTable('users', { id: integer('id') }),
@@ -2202,7 +2319,9 @@ describe('transition tests', () => {
22022319
active_users: pgView('active_users', {
22032320
id: integer('id'),
22042321
name: varchar('name'),
2205-
}).with({ checkOption: 'local' }).as(sql`SELECT id, name FROM users WHERE active = true`),
2322+
})
2323+
.with({ checkOption: 'local' })
2324+
.as(sql`SELECT id, name FROM users WHERE active = true`),
22062325
};
22072326

22082327
const expectedTypes: JsonStatement['type'][] = ['alter_view'];

drizzle-orm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "drizzle-orm",
3-
"version": "1.0.0-beta.20",
3+
"version": "1.0.0-beta.21",
44
"description": "Drizzle ORM package for SQL databases",
55
"type": "module",
66
"scripts": {

drizzle-seed/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "drizzle-seed",
3-
"version": "1.0.0-beta.20",
3+
"version": "1.0.0-beta.21",
44
"main": "./index.cjs",
55
"module": "./index.mjs",
66
"types": "./index.d.ts",

eslint-plugin-drizzle/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "eslint-plugin-drizzle",
3-
"version": "1.0.0-beta.20",
3+
"version": "1.0.0-beta.21",
44
"description": "Eslint plugin for drizzle users to avoid common pitfalls",
55
"main": "src/index.js",
66
"scripts": {

0 commit comments

Comments
 (0)