Skip to content
Closed
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
26 changes: 26 additions & 0 deletions packages/core/ISSUES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# 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?

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

### Deployment Failure Recovery
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ exports[`sqitch package dependencies [simple/1st] 1`] = `
"table_users",
"table_products",
],
"resolvedTags": {},
}
`;

Expand All @@ -43,6 +44,7 @@ exports[`sqitch package dependencies [simple/2nd] 1`] = `
"create_table",
"create_another_table",
],
"resolvedTags": {},
}
`;

Expand All @@ -64,6 +66,7 @@ exports[`sqitch package dependencies [simple/3rd] 1`] = `
"create_schema",
"create_table",
],
"resolvedTags": {},
}
`;

Expand All @@ -76,5 +79,6 @@ exports[`sqitch package dependencies [utils] 1`] = `
"resolved": [
"procedures/myfunction",
],
"resolvedTags": {},
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ exports[`sqitch package dependencies with internal tag resolution [simple-w-tags
"table_users",
"table_products",
],
"resolvedTags": {},
}
`;

Expand Down Expand Up @@ -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",
},
}
`;

Expand Down Expand Up @@ -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",
},
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ exports[`sqitch package dependencies with resolved tags [simple-w-tags/1st] 1`]
"table_users",
"table_products",
],
"resolvedTags": {},
}
`;

Expand Down Expand Up @@ -49,6 +50,7 @@ exports[`sqitch package dependencies with resolved tags [simple-w-tags/2nd] 1`]
"create_table",
"create_another_table",
],
"resolvedTags": {},
}
`;

Expand Down Expand Up @@ -76,5 +78,6 @@ exports[`sqitch package dependencies with resolved tags [simple-w-tags/3rd] 1`]
"create_schema",
"create_table",
],
"resolvedTags": {},
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ exports[`sqitch package dependencies [simple-w-tags/1st] 1`] = `
"table_users",
"table_products",
],
"resolvedTags": {},
}
`;

Expand Down Expand Up @@ -49,5 +50,6 @@ exports[`sqitch package dependencies [simple-w-tags/2nd] 1`] = `
"create_table",
"create_another_table",
],
"resolvedTags": {},
}
`;
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
176 changes: 168 additions & 8 deletions packages/core/src/migrate/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] {
Expand Down Expand Up @@ -80,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);
Expand Down Expand Up @@ -186,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
Expand All @@ -210,7 +258,66 @@ 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: DependencyResult | null = null;
let launchqlProject: LaunchQLProject | null = null;
if (hasTagDependencies) {
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;
let targetProject = project || plan.project;
let targetChangeName = toChange;

if (toChange && toChange.includes('@')) {
if (toChange.includes(':@')) {
const [crossProject, tag] = toChange.split(':@');
targetProject = crossProject;

try {
if (!launchqlProject) {
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;
}
} else {
resolvedToChange = resolveTagToChangeName(planPath, toChange, project || plan.project);
targetChangeName = resolvedToChange;
}
}

const changes = getChangesInOrder(planPath, true); // Reverse order for revert

const reverted: string[] = [];
Expand All @@ -227,8 +334,61 @@ 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];

if (resolvedTag.includes(':')) {
const [resolvedProject, resolvedChange] = resolvedTag.split(':', 2);
actualTargetProject = resolvedProject;
actualTargetChangeName = resolvedChange;
} else {
actualTargetChangeName = resolvedTag;
}
}

const targetDeployedResult = await executeQuery(
context,
'SELECT launchql_migrate.is_deployed($1, $2) as is_deployed',
[actualTargetProject, actualTargetChangeName]
);

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
Expand Down
23 changes: 23 additions & 0 deletions packages/core/src/migrate/sql/debug-schema.sql
Original file line number Diff line number Diff line change
@@ -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 $$;
Loading