diff --git a/packages/core/__tests__/projects/forked-deployment-scenarios.test.ts b/packages/core/__tests__/projects/forked-deployment-scenarios.test.ts index c2821352e..2d2f7a98d 100644 --- a/packages/core/__tests__/projects/forked-deployment-scenarios.test.ts +++ b/packages/core/__tests__/projects/forked-deployment-scenarios.test.ts @@ -33,10 +33,10 @@ describe('Forked Deployment with deployModules - my-third', () => { expect(tables.rows).toHaveLength(1); expect(tables.rows.map((r: any) => r.table_name)).toEqual(['customers']); - // await fixture.revertModule('my-third', db.name, ['sqitch', 'simple-w-tags'], 'my-first:@v1.0.0'); + await fixture.revertModule('my-third', db.name, ['sqitch', 'simple-w-tags'], 'my-first:@v1.0.0'); - // expect(await db.exists('schema', 'metaschema')).toBe(false); - // expect(await db.exists('table', 'metaschema.customers')).toBe(false); + expect(await db.exists('schema', 'metaschema')).toBe(false); + expect(await db.exists('table', 'metaschema.customers')).toBe(false); }); }); diff --git a/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-resolved-tags.test.ts.snap b/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-resolved-tags.test.ts.snap index 915afc4a7..bc7950647 100644 --- a/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-resolved-tags.test.ts.snap +++ b/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-resolved-tags.test.ts.snap @@ -21,6 +21,7 @@ exports[`sqitch package dependencies with resolved tags [simple-w-tags/1st] 1`] "table_users", "table_products", ], + "resolvedTags": {}, } `; @@ -49,6 +50,7 @@ exports[`sqitch package dependencies with resolved tags [simple-w-tags/2nd] 1`] "create_table", "create_another_table", ], + "resolvedTags": {}, } `; @@ -76,5 +78,6 @@ exports[`sqitch package dependencies with resolved tags [simple-w-tags/3rd] 1`] "create_schema", "create_table", ], + "resolvedTags": {}, } `; diff --git a/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-with-tags.test.ts.snap b/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-with-tags.test.ts.snap index 706fc1d5d..a98d7c427 100644 --- a/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-with-tags.test.ts.snap +++ b/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-with-tags.test.ts.snap @@ -21,6 +21,7 @@ exports[`sqitch package dependencies [simple-w-tags/1st] 1`] = ` "table_users", "table_products", ], + "resolvedTags": {}, } `; @@ -49,5 +50,6 @@ exports[`sqitch package dependencies [simple-w-tags/2nd] 1`] = ` "create_table", "create_another_table", ], + "resolvedTags": {}, } `; diff --git a/packages/core/src/migrate/client.ts b/packages/core/src/migrate/client.ts index d3d092d2f..4d80db8d6 100644 --- a/packages/core/src/migrate/client.ts +++ b/packages/core/src/migrate/client.ts @@ -172,6 +172,7 @@ export class LaunchQLMigrate { try { // Call the deploy stored procedure + console.log('DEBUG: About to deploy change:', change.name, 'in project:', project || plan.project); await executeQuery( context, 'CALL launchql_migrate.deploy($1, $2, $3, $4, $5)', @@ -184,6 +185,13 @@ export class LaunchQLMigrate { ] ); + const verifyResult = await executeQuery( + context, + 'SELECT * FROM launchql_migrate.changes WHERE project = $1 AND change_name = $2', + [project || plan.project, change.name] + ); + console.log('DEBUG: Change record after deploy:', JSON.stringify(verifyResult.rows, null, 2)); + deployed.push(change.name); log.success(`Successfully deployed: ${change.name}`); } catch (error) { @@ -210,7 +218,57 @@ export class LaunchQLMigrate { const { project, targetDatabase, planPath, toChange, useTransaction = true } = options; const plan = parsePlanFile(planPath); - const resolvedToChange = toChange && toChange.includes('@') ? resolveTagToChangeName(planPath, toChange, project || plan.project) : toChange; + + const fullPlanResult = parsePlanFileFull(planPath); + const packageDir = dirname(planPath); + + // Check if we have cross-module tag dependencies or cross-module toChange + const hasTagDependencies = fullPlanResult.data?.changes.some((change: any) => + change.dependencies.some((dep: string) => dep.includes('@')) + ) || (toChange && toChange.includes(':@')); + + let resolvedDeps: any = null; + if (hasTagDependencies) { + const parentDir = dirname(dirname(packageDir)); + console.log('DEBUG: Using parent directory for resolution:', parentDir); + resolvedDeps = resolveDependencies(parentDir, fullPlanResult.data?.project || plan.project, { + tagResolution: 'internal', + loadPlanFiles: true + }); + console.log('DEBUG: resolvedDeps:', JSON.stringify(resolvedDeps, null, 2)); + } + + let resolvedToChange = toChange; + let targetProject = project || plan.project; + let targetChangeName = toChange; + + if (toChange && toChange.includes('@')) { + console.log('DEBUG: Processing toChange:', toChange); + if (toChange.includes(':@')) { + const [crossProject, tag] = toChange.split(':@'); + targetProject = crossProject; + console.log('DEBUG: Cross-module case - targetProject:', targetProject, 'tag:', tag); + const parentDir = dirname(dirname(packageDir)); + const targetPlanPath = join(parentDir, 'packages', crossProject, 'launchql.plan'); + console.log('DEBUG: Looking for target plan at:', targetPlanPath); + + try { + const resolvedChange = resolveTagToChangeName(targetPlanPath, `@${tag}`, crossProject); + targetChangeName = resolvedChange; + resolvedToChange = resolvedChange; + console.log('DEBUG: Resolved cross-module tag to:', resolvedChange); + } catch (error) { + console.log('DEBUG: Failed to resolve cross-module tag, using original:', toChange); + resolvedToChange = toChange; + targetChangeName = toChange; + } + } else { + resolvedToChange = resolveTagToChangeName(planPath, toChange, project || plan.project); + targetChangeName = resolvedToChange; + console.log('DEBUG: Local tag resolved to:', resolvedToChange); + } + } + const changes = getChangesInOrder(planPath, true); // Reverse order for revert const reverted: string[] = []; @@ -227,8 +285,72 @@ export class LaunchQLMigrate { await withTransaction(targetPool, { useTransaction }, async (context) => { for (const change of changes) { // Stop if we've reached the target change - if (resolvedToChange && change.name === resolvedToChange) { - break; + if (resolvedToChange && targetProject && targetChangeName) { + if (toChange && toChange.includes(':@')) { + let actualTargetChangeName = targetChangeName; + let actualTargetProject = targetProject; + + if (resolvedDeps && resolvedDeps.resolvedTags && resolvedDeps.resolvedTags[toChange]) { + const resolvedTag = resolvedDeps.resolvedTags[toChange]; + console.log('DEBUG: Using resolved tag:', resolvedTag); + + if (resolvedTag.includes(':')) { + const [resolvedProject, resolvedChange] = resolvedTag.split(':', 2); + actualTargetProject = resolvedProject; + actualTargetChangeName = resolvedChange; + } else { + actualTargetChangeName = resolvedTag; + } + } + + console.log('DEBUG: Checking deployment for project:', actualTargetProject, 'change:', actualTargetChangeName); + + const allChangesResult = await executeQuery( + context, + 'SELECT project, change_name, deployed_at FROM launchql_migrate.changes ORDER BY deployed_at', + [] + ); + console.log('DEBUG: All changes in DB (including NULL deployed_at):', JSON.stringify(allChangesResult.rows, null, 2)); + + const targetDeployedResult = await executeQuery( + context, + 'SELECT launchql_migrate.is_deployed($1, $2) as is_deployed', + [actualTargetProject, actualTargetChangeName] + ); + console.log('DEBUG: is_deployed result:', targetDeployedResult.rows[0]); + + if (!targetDeployedResult.rows[0]?.is_deployed) { + log.warn(`Target change ${targetProject}:${actualTargetChangeName} is not deployed, stopping revert`); + break; + } + + // Get deployment time of target change + const targetTimeResult = await executeQuery( + context, + 'SELECT deployed_at FROM launchql_migrate.changes WHERE project = $1 AND change_name = $2', + [actualTargetProject, actualTargetChangeName] + ); + + const currentTimeResult = await executeQuery( + context, + 'SELECT deployed_at FROM launchql_migrate.changes WHERE project = $1 AND change_name = $2', + [project || plan.project, change.name] + ); + + if (targetTimeResult.rows[0] && currentTimeResult.rows[0]) { + const targetTime = new Date(targetTimeResult.rows[0].deployed_at); + const currentTime = new Date(currentTimeResult.rows[0].deployed_at); + + if (currentTime <= targetTime) { + log.info(`Stopping revert at ${change.name} (deployed at ${currentTime}) as it was deployed before/at target ${actualTargetProject}:${actualTargetChangeName} (deployed at ${targetTime})`); + break; + } + } + } else { + if (change.name === resolvedToChange) { + break; + } + } } // Check if deployed diff --git a/packages/core/src/resolution/deps.ts b/packages/core/src/resolution/deps.ts index c3e9ec677..59ece0c5b 100644 --- a/packages/core/src/resolution/deps.ts +++ b/packages/core/src/resolution/deps.ts @@ -23,6 +23,8 @@ interface DependencyResult { resolved: string[]; /** The complete dependency graph mapping modules to their dependencies */ deps: DependencyGraph; + /** Mapping of resolved tags to their target changes (only for resolveDependencies) */ + resolvedTags?: Record; } /** @@ -529,5 +531,5 @@ export const resolveDependencies = ( const normalSql = resolved.filter((module) => !module.startsWith('extensions/')); resolved = [...extensions, ...normalSql]; - return { external, resolved, deps }; + return { external, resolved, deps, resolvedTags: tagMappings }; }; diff --git a/packages/core/test-utils/CoreDeployTestFixture.ts b/packages/core/test-utils/CoreDeployTestFixture.ts index b0d87b0f8..071f87e23 100644 --- a/packages/core/test-utils/CoreDeployTestFixture.ts +++ b/packages/core/test-utils/CoreDeployTestFixture.ts @@ -35,8 +35,9 @@ export class CoreDeployTestFixture extends TestFixture { cwd: basePath, recursive: true, projectName, - fast: true, - usePlan: true + fast: false, + usePlan: true, + useSqitch: false }; await deployModules(options); @@ -58,7 +59,8 @@ export class CoreDeployTestFixture extends TestFixture { cwd: projectPath, recursive: true, projectName, - toChange + toChange, + useSqitch: false }; await revertModules(options); diff --git a/yarn.lock b/yarn.lock index 1d7090e29..37771e761 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2864,7 +2864,7 @@ "@types/node" "*" "@types/pg" "*" -"@types/pg@*", "@types/pg@>=6 <9", "@types/pg@^8.10.9", "@types/pg@^8.15.2": +"@types/pg@*", "@types/pg@>=6 <9", "@types/pg@^8.15.2": version "8.15.4" resolved "https://registry.yarnpkg.com/@types/pg/-/pg-8.15.4.tgz#419f791c6fac8e0bed66dd8f514b60f8ba8db46d" integrity sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg== @@ -8333,7 +8333,7 @@ pg-types@2.2.0, pg-types@^2.2.0: postgres-date "~1.0.4" postgres-interval "^1.1.0" -"pg@>=6.1.0 <9", pg@^8.11.3, pg@^8.16.0: +"pg@>=6.1.0 <9", pg@^8.16.0: version "8.16.3" resolved "https://registry.yarnpkg.com/pg/-/pg-8.16.3.tgz#160741d0b44fdf64680e45374b06d632e86c99fd" integrity sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==