Skip to content
Merged
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
36 changes: 36 additions & 0 deletions packages/core/__tests__/core/module-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,39 @@ it('writes module metadata files without modifying fixture', async () => {

fixture.cleanup();
});

it('throws error when module depends on itself', async () => {
const fixture = new TestFixture('sqitch', 'launchql', 'packages', 'secrets');
const dst = fixture.tempFixtureDir;
const project = new LaunchQLProject(dst);

expect(() =>
project.setModuleDependencies(['plpgsql', 'secrets', 'uuid-ossp'])
).toThrow('Circular reference detected: module "secrets" cannot depend on itself');

fixture.cleanup();
});

it('throws error with specific circular dependency example from issue', async () => {
const fixture = new TestFixture('sqitch', 'launchql', 'packages', 'secrets');
const dst = fixture.tempFixtureDir;
const project = new LaunchQLProject(dst);

expect(() =>
project.setModuleDependencies(['some-native-module', 'secrets'])
).toThrow('Circular reference detected: module "secrets" cannot depend on itself');

fixture.cleanup();
});

it('allows valid dependencies without circular references', async () => {
const fixture = new TestFixture('sqitch', 'launchql', 'packages', 'secrets');
const dst = fixture.tempFixtureDir;
const project = new LaunchQLProject(dst);

expect(() =>
project.setModuleDependencies(['plpgsql', 'uuid-ossp', 'other-module'])
).not.toThrow();

fixture.cleanup();
});
32 changes: 32 additions & 0 deletions packages/core/src/core/class/launchql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,41 @@ export class LaunchQLProject {

setModuleDependencies(modules: string[]): void {
this.ensureModule();

// Validate for circular dependencies
this.validateModuleDependencies(modules);

writeExtensions(this.cwd, modules);
}

private validateModuleDependencies(modules: string[]): void {
const currentModuleName = this.getModuleName();

if (modules.includes(currentModuleName)) {
throw new Error(`Circular reference detected: module "${currentModuleName}" cannot depend on itself`);
}

// Check for circular dependencies by examining each module's dependencies
const visited = new Set<string>();
const visiting = new Set<string>();

const checkCircular = (moduleName: string, path: string[] = []): void => {
if (visiting.has(moduleName)) {
throw new Error(`Circular reference detected: ${path.join(' -> ')} -> ${moduleName}`);
}
if (visited.has(moduleName)) {
return;
}

visiting.add(moduleName);
// More complex dependency resolution would require loading other modules' dependencies
visiting.delete(moduleName);
visited.add(moduleName);
};

modules.forEach(module => checkCircular(module, [currentModuleName]));
}

private initModuleSqitch(modName: string, targetPath: string): void {
// Create launchql.plan file using project-files package
const plan = generatePlan({
Expand Down