diff --git a/packages/core/__tests__/migrate/local-tracking-guard.test.ts b/packages/core/__tests__/migrate/local-tracking-guard.test.ts new file mode 100644 index 000000000..5157a2298 --- /dev/null +++ b/packages/core/__tests__/migrate/local-tracking-guard.test.ts @@ -0,0 +1,52 @@ +import { LaunchQLMigrate } from '../../src/migrate/client'; +import { MigrateTestFixture, teardownAllPools, TestDatabase } from '../../test-utils'; + +describe('local tracking guard for deployed/skipped', () => { + let fixture: MigrateTestFixture; + let db: TestDatabase; + + beforeEach(async () => { + fixture = new MigrateTestFixture(); + db = await fixture.setupTestDatabase(); + }); + + afterEach(async () => { + await fixture.cleanup(); + }); + + afterAll(async () => { + await teardownAllPools(); + }); + + it('normalizes same-package qualified names to unqualified in deployed', async () => { + const tempDir = fixture.createPlanFile('test-local-tracking', [ + { name: 'change1' } + ]); + + fixture.createScript(tempDir, 'deploy', 'change1', 'SELECT 1;'); + + const client = new LaunchQLMigrate(db.config); + + const result = await client.deploy({ + modulePath: tempDir, + logOnly: true, + }); + + expect(result.deployed).toContain('change1'); + expect(result.deployed.every((n: string) => !n.includes(':'))).toBe(true); + }); + + it('throws error on cross-package qualified names', async () => { + const tempDir = fixture.createPlanFile('test-local-tracking', [ + { name: 'change1' } + ]); + + fixture.createScript(tempDir, 'deploy', 'change1', 'SELECT 1;'); + + const client = new LaunchQLMigrate(db.config); + + expect(() => { + (client as any).toUnqualifiedLocal('pkgA', 'pkgB:change1'); + }).toThrow('Cross-package change encountered in local tracking: pkgB:change1 (current package: pkgA)'); + }); +}); diff --git a/packages/core/src/migrate/client.ts b/packages/core/src/migrate/client.ts index 4e6159170..9d3d16d2e 100644 --- a/packages/core/src/migrate/client.ts +++ b/packages/core/src/migrate/client.ts @@ -1,14 +1,13 @@ import { Logger } from '@launchql/logger'; +import { errors } from '@launchql/types'; import { readFileSync } from 'fs'; import { dirname,join } from 'path'; import { Pool } from 'pg'; import { getPgPool } from 'pg-cache'; import { PgConfig } from 'pg-env'; -import { errors } from '@launchql/types'; -import { LaunchQLPackage } from '../core/class/launchql'; import { Change, parsePlanFile, parsePlanFileSimple, readScript } from '../files'; -import { DependencyResult, resolveDependencies } from '../resolution/deps'; +import { resolveDependencies } from '../resolution/deps'; import { resolveTagToChangeName } from '../resolution/resolve'; import { cleanSql } from './clean'; import { @@ -49,6 +48,13 @@ export class LaunchQLMigrate { private eventLogger: EventLogger; private initialized: boolean = false; + private toUnqualifiedLocal(pkg: string, nm: string) { + if (!nm.includes(':')) return nm; + const [p, local] = nm.split(':', 2); + if (p === pkg) return local; + throw new Error(`Cross-package change encountered in local tracking: ${nm} (current package: ${pkg})`); + } + constructor(config: PgConfig, options: LaunchQLMigrateOptions = {}) { this.pgConfig = config; // Use environment variable DEPLOYMENT_HASH_METHOD if available, otherwise use options or default to 'content' @@ -156,7 +162,7 @@ export class LaunchQLMigrate { const isDeployed = await this.isDeployed(plan.package, change.name); if (isDeployed) { log.info(`Skipping already deployed change: ${change.name}`); - const unqualified = change.name.includes(':') ? change.name.split(':')[1] : change.name; + const unqualified = this.toUnqualifiedLocal(plan.package, change.name); skipped.push(unqualified); continue; } @@ -196,7 +202,7 @@ export class LaunchQLMigrate { ] ); - const unqualified = change.name.includes(':') ? change.name.split(':')[1] : change.name; + const unqualified = this.toUnqualifiedLocal(plan.package, change.name); deployed.push(unqualified); log.success(`Successfully ${logOnly ? 'logged' : 'deployed'}: ${change.name}`); } catch (error: any) { @@ -302,7 +308,8 @@ export class LaunchQLMigrate { const isDeployed = await this.isDeployed(plan.package, change.name); if (!isDeployed) { log.info(`Skipping not deployed change: ${change.name}`); - skipped.push(change.name); + const unqualified = this.toUnqualifiedLocal(plan.package, change.name); + skipped.push(unqualified); continue; }