Skip to content

Commit 82b36ef

Browse files
committed
feat: add schema isolation infrastructure for parallel Jest workers
Add optional schema-per-worker isolation for test parallelization: - data-source.ts: Add TYPEORM_SCHEMA and ENABLE_SCHEMA_ISOLATION env var support to configure dynamic schemas for Jest workers - setup.ts: Add createWorkerSchema() to create isolated schemas by copying table structures and views from public schema - setup.ts: Update cleanDatabase() to use schema-aware queries with pg_get_serial_sequence for sequence resets Schema isolation can be enabled by setting ENABLE_SCHEMA_ISOLATION=true in CI. This creates a separate schema for each Jest worker, allowing parallel test execution without conflicts. Note: This is infrastructure for ENG-283. Full enablement (removing --runInBand) requires addressing raw SQL queries in auth/boot tests that don't use the schema prefix. ENG-283
1 parent 3832b36 commit 82b36ef

2 files changed

Lines changed: 128 additions & 5 deletions

File tree

__tests__/setup.ts

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as matchers from 'jest-extended';
2+
import { DataSource } from 'typeorm';
23
import '../src/config';
34
import createOrGetConnection from '../src/db';
5+
import { testSchema } from '../src/data-source';
46
import { remoteConfig } from '../src/remoteConfig';
57
import { loadAuthKeys } from '../src/auth';
68

@@ -61,17 +63,30 @@ const cleanDatabase = async (): Promise<void> => {
6163
await remoteConfig.init();
6264

6365
const con = await createOrGetConnection();
66+
const schema = con.options.schema || 'public';
67+
6468
for (const entity of con.entityMetadatas) {
6569
const repository = con.getRepository(entity.name);
6670
if (repository.metadata.tableType === 'view') continue;
6771
await repository.query(`DELETE
68-
FROM "${entity.tableName}";`);
72+
FROM "${schema}"."${entity.tableName}";`);
6973

7074
for (const column of entity.primaryColumns) {
7175
if (column.generationStrategy === 'increment') {
72-
await repository.query(
73-
`ALTER SEQUENCE ${entity.tableName}_${column.databaseName}_seq RESTART WITH 1`,
74-
);
76+
// Use pg_get_serial_sequence to find the actual sequence name
77+
// This handles both original and copied tables with different sequence naming
78+
try {
79+
const seqResult = await repository.query(
80+
`SELECT pg_get_serial_sequence('"${schema}"."${entity.tableName}"', '${column.databaseName}') as seq_name`,
81+
);
82+
if (seqResult[0]?.seq_name) {
83+
await repository.query(
84+
`ALTER SEQUENCE ${seqResult[0].seq_name} RESTART WITH 1`,
85+
);
86+
}
87+
} catch {
88+
// Sequence might not exist, ignore
89+
}
7590
}
7691
}
7792
}
@@ -82,6 +97,92 @@ jest.mock('file-type', () => ({
8297
fileTypeFromBuffer: () => fileTypeFromBuffer(),
8398
}));
8499

100+
/**
101+
* Create the worker schema for test isolation.
102+
* Creates a new schema and copies all table structures from public schema.
103+
* This is used when ENABLE_SCHEMA_ISOLATION=true for parallel Jest workers.
104+
*/
105+
const createWorkerSchema = async (): Promise<void> => {
106+
// Only create non-public schemas (when running with multiple Jest workers)
107+
if (testSchema === 'public') {
108+
return;
109+
}
110+
111+
// Bootstrap connection using public schema
112+
const bootstrapDataSource = new DataSource({
113+
type: 'postgres',
114+
host: process.env.TYPEORM_HOST || 'localhost',
115+
port: 5432,
116+
username: process.env.TYPEORM_USERNAME || 'postgres',
117+
password: process.env.TYPEORM_PASSWORD || '12345',
118+
database:
119+
process.env.TYPEORM_DATABASE ||
120+
(process.env.NODE_ENV === 'test' ? 'api_test' : 'api'),
121+
schema: 'public',
122+
});
123+
124+
await bootstrapDataSource.initialize();
125+
126+
// Drop and create the worker schema
127+
await bootstrapDataSource.query(
128+
`DROP SCHEMA IF EXISTS "${testSchema}" CASCADE`,
129+
);
130+
await bootstrapDataSource.query(`CREATE SCHEMA "${testSchema}"`);
131+
132+
// Get all tables from public schema (excluding views and TypeORM metadata)
133+
const tables = await bootstrapDataSource.query(`
134+
SELECT tablename FROM pg_tables
135+
WHERE schemaname = 'public'
136+
AND tablename NOT LIKE 'pg_%'
137+
AND tablename != 'typeorm_metadata'
138+
`);
139+
140+
// Copy table structure from public to worker schema
141+
for (const { tablename } of tables) {
142+
await bootstrapDataSource.query(`
143+
CREATE TABLE "${testSchema}"."${tablename}"
144+
(LIKE "public"."${tablename}" INCLUDING ALL)
145+
`);
146+
}
147+
148+
// Copy migrations table so TypeORM knows migrations are already applied
149+
await bootstrapDataSource.query(`
150+
INSERT INTO "${testSchema}"."migrations" SELECT * FROM "public"."migrations"
151+
`);
152+
153+
// Get all views from public schema and recreate them in worker schema
154+
const views = await bootstrapDataSource.query(`
155+
SELECT viewname, definition FROM pg_views
156+
WHERE schemaname = 'public'
157+
`);
158+
159+
for (const { viewname, definition } of views) {
160+
// Replace public schema references with worker schema in view definition
161+
const modifiedDefinition = definition.replace(
162+
/public\./g,
163+
`${testSchema}.`,
164+
);
165+
await bootstrapDataSource.query(`
166+
CREATE OR REPLACE VIEW "${testSchema}"."${viewname}" AS ${modifiedDefinition}
167+
`);
168+
}
169+
170+
await bootstrapDataSource.destroy();
171+
};
172+
173+
let schemaInitialized = false;
174+
175+
beforeAll(async () => {
176+
if (!schemaInitialized) {
177+
// Create worker schema for parallel test isolation
178+
// Public schema is set up by the pretest script
179+
if (testSchema !== 'public') {
180+
await createWorkerSchema();
181+
}
182+
schemaInitialized = true;
183+
}
184+
});
185+
85186
beforeEach(async () => {
86187
loadAuthKeys();
87188

src/data-source.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,31 @@
11
import 'reflect-metadata';
22
import { DataSource } from 'typeorm';
33

4+
/**
5+
* Determine schema for test isolation.
6+
* Each Jest worker gets its own schema to enable parallel test execution.
7+
* Schema isolation is enabled in CI when ENABLE_SCHEMA_ISOLATION=true,
8+
* which allows parallel Jest workers to run without conflicts.
9+
*/
10+
const getSchema = (): string => {
11+
if (process.env.TYPEORM_SCHEMA) {
12+
return process.env.TYPEORM_SCHEMA;
13+
}
14+
// Enable schema isolation for parallel Jest workers in CI
15+
if (
16+
process.env.ENABLE_SCHEMA_ISOLATION === 'true' &&
17+
process.env.JEST_WORKER_ID
18+
) {
19+
return `test_worker_${process.env.JEST_WORKER_ID}`;
20+
}
21+
return 'public';
22+
};
23+
24+
export const testSchema = getSchema();
25+
426
export const AppDataSource = new DataSource({
527
type: 'postgres',
6-
schema: 'public',
28+
schema: testSchema,
729
synchronize: false,
830
extra: {
931
max: 30,

0 commit comments

Comments
 (0)