From c7650afeb6fac55d455f1bcfd3a8d4251a37ac1e Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Fri, 18 Jul 2025 01:56:54 -0700 Subject: [PATCH 1/9] updates --- packages/core/ISSUES.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/core/ISSUES.md b/packages/core/ISSUES.md index b380a8c0a..01deb7248 100644 --- a/packages/core/ISSUES.md +++ b/packages/core/ISSUES.md @@ -1,5 +1,22 @@ # LaunchQL Core - Issues and Test Cases + +## Current Priority + + +read packages/core/__tests__/projects/forked-deployment-scenarios.test.ts as an example + +you'll see some commented out code: + +// 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); + +* we need to assess, is this the appriopriate API for revertModule? +* are the underlying methods used by revertModule passing down all the needed information? + + ## Critical Test Scenarios ### Deployment Failure Recovery From b7b9a3d47d29fc38386fa3884160626122f1766e Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Fri, 18 Jul 2025 01:58:12 -0700 Subject: [PATCH 2/9] updates --- packages/core/ISSUES.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/core/ISSUES.md b/packages/core/ISSUES.md index 01deb7248..b226f94d6 100644 --- a/packages/core/ISSUES.md +++ b/packages/core/ISSUES.md @@ -16,6 +16,15 @@ you'll see some commented out code: * we need to assess, is this the appriopriate API for revertModule? * are the underlying methods used by revertModule passing down all the needed information? +please carefully make sure that we can revert back to a change, and also revert a tag. + +our tag resolution should be higher up in the API surface, but I believe the LaunchQLMigrate class already handles this, we should check + + + + + + ## Critical Test Scenarios From 199c3bdf9fc74d6b41be425ac7b8dce13b22ad87 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:00:08 +0000 Subject: [PATCH 3/9] feat: add cross-module tag resolution to revert functionality - Add resolveDependencies call to revert method in LaunchQLMigrate client - Implement cross-module tag resolution (e.g., 'my-first:@v1.0.0') in revert flow - Add resolvedTags field to DependencyResult interface - Fix test fixture to use new migration system instead of fast deployment - Enable revert to specific tags across module boundaries - Uncomment and fix test case for cross-module tag revert functionality Co-Authored-By: Dan Lynch --- .../forked-deployment-scenarios.test.ts | 6 +- packages/core/src/migrate/client.ts | 128 +++++++++++++++++- packages/core/src/resolution/deps.ts | 4 +- .../core/test-utils/CoreDeployTestFixture.ts | 8 +- 4 files changed, 136 insertions(+), 10 deletions(-) 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/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); From d01533fce2362ac73df39048902dc0985b9a52f1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:00:36 +0000 Subject: [PATCH 4/9] chore: update yarn.lock after test runs Co-Authored-By: Dan Lynch --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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== From 94a7bbc445f26f00a98e25b0b681043992bf9ff9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:06:32 +0000 Subject: [PATCH 5/9] fix: update snapshots for resolvedTags field in dependency resolution Co-Authored-By: Dan Lynch --- .../dependency-resolution-resolved-tags.test.ts.snap | 3 +++ .../__snapshots__/dependency-resolution-with-tags.test.ts.snap | 2 ++ 2 files changed, 5 insertions(+) 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": {}, } `; From f3014946e2925611e57069d857ecfb61ef2e315d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:12:43 +0000 Subject: [PATCH 6/9] chore: remove debug console logs Co-Authored-By: Dan Lynch --- packages/core/src/migrate/client.ts | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/packages/core/src/migrate/client.ts b/packages/core/src/migrate/client.ts index 4d80db8d6..94805e0e0 100644 --- a/packages/core/src/migrate/client.ts +++ b/packages/core/src/migrate/client.ts @@ -172,7 +172,6 @@ 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)', @@ -185,13 +184,6 @@ 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) { @@ -230,12 +222,10 @@ export class LaunchQLMigrate { 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; @@ -243,29 +233,23 @@ export class LaunchQLMigrate { 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); } } @@ -292,7 +276,6 @@ export class LaunchQLMigrate { 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); @@ -303,21 +286,11 @@ export class LaunchQLMigrate { } } - 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`); From 76fd4ea69d7cb96239328d8f5db41624fc52fbe0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:22:17 +0000 Subject: [PATCH 7/9] refactor: replace brittle path construction with LaunchQLProject workspace navigation Co-Authored-By: Dan Lynch --- .../dependency-resolution-basic.test.ts.snap | 4 ++ ...ency-resolution-internal-tags.test.ts.snap | 10 +++++ packages/core/src/migrate/client.ts | 39 ++++++++++++++----- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-basic.test.ts.snap b/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-basic.test.ts.snap index 348bc6751..6577ac0ff 100644 --- a/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-basic.test.ts.snap +++ b/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-basic.test.ts.snap @@ -21,6 +21,7 @@ exports[`sqitch package dependencies [simple/1st] 1`] = ` "table_users", "table_products", ], + "resolvedTags": {}, } `; @@ -43,6 +44,7 @@ exports[`sqitch package dependencies [simple/2nd] 1`] = ` "create_table", "create_another_table", ], + "resolvedTags": {}, } `; @@ -64,6 +66,7 @@ exports[`sqitch package dependencies [simple/3rd] 1`] = ` "create_schema", "create_table", ], + "resolvedTags": {}, } `; @@ -76,5 +79,6 @@ exports[`sqitch package dependencies [utils] 1`] = ` "resolved": [ "procedures/myfunction", ], + "resolvedTags": {}, } `; diff --git a/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-internal-tags.test.ts.snap b/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-internal-tags.test.ts.snap index f006016c3..36340a40b 100644 --- a/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-internal-tags.test.ts.snap +++ b/packages/core/__tests__/resolution/__snapshots__/dependency-resolution-internal-tags.test.ts.snap @@ -21,6 +21,7 @@ exports[`sqitch package dependencies with internal tag resolution [simple-w-tags "table_users", "table_products", ], + "resolvedTags": {}, } `; @@ -49,6 +50,9 @@ exports[`sqitch package dependencies with internal tag resolution [simple-w-tags "create_table", "create_another_table", ], + "resolvedTags": { + "my-first:@v1.0.0": "my-first:table_users", + }, } `; @@ -76,5 +80,11 @@ exports[`sqitch package dependencies with internal tag resolution [simple-w-tags "create_schema", "create_table", ], + "resolvedTags": { + "my-first:@v1.1.0": "my-first:table_products", + "my-second:@v2.0.0": "my-second:create_table", + "my-second:@v2.1.0": "my-second:create_another_table", + "my-third:@v2.0.0": "my-third:create_schema", + }, } `; diff --git a/packages/core/src/migrate/client.ts b/packages/core/src/migrate/client.ts index 94805e0e0..145cd88ac 100644 --- a/packages/core/src/migrate/client.ts +++ b/packages/core/src/migrate/client.ts @@ -220,12 +220,18 @@ export class LaunchQLMigrate { ) || (toChange && toChange.includes(':@')); let resolvedDeps: any = null; + let launchqlProject: any = null; if (hasTagDependencies) { - const parentDir = dirname(dirname(packageDir)); - resolvedDeps = resolveDependencies(parentDir, fullPlanResult.data?.project || plan.project, { - tagResolution: 'internal', - loadPlanFiles: true - }); + const { LaunchQLProject } = await import('../core/class/launchql'); + launchqlProject = new LaunchQLProject(packageDir); + const workspacePath = launchqlProject.getWorkspacePath(); + + if (workspacePath) { + resolvedDeps = resolveDependencies(workspacePath, fullPlanResult.data?.project || plan.project, { + tagResolution: 'internal', + loadPlanFiles: true + }); + } } let resolvedToChange = toChange; @@ -236,13 +242,26 @@ export class LaunchQLMigrate { if (toChange.includes(':@')) { const [crossProject, tag] = toChange.split(':@'); targetProject = crossProject; - const parentDir = dirname(dirname(packageDir)); - const targetPlanPath = join(parentDir, 'packages', crossProject, 'launchql.plan'); try { - const resolvedChange = resolveTagToChangeName(targetPlanPath, `@${tag}`, crossProject); - targetChangeName = resolvedChange; - resolvedToChange = resolvedChange; + if (!launchqlProject) { + const { LaunchQLProject } = await import('../core/class/launchql'); + launchqlProject = new LaunchQLProject(packageDir); + } + + const moduleMap = launchqlProject.getModuleMap(); + const targetModule = moduleMap[crossProject]; + const workspacePath = launchqlProject.getWorkspacePath(); + + if (targetModule && workspacePath) { + const targetPlanPath = join(workspacePath, targetModule.path, 'launchql.plan'); + const resolvedChange = resolveTagToChangeName(targetPlanPath, `@${tag}`, crossProject); + targetChangeName = resolvedChange; + resolvedToChange = resolvedChange; + } else { + resolvedToChange = toChange; + targetChangeName = toChange; + } } catch (error) { resolvedToChange = toChange; targetChangeName = toChange; From d1352ceb791af727982fb4c030dfad8944ec668c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 20:42:49 +0000 Subject: [PATCH 8/9] refactor: fix type and import issues in client.ts - Replace any types with proper DependencyResult interface - Move LaunchQLProject import to top of file instead of async import - Export DependencyResult interface from deps.ts Co-Authored-By: Dan Lynch --- packages/core/src/migrate/client.ts | 9 ++++----- packages/core/src/resolution/deps.ts | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/core/src/migrate/client.ts b/packages/core/src/migrate/client.ts index 145cd88ac..79c35351b 100644 --- a/packages/core/src/migrate/client.ts +++ b/packages/core/src/migrate/client.ts @@ -15,11 +15,12 @@ import { StatusResult } from './types'; import { parsePlanFileSimple as parsePlanFile, parsePlanFile as parsePlanFileFull, Change, readScript, scriptExists } from '../files'; -import { resolveDependencies } from '../resolution/deps'; +import { resolveDependencies, DependencyResult } from '../resolution/deps'; import { resolveTagToChangeName } from '../resolution/resolve'; import { hashFile } from './utils/hash'; import { cleanSql } from './clean'; import { withTransaction, executeQuery, TransactionContext } from './utils/transaction'; +import { LaunchQLProject } from '../core/class/launchql'; // Helper function to get changes in order function getChangesInOrder(planPath: string, reverse: boolean = false): Change[] { @@ -219,10 +220,9 @@ export class LaunchQLMigrate { change.dependencies.some((dep: string) => dep.includes('@')) ) || (toChange && toChange.includes(':@')); - let resolvedDeps: any = null; - let launchqlProject: any = null; + let resolvedDeps: DependencyResult | null = null; + let launchqlProject: LaunchQLProject | null = null; if (hasTagDependencies) { - const { LaunchQLProject } = await import('../core/class/launchql'); launchqlProject = new LaunchQLProject(packageDir); const workspacePath = launchqlProject.getWorkspacePath(); @@ -245,7 +245,6 @@ export class LaunchQLMigrate { try { if (!launchqlProject) { - const { LaunchQLProject } = await import('../core/class/launchql'); launchqlProject = new LaunchQLProject(packageDir); } diff --git a/packages/core/src/resolution/deps.ts b/packages/core/src/resolution/deps.ts index 59ece0c5b..4b7ddd7a4 100644 --- a/packages/core/src/resolution/deps.ts +++ b/packages/core/src/resolution/deps.ts @@ -16,7 +16,7 @@ interface DependencyGraph { /** * Result object returned by dependency resolution functions */ -interface DependencyResult { +export interface DependencyResult { /** Array of external dependencies that are not part of the current project */ external: string[]; /** Array of modules in topologically sorted order for deployment */ From 0b984ed2b95acfbcba9296107d61a585bf7d387b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 21:18:29 +0000 Subject: [PATCH 9/9] feat: add debug-mode enhanced error handling for PostgreSQL transaction failures - Add MigrationError utility class with contextual error information - Enhance executeQuery and withTransaction with debug-mode error context - Improve stored procedure error messages with change/project context - Add debug schema enhancements for error tracking - Update deployment error handling with detailed context in debug mode - All enhancements are debug-mode only (LAUNCHQL_DEBUG=true or NODE_ENV=development) Co-Authored-By: Dan Lynch --- packages/core/src/index.ts | 1 + packages/core/src/migrate/client.ts | 55 ++++++- .../core/src/migrate/sql/debug-schema.sql | 23 +++ packages/core/src/migrate/sql/procedures.sql | 23 ++- packages/core/src/migrate/utils/errors.ts | 151 ++++++++++++++++++ .../core/src/migrate/utils/transaction.ts | 54 ++++++- packages/core/src/projects/deploy.ts | 106 ++++++++++-- 7 files changed, 393 insertions(+), 20 deletions(-) create mode 100644 packages/core/src/migrate/sql/debug-schema.sql create mode 100644 packages/core/src/migrate/utils/errors.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ff526dd9d..28b6390bb 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -39,3 +39,4 @@ export { export { hashFile, hashString } from './migrate/utils/hash'; export { withTransaction, TransactionContext, TransactionOptions, executeQuery } from './migrate/utils/transaction'; export { cleanSql } from './migrate/clean'; +export { MigrationError, MigrationErrorContext, isDebugMode, enhanceErrorWithContext } from './migrate/utils/errors'; diff --git a/packages/core/src/migrate/client.ts b/packages/core/src/migrate/client.ts index 79c35351b..ac621f6d8 100644 --- a/packages/core/src/migrate/client.ts +++ b/packages/core/src/migrate/client.ts @@ -81,6 +81,28 @@ export class LaunchQLMigrate { log.success('Migration schema found and ready'); } + const isDebugMode = process.env.LAUNCHQL_DEBUG === 'true' || process.env.NODE_ENV === 'development'; + if (isDebugMode) { + try { + await this.pool.query(` + CREATE TABLE IF NOT EXISTS launchql_migrate.debug_mode_enabled ( + enabled BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW() + ); + INSERT INTO launchql_migrate.debug_mode_enabled (enabled) + VALUES (TRUE) ON CONFLICT DO NOTHING; + `); + + const debugSchemaPath = join(__dirname, 'sql', 'debug-schema.sql'); + const debugSchemaSql = readFileSync(debugSchemaPath, 'utf8'); + await this.pool.query(debugSchemaSql); + + log.debug('Debug mode schema enhancements applied'); + } catch (debugError) { + log.warn('Failed to apply debug schema enhancements:', debugError); + } + } + this.initialized = true; } catch (error) { log.error('Failed to initialize migration schema:', error); @@ -187,10 +209,35 @@ export class LaunchQLMigrate { deployed.push(change.name); log.success(`Successfully deployed: ${change.name}`); - } catch (error) { - log.error(`Failed to deploy ${change.name}:`, error); - failed = change.name; - throw error; // Re-throw to trigger rollback if in transaction + } catch (error: any) { + const isDebugMode = process.env.LAUNCHQL_DEBUG === 'true' || process.env.NODE_ENV === 'development'; + + if (isDebugMode) { + const contextualError = new Error(`Failed to deploy change '${change.name}' in project '${project || plan.project}': ${error.message}`); + contextualError.stack = error.stack; + (contextualError as any).code = error.code; + (contextualError as any).originalError = error; + (contextualError as any).changeName = change.name; + (contextualError as any).projectName = project || plan.project; + (contextualError as any).changeKey = changeKey; + (contextualError as any).scriptHash = scriptHash; + + log.error(`Failed to deploy ${change.name}:`, { + message: contextualError.message, + changeName: change.name, + projectName: project || plan.project, + changeKey, + scriptHash, + originalError: error.message, + code: error.code + }); + failed = change.name; + throw contextualError; + } else { + log.error(`Failed to deploy ${change.name}:`, error); + failed = change.name; + throw error; // Re-throw to trigger rollback if in transaction + } } // Stop if this was the target change diff --git a/packages/core/src/migrate/sql/debug-schema.sql b/packages/core/src/migrate/sql/debug-schema.sql new file mode 100644 index 000000000..33cba846b --- /dev/null +++ b/packages/core/src/migrate/sql/debug-schema.sql @@ -0,0 +1,23 @@ + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = 'launchql_migrate' + AND table_name = 'debug_mode_enabled') THEN + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'launchql_migrate' + AND table_name = 'events' + AND column_name = 'error_message') THEN + ALTER TABLE launchql_migrate.events ADD COLUMN error_message TEXT; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'launchql_migrate' + AND table_name = 'events' + AND column_name = 'error_code') THEN + ALTER TABLE launchql_migrate.events ADD COLUMN error_code TEXT; + END IF; + + END IF; +END $$; diff --git a/packages/core/src/migrate/sql/procedures.sql b/packages/core/src/migrate/sql/procedures.sql index f4488c0e9..21aaa70d8 100644 --- a/packages/core/src/migrate/sql/procedures.sql +++ b/packages/core/src/migrate/sql/procedures.sql @@ -97,7 +97,15 @@ BEGIN EXCEPTION WHEN OTHERS THEN INSERT INTO launchql_migrate.events (event_type, change_name, project) VALUES ('fail', p_change_name, p_project); - RAISE; + + IF EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = 'launchql_migrate' + AND table_name = 'debug_mode_enabled') THEN + RAISE EXCEPTION 'Deploy failed for change "%" in project "%": % (SQL State: %)', + p_change_name, p_project, SQLERRM, SQLSTATE; + ELSE + RAISE; + END IF; END; -- Record deployment @@ -165,7 +173,18 @@ BEGIN END IF; -- Execute revert - EXECUTE p_revert_sql; + BEGIN + EXECUTE p_revert_sql; + EXCEPTION WHEN OTHERS THEN + IF EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = 'launchql_migrate' + AND table_name = 'debug_mode_enabled') THEN + RAISE EXCEPTION 'Revert failed for change "%" in project "%": % (SQL State: %)', + p_change_name, p_project, SQLERRM, SQLSTATE; + ELSE + RAISE; + END IF; + END; -- Remove from deployed DELETE FROM launchql_migrate.changes diff --git a/packages/core/src/migrate/utils/errors.ts b/packages/core/src/migrate/utils/errors.ts new file mode 100644 index 000000000..f25687ccd --- /dev/null +++ b/packages/core/src/migrate/utils/errors.ts @@ -0,0 +1,151 @@ +/** + * Centralized error handling utilities for the migration system + * Enhanced error context and logging are only active in debug mode + */ + +export interface MigrationErrorContext { + changeName?: string; + projectName?: string; + modulePath?: string; + sqlQuery?: string; + sqlParams?: any[]; + errorCode?: string; + changeKey?: string; + scriptHash?: string; +} + +export class MigrationError extends Error { + public readonly code?: string; + public readonly originalError?: Error; + public readonly context: MigrationErrorContext; + public readonly isDebugMode: boolean; + + constructor(message: string, context: MigrationErrorContext = {}, originalError?: Error) { + super(message); + this.name = 'MigrationError'; + this.context = context; + this.originalError = originalError; + this.code = (originalError as any)?.code || context.errorCode; + this.isDebugMode = process.env.LAUNCHQL_DEBUG === 'true' || process.env.NODE_ENV === 'development'; + + if (originalError?.stack) { + this.stack = originalError.stack; + } + } + + public getDetailedMessage(): string { + if (!this.isDebugMode) { + return this.message; + } + + const parts = [this.message]; + + if (this.context.changeName) { + parts.push(`Change: ${this.context.changeName}`); + } + if (this.context.projectName) { + parts.push(`Project: ${this.context.projectName}`); + } + if (this.context.modulePath) { + parts.push(`Module: ${this.context.modulePath}`); + } + if (this.context.changeKey) { + parts.push(`Change Key: ${this.context.changeKey}`); + } + if (this.context.scriptHash) { + parts.push(`Script Hash: ${this.context.scriptHash}`); + } + if (this.context.sqlQuery) { + parts.push(`SQL: ${this.context.sqlQuery}`); + } + if (this.context.sqlParams) { + parts.push(`Params: ${JSON.stringify(this.context.sqlParams)}`); + } + if (this.code) { + parts.push(`Error Code: ${this.code}`); + } + + return parts.join('\n '); + } + + public getContextualProperties(): Record { + if (!this.isDebugMode) { + return {}; + } + + return { + ...this.context, + code: this.code, + originalError: this.originalError + }; + } + + public static createDeploymentError( + changeName: string, + projectName: string, + originalError: Error, + additionalContext: Partial = {} + ): MigrationError { + const context: MigrationErrorContext = { + changeName, + projectName, + ...additionalContext + }; + + return new MigrationError( + `Failed to deploy change '${changeName}' in project '${projectName}': ${originalError.message}`, + context, + originalError + ); + } + + public static createRevertError( + changeName: string, + projectName: string, + originalError: Error, + additionalContext: Partial = {} + ): MigrationError { + const context: MigrationErrorContext = { + changeName, + projectName, + ...additionalContext + }; + + return new MigrationError( + `Failed to revert change '${changeName}' in project '${projectName}': ${originalError.message}`, + context, + originalError + ); + } + + public static createTransactionError( + originalError: Error, + additionalContext: Partial = {} + ): MigrationError { + const context: MigrationErrorContext = { + ...additionalContext + }; + + let message = `Transaction failed: ${originalError.message}`; + if ((originalError as any).code === '25P02') { + message = `Transaction aborted due to previous error: ${originalError.message}`; + } + + return new MigrationError(message, context, originalError); + } +} + +export function isDebugMode(): boolean { + return process.env.LAUNCHQL_DEBUG === 'true' || process.env.NODE_ENV === 'development'; +} + +export function enhanceErrorWithContext( + error: Error, + context: MigrationErrorContext +): MigrationError { + if (error instanceof MigrationError) { + return error; + } + + return new MigrationError(error.message, context, error); +} diff --git a/packages/core/src/migrate/utils/transaction.ts b/packages/core/src/migrate/utils/transaction.ts index 2672ff59d..d25a3922d 100644 --- a/packages/core/src/migrate/utils/transaction.ts +++ b/packages/core/src/migrate/utils/transaction.ts @@ -41,9 +41,39 @@ export async function withTransaction( log.debug('Transaction committed successfully'); return result; - } catch (error) { - await client.query('ROLLBACK'); - log.error('Transaction rolled back due to error:', error); + } catch (error: any) { + const isDebugMode = process.env.LAUNCHQL_DEBUG === 'true' || process.env.NODE_ENV === 'development'; + + try { + await client.query('ROLLBACK'); + } catch (rollbackError) { + log.error('Failed to rollback transaction:', rollbackError); + } + + if (isDebugMode) { + if (error.code === '25P02') { + log.error('Transaction aborted - all subsequent commands ignored until rollback. Original error:', error.originalError || error); + const enhancedError = new Error(`Transaction aborted due to previous error. ${error.message}`); + (enhancedError as any).code = error.code; + (enhancedError as any).originalError = error; + (enhancedError as any).transactionState = 'aborted'; + throw enhancedError; + } + + if (error.originalError || error.sqlQuery) { + log.error('Transaction rolled back due to enhanced error:', { + message: error.message, + code: error.code, + sqlQuery: error.sqlQuery, + sqlParams: error.sqlParams + }); + } else { + log.error('Transaction rolled back due to error:', error); + } + } else { + log.error('Transaction rolled back due to error:', error); + } + throw error; } finally { client.release(); @@ -58,5 +88,21 @@ export async function executeQuery( query: string, params?: any[] ): Promise { - return context.client.query(query, params); + const isDebugMode = process.env.LAUNCHQL_DEBUG === 'true' || process.env.NODE_ENV === 'development'; + + if (!isDebugMode) { + return context.client.query(query, params); + } + + try { + return await context.client.query(query, params); + } catch (error: any) { + const enhancedError = new Error(`SQL execution failed: ${error.message}\nQuery: ${query}\nParams: ${JSON.stringify(params)}`); + enhancedError.stack = error.stack; + (enhancedError as any).code = error.code; + (enhancedError as any).originalError = error; + (enhancedError as any).sqlQuery = query; + (enhancedError as any).sqlParams = params; + throw enhancedError; + } } diff --git a/packages/core/src/projects/deploy.ts b/packages/core/src/projects/deploy.ts index cffe68b3c..0faeb2924 100644 --- a/packages/core/src/projects/deploy.ts +++ b/packages/core/src/projects/deploy.ts @@ -108,13 +108,30 @@ export const deployProject = async ( usePlan: options?.usePlan ?? true, extension: false }); - } catch (err) { + } catch (err: any) { + const isDebugMode = process.env.LAUNCHQL_DEBUG === 'true' || process.env.NODE_ENV === 'development'; + log.error(`❌ Failed to package module "${extension}" at path: ${modulePath}`); log.error(` Error: ${err instanceof Error ? err.message : String(err)}`); - console.error(err); // Preserve full stack trace + + if (isDebugMode) { + log.error(` Module path: ${modulePath}`); + log.error(` Extension: ${extension}`); + if (err.code) { + log.error(` Error code: ${err.code}`); + } + if (err.stack) { + log.error(` Stack trace: ${err.stack}`); + } + console.error('Full packaging error context:', err); + } else { + console.error(err); // Preserve full stack trace + } + throw errors.DEPLOYMENT_FAILED({ type: 'Deployment', - module: extension + module: extension, + ...(isDebugMode && { originalError: err }) }); } @@ -142,9 +159,28 @@ export const deployProject = async ( log.error(`❌ Deployment failed for module ${extension}`); throw errors.DEPLOYMENT_FAILED({ type: 'Deployment', module: extension }); } - } catch (err) { + } catch (err: any) { + const isDebugMode = process.env.LAUNCHQL_DEBUG === 'true' || process.env.NODE_ENV === 'development'; + log.error(`❌ Deployment failed for module ${extension}`); - throw errors.DEPLOYMENT_FAILED({ type: 'Deployment', module: extension }); + + if (isDebugMode) { + log.error(` Module path: ${modulePath}`); + log.error(` Error: ${err instanceof Error ? err.message : String(err)}`); + if (err.code) { + log.error(` Error code: ${err.code}`); + } + if (err.changeName) { + log.error(` Failed change: ${err.changeName}`); + } + console.error('Full sqitch deployment error context:', err); + } + + throw errors.DEPLOYMENT_FAILED({ + type: 'Deployment', + module: extension, + ...(isDebugMode && { originalError: err }) + }); } } else { // Use new migration system @@ -155,16 +191,66 @@ export const deployProject = async ( useTransaction: options?.useTransaction, toChange: options?.toChange }); - } catch (deployError) { + } catch (deployError: any) { + const isDebugMode = process.env.LAUNCHQL_DEBUG === 'true' || process.env.NODE_ENV === 'development'; + log.error(`❌ Deployment failed for module ${extension}`); - throw errors.DEPLOYMENT_FAILED({ type: 'Deployment', module: extension }); + + if (isDebugMode) { + log.error(` Module path: ${modulePath}`); + log.error(` Error: ${deployError instanceof Error ? deployError.message : String(deployError)}`); + if (deployError.code) { + log.error(` Error code: ${deployError.code}`); + } + if (deployError.changeName) { + log.error(` Failed change: ${deployError.changeName}`); + } + if (deployError.projectName) { + log.error(` Project: ${deployError.projectName}`); + } + if (deployError.sqlQuery) { + log.error(` SQL Query: ${deployError.sqlQuery}`); + } + if (deployError.sqlParams) { + log.error(` SQL Params: ${JSON.stringify(deployError.sqlParams)}`); + } + console.error('Full migration deployment error context:', deployError); + } + + throw errors.DEPLOYMENT_FAILED({ + type: 'Deployment', + module: extension, + ...(isDebugMode && { originalError: deployError }) + }); } } } - } catch (err) { + } catch (err: any) { + const isDebugMode = process.env.LAUNCHQL_DEBUG === 'true' || process.env.NODE_ENV === 'development'; + log.error(`🛑 Error during deployment: ${err instanceof Error ? err.message : err}`); - console.error(err); // Keep raw error output for stack traces - throw errors.DEPLOYMENT_FAILED({ type: 'Deployment', module: extension }); + + if (isDebugMode) { + log.error(` Module: ${extension}`); + if (err.code) { + log.error(` Error code: ${err.code}`); + } + if (err.changeName) { + log.error(` Failed change: ${err.changeName}`); + } + if (err.projectName) { + log.error(` Project: ${err.projectName}`); + } + console.error('Full deployment error context:', err); + } else { + console.error(err); // Keep raw error output for stack traces + } + + throw errors.DEPLOYMENT_FAILED({ + type: 'Deployment', + module: extension, + ...(isDebugMode && { originalError: err }) + }); } }