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

[PG] Fix fk introspection ordering columns wrongly (critical failure) #3994

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 47 additions & 37 deletions drizzle-kit/src/serializer/pgSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1258,43 +1258,53 @@ WHERE

const tableForeignKeys = await db.query(
`SELECT
con.contype AS constraint_type,
nsp.nspname AS constraint_schema,
con.conname AS constraint_name,
rel.relname AS table_name,
att.attname AS column_name,
fnsp.nspname AS foreign_table_schema,
frel.relname AS foreign_table_name,
fatt.attname AS foreign_column_name,
CASE con.confupdtype
WHEN 'a' THEN 'NO ACTION'
WHEN 'r' THEN 'RESTRICT'
WHEN 'n' THEN 'SET NULL'
WHEN 'c' THEN 'CASCADE'
WHEN 'd' THEN 'SET DEFAULT'
END AS update_rule,
CASE con.confdeltype
WHEN 'a' THEN 'NO ACTION'
WHEN 'r' THEN 'RESTRICT'
WHEN 'n' THEN 'SET NULL'
WHEN 'c' THEN 'CASCADE'
WHEN 'd' THEN 'SET DEFAULT'
END AS delete_rule
FROM
pg_catalog.pg_constraint con
JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid
JOIN pg_catalog.pg_namespace nsp ON nsp.oid = con.connamespace
LEFT JOIN pg_catalog.pg_attribute att ON att.attnum = ANY (con.conkey)
AND att.attrelid = con.conrelid
LEFT JOIN pg_catalog.pg_class frel ON frel.oid = con.confrelid
LEFT JOIN pg_catalog.pg_namespace fnsp ON fnsp.oid = frel.relnamespace
LEFT JOIN pg_catalog.pg_attribute fatt ON fatt.attnum = ANY (con.confkey)
AND fatt.attrelid = con.confrelid
WHERE
nsp.nspname = '${tableSchema}'
AND rel.relname = '${tableName}'
AND con.contype IN ('f');`,
);
con.contype AS constraint_type,
nsp.nspname AS constraint_schema,
con.conname AS constraint_name,
rel.relname AS table_name,
att.attname AS column_name,
fnsp.nspname AS foreign_table_schema,
frel.relname AS foreign_table_name,
fatt.attname AS foreign_column_name,
CASE con.confupdtype
WHEN 'a' THEN 'NO ACTION'
WHEN 'r' THEN 'RESTRICT'
WHEN 'n' THEN 'SET NULL'
WHEN 'c' THEN 'CASCADE'
WHEN 'd' THEN 'SET DEFAULT'
END AS update_rule,
CASE con.confdeltype
WHEN 'a' THEN 'NO ACTION'
WHEN 'r' THEN 'RESTRICT'
WHEN 'n' THEN 'SET NULL'
WHEN 'c' THEN 'CASCADE'
WHEN 'd' THEN 'SET DEFAULT'
END AS delete_rule
FROM pg_catalog.pg_constraint con
JOIN pg_catalog.pg_class rel
ON rel.oid = con.conrelid
JOIN pg_catalog.pg_namespace nsp
ON nsp.oid = con.connamespace

CROSS JOIN generate_subscripts(con.conkey, 1) AS s(i)

LEFT JOIN pg_catalog.pg_attribute att
ON att.attrelid = con.conrelid
AND att.attnum = con.conkey[s.i]

LEFT JOIN pg_catalog.pg_class frel
ON frel.oid = con.confrelid

LEFT JOIN pg_catalog.pg_namespace fnsp
ON fnsp.oid = frel.relnamespace

LEFT JOIN pg_catalog.pg_attribute fatt
ON fatt.attrelid = con.confrelid
AND fatt.attnum = con.confkey[s.i]

WHERE nsp.nspname = '${tableSchema}'
AND rel.relname = '${tableName}'
AND con.contype IN ('f')`);

foreignKeysCount += tableForeignKeys.length;
if (progressCallback) {
Expand Down
45 changes: 45 additions & 0 deletions drizzle-kit/tests/introspect/pg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
cidr,
date,
doublePrecision,
foreignKey,
inet,
integer,
interval,
Expand All @@ -31,6 +32,7 @@ import {
text,
time,
timestamp,
unique,
uuid,
varchar,
} from 'drizzle-orm/pg-core';
Expand Down Expand Up @@ -892,3 +894,46 @@ test('multiple policies with roles from schema', async () => {
expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});



test('introspect foreign keys', async () => {
const client = new PGlite();

const userProfileImages = pgTable('user_profile_images', {
id: uuid('id').primaryKey(),
ownerId: uuid('owner_id').notNull(),
image_blob: text('image_blob').notNull(), // whatever
}, (table) => [
// Ensures (id, ownerId) is also unique for the composite fk to work
unique().on(table.id, table.ownerId)
]);

const userProfileCoverImages = pgTable('user_profile_cover_images', {
id: uuid('id').primaryKey(),
ownerId: uuid('owner_id').notNull(),
profileImageId: uuid('profile_image_id')
.notNull(),
}, (table) => [
// Guarantees each cover image matches the correct profile + owner
foreignKey({
columns: [table.ownerId, table.profileImageId],
foreignColumns: [userProfileImages.ownerId, userProfileImages.id],
Comment on lines +920 to +921
Copy link
Author

Choose a reason for hiding this comment

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

The order of these matters. Previously it had weird behaviour where it would appear that these were sorted. The information is in the linked issue. Perhaps there is a better way to describe the preconditions

name: 'user_profile_cover_images_ownerId_profileImageId_fk'
}).onDelete('cascade')
]);

const schema = {
userProfileImages,
userProfileCoverImages,
};

const { statements, sqlStatements } = await introspectPgToFile(
client,
schema,
'introspect-foreign-keys',
);

expect(statements.length).toBe(0);
expect(sqlStatements.length).toBe(0);
});