Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ exports[`Deploy Failure Scenarios constraint violation with transaction - automa
{
"changeCount": 0,
"changes": [],
"eventCount": 0,
"events": [],
"eventCount": 1,
"events": [
{
"change_name": "violate_constraint",
"error_code": "23505",
"error_message": "duplicate key value violates unique constraint "test_users_email_key"",
"event_type": "deploy",
"project": "test-constraint-fail",
},
],
}
`;

Expand All @@ -24,58 +32,118 @@ exports[`Deploy Failure Scenarios constraint violation without transaction - par
"script_hash": "833d7d349e3c4f07e1a24ed40ac9814329efc87c180180342a09874f8124a037",
},
],
"eventCount": 2,
"eventCount": 3,
"events": [
{
"change_name": "create_table",
"error_code": null,
"error_message": null,
"event_type": "deploy",
"project": "test-constraint-partial",
},
{
"change_name": "add_record",
"error_code": null,
"error_message": null,
"event_type": "deploy",
"project": "test-constraint-partial",
},
{
"change_name": "violate_constraint",
"error_code": "23505",
"error_message": "duplicate key value violates unique constraint "test_products_sku_key"",
"event_type": "deploy",
"project": "test-constraint-partial",
},
],
}
`;

exports[`Deploy Failure Scenarios verify database state after constraint failure: partial-deployment-state-comparison 1`] = `
exports[`Deploy Failure Scenarios non-transaction mode - partial deployment on constraint failure: non-transaction-mode-constraint-failure 1`] = `
{
"changeCount": 2,
"changes": [
{
"change_name": "setup_schema",
"project": "test-state-check",
"project": "test-nontransaction-partial",
"script_hash": "a3419a48994fd13a668befcaab23c4d0d7e9e08e6e6a9093effb3c85b7e953d9",
},
{
"change_name": "create_constraint_table",
"project": "test-state-check",
"project": "test-nontransaction-partial",
"script_hash": "ec5b17e155a2cd4e098716204192083d31d096a4cf163550d5ea176a4615a4d2",
},
],
"eventCount": 2,
"eventCount": 3,
"events": [
{
"change_name": "setup_schema",
"error_code": null,
"error_message": null,
"event_type": "deploy",
"project": "test-state-check",
"project": "test-nontransaction-partial",
},
{
"change_name": "create_constraint_table",
"error_code": null,
"error_message": null,
"event_type": "deploy",
"project": "test-nontransaction-partial",
},
{
"change_name": "fail_on_constraint",
"error_code": "23514",
"error_message": "new row for relation "orders" violates check constraint "orders_amount_check"",
"event_type": "deploy",
"project": "test-state-check",
"project": "test-nontransaction-partial",
},
],
}
`;

exports[`Deploy Failure Scenarios verify database state after constraint failure: transaction-rollback-state-comparison 1`] = `
exports[`Deploy Failure Scenarios transaction mode - complete rollback on constraint failure: transaction-mode-constraint-failure 1`] = `
{
"changeCount": 0,
"changes": [],
"eventCount": 0,
"events": [],
"eventCount": 1,
"events": [
{
"change_name": "fail_on_constraint",
"error_code": "23514",
"error_message": "new row for relation "orders" violates check constraint "orders_amount_check"",
"event_type": "deploy",
"project": "test-transaction-rollback",
},
],
}
`;

exports[`Deploy Failure Scenarios verify failure - non-existent table reference: verify-failure-non-existent-table 1`] = `
{
"changeCount": 1,
"changes": [
{
"change_name": "create_simple_table",
"project": "test-verify-fail",
"script_hash": "f5f0794a55d611246115a67e39747c887da6d6f83d79f63c3aa730fa97772942",
},
],
"eventCount": 2,
"events": [
{
"change_name": "create_simple_table",
"error_code": null,
"error_message": null,
"event_type": "deploy",
"project": "test-verify-fail",
},
{
"change_name": "create_simple_table",
"error_code": "VERIFICATION_FAILED",
"error_message": "Verification failed for create_simple_table",
"event_type": "verify",
"project": "test-verify-fail",
},
],
}
`;
163 changes: 134 additions & 29 deletions packages/core/__tests__/projects/deploy-failure-scenarios.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ describe('Deploy Failure Scenarios', () => {
expect(finalState).toMatchSnapshot('transaction-rollback-migration-state');

expect(finalState.changeCount).toBe(0);
expect(finalState.eventCount).toBe(0); // Complete rollback - no events logged
expect(finalState.eventCount).toBe(1); // Now expect deploy failure event to be logged
expect(finalState.events[0].event_type).toBe('deploy');
expect(finalState.events[0].error_message).toContain('duplicate key value violates unique constraint');

expect(await db.exists('table', 'test_users')).toBe(false);
});
Expand Down Expand Up @@ -135,23 +137,27 @@ describe('Deploy Failure Scenarios', () => {
const finalRecord = await db.query("SELECT * FROM test_products WHERE sku = 'PROD-002'");
expect(finalRecord.rows).toHaveLength(0);

const successEvents = finalState.events.filter((e: any) => e.event_type === 'deploy');
const successEvents = finalState.events.filter((e: any) => e.event_type === 'deploy' && !e.error_message);
expect(successEvents.length).toBe(2); // create_table, add_record
expect(finalState.eventCount).toBe(2); // Only successful deployments logged
const failEvents = finalState.events.filter((e: any) => e.event_type === 'deploy' && e.error_message);
expect(failEvents.length).toBe(1); // violate_constraint failure logged
expect(finalState.eventCount).toBe(3); // 2 successful deployments + 1 failure
});

test('verify database state after constraint failure', async () => {
test('transaction mode - complete rollback on constraint failure', async () => {
/*
* SCENARIO: Comparison of transaction vs non-transaction behavior
* SCENARIO: Transaction-based deployment with constraint failure
*
* This test demonstrates the key difference between transaction and non-transaction
* deployment modes when failures occur. It shows how the same failure scenario
* results in completely different database states.
* This test demonstrates LaunchQL's complete rollback behavior when useTransaction: true (default).
* When ANY change fails during deployment, ALL changes are automatically rolled back.
*
* Transaction mode: Complete rollback (clean state)
* Non-transaction mode: Partial deployment (mixed state requiring cleanup)
* Expected behavior:
* - All 3 changes attempted in single transaction
* - Constraint violation on 3rd change triggers complete rollback
* - Database state: clean (as if deployment never happened)
* - Migration tracking: zero deployed changes, failure event logged outside transaction
*/
const tempDir = fixture.createPlanFile('test-state-check', [
const tempDir = fixture.createPlanFile('test-transaction-rollback', [
{ name: 'setup_schema' },
{ name: 'create_constraint_table', dependencies: ['setup_schema'] },
{ name: 'fail_on_constraint', dependencies: ['create_constraint_table'] }
Expand All @@ -176,50 +182,149 @@ describe('Deploy Failure Scenarios', () => {
useTransaction: true
})).rejects.toThrow(/violates check constraint/);

const transactionState = await db.getMigrationState();
const finalState = await db.getMigrationState();

expect(transactionState).toMatchSnapshot('transaction-rollback-state-comparison');
expect(finalState).toMatchSnapshot('transaction-mode-constraint-failure');

expect(await db.exists('schema', 'test_schema')).toBe(false);
expect(transactionState.changeCount).toBe(0);
expect(finalState.changeCount).toBe(0);
expect(finalState.eventCount).toBe(1); // Deploy failure event logged outside transaction
expect(finalState.events[0].event_type).toBe('deploy');
expect(finalState.events[0].error_message).toContain('violates check constraint');

/*
* KEY INSIGHT: Transaction mode provides complete rollback
*
* - launchql_migrate.changes: 0 rows (complete rollback)
* - launchql_migrate.events: 1 failure event (logged outside transaction)
* - Database objects: none (clean state)
*
* RECOMMENDATION: Use transaction mode (default) for atomic deployments
* where you want all-or-nothing behavior.
*/
});

test('non-transaction mode - partial deployment on constraint failure', async () => {
/*
* SCENARIO: Non-transaction deployment with constraint failure
*
* This test demonstrates LaunchQL's partial deployment behavior when useTransaction: false.
* Each change is deployed individually - successful changes remain deployed
* even when later changes fail. Deployment stops at first failure.
*
* Expected behavior:
* - Changes deployed one-by-one (no transaction wrapper)
* - First 2 changes succeed and remain deployed
* - 3rd change fails on constraint violation, deployment stops
* - Database state: partial (successful changes persist)
* - Migration tracking: shows successful deployments + failure event
*/
const tempDir = fixture.createPlanFile('test-nontransaction-partial', [
{ name: 'setup_schema' },
{ name: 'create_constraint_table', dependencies: ['setup_schema'] },
{ name: 'fail_on_constraint', dependencies: ['create_constraint_table'] }
]);

fixture.createScript(tempDir, 'deploy', 'setup_schema',
'CREATE SCHEMA test_schema;'
);

fixture.createScript(tempDir, 'deploy', 'create_constraint_table',
'CREATE TABLE test_schema.orders (id SERIAL PRIMARY KEY, amount DECIMAL(10,2) CHECK (amount > 0));'
);

fixture.createScript(tempDir, 'deploy', 'fail_on_constraint',
'INSERT INTO test_schema.orders (amount) VALUES (-100.00);'
);

const client = new LaunchQLMigrate(db.config);

await expect(client.deploy({
modulePath: tempDir,
useTransaction: false
})).rejects.toThrow(/violates check constraint/);

const partialState = await db.getMigrationState();
const finalState = await db.getMigrationState();

expect(partialState).toMatchSnapshot('partial-deployment-state-comparison');
expect(finalState).toMatchSnapshot('non-transaction-mode-constraint-failure');

expect(await db.exists('schema', 'test_schema')).toBe(true);
expect(await db.exists('table', 'test_schema.orders')).toBe(true);

const records = await db.query('SELECT * FROM test_schema.orders');
expect(records.rows).toHaveLength(0);

expect(partialState.changeCount).toBe(2);
expect(partialState.changes.map((c: any) => c.change_name)).toEqual(['setup_schema', 'create_constraint_table']);
expect(finalState.changeCount).toBe(2);
expect(finalState.changes.map((c: any) => c.change_name)).toEqual(['setup_schema', 'create_constraint_table']);

const successEvents = partialState.events.filter((e: any) => e.event_type === 'deploy');
const successEvents = finalState.events.filter((e: any) => e.event_type === 'deploy' && !e.error_message);
expect(successEvents.length).toBe(2); // setup_schema, create_constraint_table
expect(partialState.eventCount).toBe(2); // Only successful deployments logged
const failEvents = finalState.events.filter((e: any) => e.event_type === 'deploy' && e.error_message);
expect(failEvents.length).toBe(1); // fail_on_constraint failure logged
expect(finalState.eventCount).toBe(3); // 2 successful deployments + 1 failure

/*
* KEY INSIGHT: Same failure scenario, different outcomes
* KEY INSIGHT: Non-transaction mode provides partial deployment
*
* Transaction mode:
* - launchql_migrate.changes: 0 rows (complete rollback)
* - launchql_migrate.events: failure events only
* - Database objects: none (clean state)
*
* Non-transaction mode:
* - launchql_migrate.changes: 2 rows (partial success)
* - launchql_migrate.events: mix of success + failure events
* - launchql_migrate.events: 2 success + 1 failure event
* - Database objects: schema + table exist (mixed state)
*
* RECOMMENDATION: Use transaction mode (default) unless you specifically
* IMPORTANT: Deployment stops immediately at first failure, just like transaction mode.
* The difference is in state persistence, not error handling behavior.
*
* RECOMMENDATION: Use non-transaction mode only when you specifically
* need partial deployment behavior for incremental rollout scenarios.
*/
});

test('verify failure - non-existent table reference', async () => {
/*
* SCENARIO: Verify script references a table that doesn't exist
*
* This test demonstrates LaunchQL's verify failure behavior when a verify script
* tries to check a table that was never created or was dropped.
*
* Expected behavior:
* - Deploy succeeds (creates a simple table)
* - Verify fails because script references non-existent table
* - Failure event logged with detailed error information
*/
const tempDir = fixture.createPlanFile('test-verify-fail', [
{ name: 'create_simple_table' }
]);

fixture.createScript(tempDir, 'deploy', 'create_simple_table',
'CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(100));'
);

fixture.createScript(tempDir, 'verify', 'create_simple_table',
'SELECT 1 FROM non_existent_table LIMIT 1;'
);

const client = new LaunchQLMigrate(db.config);

await client.deploy({
modulePath: tempDir,
useTransaction: true
});

const deployState = await db.getMigrationState();
expect(deployState.changeCount).toBe(1);
expect(await db.exists('table', 'users')).toBe(true);

await expect(client.verify({
modulePath: tempDir
})).rejects.toThrow('Verification failed for 1 change(s): create_simple_table');

const finalState = await db.getMigrationState();

expect(finalState).toMatchSnapshot('verify-failure-non-existent-table');

// Should have deploy success event + verify failure event
const verifyEvents = finalState.events.filter((e: any) => e.event_type === 'verify');
expect(verifyEvents.length).toBe(1);
expect(verifyEvents[0].error_message).toBe('Verification failed for create_simple_table');
expect(verifyEvents[0].error_code).toBe('VERIFICATION_FAILED');
});
});
Loading