Skip to content

Commit 04ea7ef

Browse files
committed
fix(drizzle-kit): limit postgres introspection fanout
1 parent a888144 commit 04ea7ef

3 files changed

Lines changed: 110 additions & 19 deletions

File tree

drizzle-kit/src/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export type DrizzlePgDB = DB & {
3636
export type PreparePgDBOptions = {
3737
queryConcurrency?: number;
3838
};
39+
export type IntrospectPgDBOptions = {
40+
tableConcurrency?: number;
41+
};
3942
export type DrizzlePgDBIntrospectSchema = Omit<
4043
PgSchemaKit,
4144
'internal'
@@ -80,6 +83,7 @@ export const introspectPgDB = async (
8083
db: DrizzlePgDB,
8184
filters: string[],
8285
schemaFilters: string[],
86+
options: IntrospectPgDBOptions = {},
8387
): Promise<DrizzlePgDBIntrospectSchema> => {
8488
const matchers = filters.map((it) => {
8589
return new Minimatch(it);
@@ -115,6 +119,7 @@ export const introspectPgDB = async (
115119
undefined,
116120
undefined,
117121
undefined,
122+
options,
118123
);
119124

120125
const schema = { id: originUUID, prevId: '', ...res } as PgSchemaKit;

drizzle-kit/src/serializer/pgSerializer.ts

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,34 @@ import type {
4444
import { type DB, escapeSingleQuotes, isPgArrayType } from '../utils';
4545
import { getColumnCasing, sqlToStr } from './utils';
4646

47+
export type PgIntrospectOptions = {
48+
tableConcurrency?: number;
49+
};
50+
51+
async function mapWithConcurrency<T>(
52+
items: T[],
53+
concurrency: number | undefined,
54+
fn: (item: T) => Promise<unknown>,
55+
) {
56+
if (concurrency === undefined || concurrency < 1) {
57+
await Promise.all(items.map(fn));
58+
return;
59+
}
60+
61+
let nextIndex = 0;
62+
const workerCount = Math.min(Math.floor(concurrency), items.length);
63+
64+
await Promise.all(
65+
Array.from({ length: workerCount }, async () => {
66+
while (nextIndex < items.length) {
67+
const currentIndex = nextIndex;
68+
nextIndex += 1;
69+
await fn(items[currentIndex]);
70+
}
71+
}),
72+
);
73+
}
74+
4775
export const indexName = (tableName: string, columns: string[]) => {
4876
return `${tableName}_${columns.join('_')}_index`;
4977
};
@@ -983,6 +1011,7 @@ export const fromDatabase = async (
9831011
status: IntrospectStatus,
9841012
) => void,
9851013
tsSchema?: PgSchemaInternal,
1014+
options: PgIntrospectOptions = {},
9861015
): Promise<PgSchemaInternal> => {
9871016
const result: Record<string, Table> = {};
9881017
const views: Record<string, View> = {};
@@ -1209,13 +1238,20 @@ WHERE
12091238

12101239
const sequencesInColumns: string[] = [];
12111240

1212-
const all = allTables
1213-
.filter((it) => it.type === 'table')
1214-
.map((row) => {
1241+
const tableRows = allTables.filter((it) => it.type === 'table');
1242+
tableCount = tableRows.filter((row) => tablesFilter(row.table_name as string)).length;
1243+
1244+
if (progressCallback) {
1245+
progressCallback('tables', tableCount, 'done');
1246+
}
1247+
1248+
await mapWithConcurrency(
1249+
tableRows,
1250+
options.tableConcurrency,
1251+
async (row) => {
12151252
return new Promise(async (res, rej) => {
12161253
const tableName = row.table_name as string;
12171254
if (!tablesFilter(tableName)) return res('');
1218-
tableCount += 1;
12191255
const tableSchema = row.table_schema;
12201256

12211257
try {
@@ -1668,18 +1704,14 @@ WHERE
16681704
}
16691705
res('');
16701706
});
1671-
});
1672-
1673-
if (progressCallback) {
1674-
progressCallback('tables', tableCount, 'done');
1675-
}
1676-
1677-
for await (const _ of all) {
1678-
}
1707+
},
1708+
);
16791709

1680-
const allViews = allTables
1681-
.filter((it) => it.type === 'view' || it.type === 'materialized_view')
1682-
.map((row) => {
1710+
const viewRows = allTables.filter((it) => it.type === 'view' || it.type === 'materialized_view');
1711+
const allViews = mapWithConcurrency(
1712+
viewRows,
1713+
options.tableConcurrency,
1714+
async (row) => {
16831715
return new Promise(async (res, rej) => {
16841716
const viewName = row.table_name as string;
16851717
if (!tablesFilter(viewName)) return res('');
@@ -1899,12 +1931,12 @@ WHERE
18991931
}
19001932
res('');
19011933
});
1902-
});
1934+
},
1935+
);
19031936

1904-
viewsCount = allViews.length;
1937+
viewsCount = viewRows.length;
19051938

1906-
for await (const _ of allViews) {
1907-
}
1939+
await allViews;
19081940

19091941
if (progressCallback) {
19101942
progressCallback('columns', columnsCount, 'done');
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { describe, expect, test, vi } from 'vitest';
2+
import { fromDatabase } from '../src/serializer/pgSerializer';
3+
4+
const TABLES_QUERY_MARKER = 'pg_catalog.pg_class c';
5+
6+
function createObservedDb({ tableCount }: { tableCount: number }) {
7+
let activeQueries = 0;
8+
let maxActiveQueries = 0;
9+
10+
const query = vi.fn(async (sql: string) => {
11+
activeQueries += 1;
12+
maxActiveQueries = Math.max(maxActiveQueries, activeQueries);
13+
14+
await new Promise((resolve) => setTimeout(resolve, 5));
15+
16+
activeQueries -= 1;
17+
18+
if (sql.includes(TABLES_QUERY_MARKER)) {
19+
return Array.from({ length: tableCount }, (_, index) => ({
20+
table_schema: 'public',
21+
table_name: `table_${index}`,
22+
type: 'table',
23+
rls_enabled: false,
24+
}));
25+
}
26+
27+
return [];
28+
});
29+
30+
return {
31+
db: { query },
32+
query,
33+
getMaxActiveQueries: () => maxActiveQueries,
34+
};
35+
}
36+
37+
describe('fromDatabase', () => {
38+
test('limits table introspection fanout with tableConcurrency', async () => {
39+
const observed = createObservedDb({ tableCount: 8 });
40+
41+
await fromDatabase(
42+
observed.db as any,
43+
undefined,
44+
[],
45+
undefined,
46+
undefined,
47+
undefined,
48+
{ tableConcurrency: 2 },
49+
);
50+
51+
expect(observed.query).toHaveBeenCalled();
52+
expect(observed.getMaxActiveQueries()).toBeLessThanOrEqual(2);
53+
});
54+
});

0 commit comments

Comments
 (0)