mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
620 lines
24 KiB
TypeScript
620 lines
24 KiB
TypeScript
import {
|
|
createTestMigrationContext,
|
|
initDbUpToMigration,
|
|
runSingleMigration,
|
|
undoLastSingleMigration,
|
|
type TestMigrationContext,
|
|
} from '@n8n/backend-test-utils';
|
|
import { DbConnection } from '@n8n/db';
|
|
import { Container } from '@n8n/di';
|
|
import { DataSource } from '@n8n/typeorm';
|
|
import { nanoid } from 'nanoid';
|
|
|
|
const MIGRATION_NAME = 'ChangeWorkflowStatisticsFKToNoAction1767018516000';
|
|
|
|
/**
|
|
* Generate parameter placeholders for a given context and count.
|
|
* PostgreSQL uses $1, $2, ... while SQLite use ?
|
|
*/
|
|
function getParamPlaceholders(context: TestMigrationContext, count: number): string {
|
|
if (context.isPostgres) {
|
|
return Array.from({ length: count }, (_, i) => `$${i + 1}`).join(', ');
|
|
}
|
|
return Array.from({ length: count }, () => '?').join(', ');
|
|
}
|
|
|
|
/**
|
|
* Generate a single parameter placeholder for WHERE clauses
|
|
*/
|
|
function getParamPlaceholder(context: TestMigrationContext, index = 1): string {
|
|
return context.isPostgres ? `$${index}` : '?';
|
|
}
|
|
|
|
describe('ChangeWorkflowStatisticsFKToNoAction Migration', () => {
|
|
let dataSource: DataSource;
|
|
|
|
beforeAll(async () => {
|
|
// Initialize DB connection
|
|
const dbConnection = Container.get(DbConnection);
|
|
await dbConnection.init();
|
|
|
|
dataSource = Container.get(DataSource);
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// Clear database before each test to ensure isolation
|
|
// Note: Migration tests must run sequentially (maxWorkers: 1) to avoid conflicts
|
|
const context = createTestMigrationContext(dataSource);
|
|
await context.queryRunner.clearDatabase();
|
|
await context.queryRunner.release();
|
|
|
|
// Run migrations up to (but not including) target migration
|
|
await initDbUpToMigration(MIGRATION_NAME);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
const dbConnection = Container.get(DbConnection);
|
|
await dbConnection.close();
|
|
});
|
|
|
|
/**
|
|
* Helper function to insert a test workflow
|
|
*/
|
|
async function insertTestWorkflow(
|
|
context: TestMigrationContext,
|
|
workflowId: string,
|
|
workflowName = 'Test Workflow',
|
|
): Promise<void> {
|
|
const tableName = context.escape.tableName('workflow_entity');
|
|
const idColumn = context.escape.columnName('id');
|
|
const nameColumn = context.escape.columnName('name');
|
|
const activeColumn = context.escape.columnName('active');
|
|
const nodesColumn = context.escape.columnName('nodes');
|
|
const connectionsColumn = context.escape.columnName('connections');
|
|
const createdAtColumn = context.escape.columnName('createdAt');
|
|
const updatedAtColumn = context.escape.columnName('updatedAt');
|
|
const triggerCountColumn = context.escape.columnName('triggerCount');
|
|
const versionIdColumn = context.escape.columnName('versionId');
|
|
|
|
const versionId = nanoid();
|
|
await context.runQuery(
|
|
`INSERT INTO ${tableName} (${idColumn}, ${nameColumn}, ${activeColumn}, ${nodesColumn}, ${connectionsColumn}, ${createdAtColumn}, ${updatedAtColumn}, ${triggerCountColumn}, ${versionIdColumn}) VALUES (:id, :name, :active, :nodes, :connections, :createdAt, :updatedAt, :triggerCount, :versionId)`,
|
|
{
|
|
id: workflowId,
|
|
name: workflowName,
|
|
active: false,
|
|
nodes: '[]',
|
|
connections: '{}',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
triggerCount: 0,
|
|
versionId,
|
|
},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Helper function to insert workflow statistics (before migration, without workflowName)
|
|
*/
|
|
async function insertWorkflowStatistics(
|
|
context: TestMigrationContext,
|
|
workflowId: string,
|
|
name: string,
|
|
count: number,
|
|
): Promise<void> {
|
|
const tableName = context.escape.tableName('workflow_statistics');
|
|
const workflowIdColumn = context.escape.columnName('workflowId');
|
|
const nameColumn = context.escape.columnName('name');
|
|
const countColumn = context.escape.columnName('count');
|
|
const latestEventColumn = context.escape.columnName('latestEvent');
|
|
const rootCountColumn = context.escape.columnName('rootCount');
|
|
|
|
await context.runQuery(
|
|
`INSERT INTO ${tableName} (${workflowIdColumn}, ${nameColumn}, ${countColumn}, ${latestEventColumn}, ${rootCountColumn}) VALUES (:workflowId, :name, :count, :latestEvent, :rootCount)`,
|
|
{ workflowId, name, count, latestEvent: new Date(), rootCount: 0 },
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Helper function to insert workflow statistics (after migration, with workflowName)
|
|
*/
|
|
async function insertWorkflowStatisticsWithName(
|
|
context: TestMigrationContext,
|
|
workflowId: string,
|
|
name: string,
|
|
count: number,
|
|
workflowName: string,
|
|
): Promise<void> {
|
|
const tableName = context.escape.tableName('workflow_statistics');
|
|
const workflowIdColumn = context.escape.columnName('workflowId');
|
|
const nameColumn = context.escape.columnName('name');
|
|
const countColumn = context.escape.columnName('count');
|
|
const latestEventColumn = context.escape.columnName('latestEvent');
|
|
const rootCountColumn = context.escape.columnName('rootCount');
|
|
const workflowNameColumn = context.escape.columnName('workflowName');
|
|
|
|
await context.runQuery(
|
|
`INSERT INTO ${tableName} (${workflowIdColumn}, ${nameColumn}, ${countColumn}, ${latestEventColumn}, ${rootCountColumn}, ${workflowNameColumn}) VALUES (:workflowId, :name, :count, :latestEvent, :rootCount, :workflowName)`,
|
|
{ workflowId, name, count, latestEvent: new Date(), rootCount: 0, workflowName },
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Helper function to get workflow statistics by workflowId
|
|
*/
|
|
async function getWorkflowStatistics(
|
|
context: TestMigrationContext,
|
|
workflowId: string,
|
|
): Promise<
|
|
Array<{ workflowId: string; name: string; count: number; workflowName?: string | null }>
|
|
> {
|
|
const tableName = context.escape.tableName('workflow_statistics');
|
|
const workflowIdColumn = context.escape.columnName('workflowId');
|
|
const nameColumn = context.escape.columnName('name');
|
|
const countColumn = context.escape.columnName('count');
|
|
|
|
// Cast count to integer for consistent return type
|
|
const countSelect = context.isPostgres ? `${countColumn}::int` : countColumn;
|
|
|
|
return await context.runQuery(
|
|
`SELECT ${workflowIdColumn}, ${nameColumn}, ${countSelect} as ${countColumn} FROM ${tableName} WHERE ${workflowIdColumn} = :workflowId`,
|
|
{ workflowId },
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Helper function to get workflow statistics by workflowId (after migration, includes workflowName)
|
|
*/
|
|
async function getWorkflowStatisticsWithName(
|
|
context: TestMigrationContext,
|
|
workflowId: string,
|
|
): Promise<
|
|
Array<{ workflowId: string; name: string; count: number; workflowName: string | null }>
|
|
> {
|
|
const tableName = context.escape.tableName('workflow_statistics');
|
|
const workflowIdColumn = context.escape.columnName('workflowId');
|
|
const nameColumn = context.escape.columnName('name');
|
|
const countColumn = context.escape.columnName('count');
|
|
const workflowNameColumn = context.escape.columnName('workflowName');
|
|
|
|
// Cast count to integer for consistent return type
|
|
const countSelect = context.isPostgres ? `${countColumn}::int` : countColumn;
|
|
|
|
return await context.runQuery(
|
|
`SELECT ${workflowIdColumn}, ${nameColumn}, ${countSelect} as ${countColumn}, ${workflowNameColumn} FROM ${tableName} WHERE ${workflowIdColumn} = :workflowId`,
|
|
{ workflowId },
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Helper function to get workflow statistics by name (for finding orphaned rows)
|
|
*/
|
|
async function getWorkflowStatisticsByName(
|
|
context: TestMigrationContext,
|
|
name: string,
|
|
): Promise<
|
|
Array<{ workflowId: string | null; name: string; count: number; workflowName: string | null }>
|
|
> {
|
|
const tableName = context.escape.tableName('workflow_statistics');
|
|
const workflowIdColumn = context.escape.columnName('workflowId');
|
|
const nameColumn = context.escape.columnName('name');
|
|
const countColumn = context.escape.columnName('count');
|
|
const workflowNameColumn = context.escape.columnName('workflowName');
|
|
|
|
// Cast count to integer for consistent return type
|
|
const countSelect = context.isPostgres ? `${countColumn}::int` : countColumn;
|
|
|
|
return await context.runQuery(
|
|
`SELECT ${workflowIdColumn}, ${nameColumn}, ${countSelect} as ${countColumn}, ${workflowNameColumn} FROM ${tableName} WHERE ${nameColumn} = :name`,
|
|
{ name },
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Helper function to delete a workflow
|
|
*/
|
|
async function deleteWorkflow(context: TestMigrationContext, workflowId: string): Promise<void> {
|
|
const tableName = context.escape.tableName('workflow_entity');
|
|
const idColumn = context.escape.columnName('id');
|
|
|
|
const placeholder = getParamPlaceholder(context);
|
|
await context.queryRunner.query(`DELETE FROM ${tableName} WHERE ${idColumn} = ${placeholder}`, [
|
|
workflowId,
|
|
]);
|
|
}
|
|
|
|
it('should preserve statistics after workflow deletion (no FK constraint) and CASCADE after rollback', async () => {
|
|
// Debug: Check schema BEFORE migration
|
|
const contextBefore = createTestMigrationContext(dataSource);
|
|
await contextBefore.queryRunner.release();
|
|
|
|
// Run the actual migration
|
|
await runSingleMigration(MIGRATION_NAME);
|
|
|
|
// Create context AFTER migration runs (since runSingleMigration reinitializes the connection)
|
|
dataSource = Container.get(DataSource);
|
|
let context = createTestMigrationContext(dataSource);
|
|
|
|
// Test that statistics are preserved when workflow is deleted (no FK constraint behavior)
|
|
const testWorkflowId = nanoid();
|
|
const uniqueStatName = `test_stat_${nanoid()}`;
|
|
const testWorkflowName = 'My Test Workflow';
|
|
|
|
await insertTestWorkflow(context, testWorkflowId, testWorkflowName);
|
|
await insertWorkflowStatisticsWithName(
|
|
context,
|
|
testWorkflowId,
|
|
uniqueStatName,
|
|
3,
|
|
testWorkflowName,
|
|
);
|
|
|
|
// Verify statistics exist
|
|
const beforeDelete = await getWorkflowStatistics(context, testWorkflowId);
|
|
expect(beforeDelete).toHaveLength(1);
|
|
expect(beforeDelete[0].workflowId).toBe(testWorkflowId);
|
|
|
|
// Delete workflow - statistics should remain unchanged (no FK constraint)
|
|
await deleteWorkflow(context, testWorkflowId);
|
|
|
|
// Verify statistics still exist with the same workflowId (orphaned reference)
|
|
// workflowName should be preserved for identifying the deleted workflow
|
|
const afterDelete = await getWorkflowStatisticsByName(context, uniqueStatName);
|
|
expect(afterDelete).toHaveLength(1);
|
|
expect(afterDelete[0].workflowId).toBe(testWorkflowId); // workflowId is unchanged
|
|
expect(afterDelete[0].count).toBe(3);
|
|
expect(afterDelete[0].workflowName).toBe(testWorkflowName);
|
|
|
|
// Cleanup statistics for this test
|
|
const statsTable = context.escape.tableName('workflow_statistics');
|
|
const nameCol = context.escape.columnName('name');
|
|
const placeholder = getParamPlaceholder(context);
|
|
await context.queryRunner.query(`DELETE FROM ${statsTable} WHERE ${nameCol} = ${placeholder}`, [
|
|
uniqueStatName,
|
|
]);
|
|
await context.queryRunner.release();
|
|
|
|
// Rollback the migration
|
|
await undoLastSingleMigration();
|
|
|
|
// Create new context after rollback
|
|
dataSource = Container.get(DataSource);
|
|
context = createTestMigrationContext(dataSource);
|
|
|
|
// Test CASCADE behavior after rollback
|
|
const cascadeWorkflowId = nanoid();
|
|
|
|
await insertTestWorkflow(context, cascadeWorkflowId);
|
|
await insertWorkflowStatistics(context, cascadeWorkflowId, 'manual_success', 2);
|
|
|
|
// Verify statistics exist
|
|
const beforeCascadeDelete = await getWorkflowStatistics(context, cascadeWorkflowId);
|
|
expect(beforeCascadeDelete).toHaveLength(1);
|
|
|
|
// Delete workflow - should CASCADE
|
|
await deleteWorkflow(context, cascadeWorkflowId);
|
|
|
|
// Verify statistics were deleted
|
|
const afterCascadeDelete = await getWorkflowStatistics(context, cascadeWorkflowId);
|
|
expect(afterCascadeDelete).toHaveLength(0);
|
|
|
|
await context.queryRunner.release();
|
|
});
|
|
|
|
it('should preserve statistics with different workflowIds after deleting multiple workflows', async () => {
|
|
// Run the actual migration
|
|
await runSingleMigration(MIGRATION_NAME);
|
|
|
|
// Create context AFTER migration runs
|
|
dataSource = Container.get(DataSource);
|
|
const context = createTestMigrationContext(dataSource);
|
|
|
|
// Create multiple workflows with the same statistic name but different workflow names
|
|
const workflowId1 = nanoid();
|
|
const workflowId2 = nanoid();
|
|
const workflowId3 = nanoid();
|
|
const workflowName1 = 'Workflow Alpha';
|
|
const workflowName2 = 'Workflow Beta';
|
|
const workflowName3 = 'Workflow Gamma';
|
|
|
|
await insertTestWorkflow(context, workflowId1, workflowName1);
|
|
await insertTestWorkflow(context, workflowId2, workflowName2);
|
|
await insertTestWorkflow(context, workflowId3, workflowName3);
|
|
|
|
// All workflows have 'manual_success' statistics (inserted after migration with workflowName)
|
|
await insertWorkflowStatisticsWithName(
|
|
context,
|
|
workflowId1,
|
|
'manual_success',
|
|
10,
|
|
workflowName1,
|
|
);
|
|
await insertWorkflowStatisticsWithName(
|
|
context,
|
|
workflowId2,
|
|
'manual_success',
|
|
20,
|
|
workflowName2,
|
|
);
|
|
await insertWorkflowStatisticsWithName(
|
|
context,
|
|
workflowId3,
|
|
'manual_success',
|
|
30,
|
|
workflowName3,
|
|
);
|
|
|
|
// Delete all workflows - statistics should remain unchanged (no FK constraint)
|
|
await deleteWorkflow(context, workflowId1);
|
|
await deleteWorkflow(context, workflowId2);
|
|
await deleteWorkflow(context, workflowId3);
|
|
|
|
// Verify all statistics still exist with their original workflowId values (orphaned references)
|
|
const orphanedStats = await getWorkflowStatisticsByName(context, 'manual_success');
|
|
expect(orphanedStats.length).toBeGreaterThanOrEqual(3);
|
|
|
|
// Find our specific test statistics by workflowId
|
|
const stat1 = orphanedStats.find((s) => s.workflowId === workflowId1);
|
|
const stat2 = orphanedStats.find((s) => s.workflowId === workflowId2);
|
|
const stat3 = orphanedStats.find((s) => s.workflowId === workflowId3);
|
|
|
|
// Verify workflowIds are preserved (not set to NULL)
|
|
expect(stat1).toBeDefined();
|
|
expect(stat1?.workflowId).toBe(workflowId1);
|
|
expect(stat1?.count).toBe(10);
|
|
expect(stat1?.workflowName).toBe(workflowName1);
|
|
|
|
expect(stat2).toBeDefined();
|
|
expect(stat2?.workflowId).toBe(workflowId2);
|
|
expect(stat2?.count).toBe(20);
|
|
expect(stat2?.workflowName).toBe(workflowName2);
|
|
|
|
expect(stat3).toBeDefined();
|
|
expect(stat3?.workflowId).toBe(workflowId3);
|
|
expect(stat3?.count).toBe(30);
|
|
expect(stat3?.workflowName).toBe(workflowName3);
|
|
|
|
// Cleanup - delete our test statistics
|
|
const statsTable = context.escape.tableName('workflow_statistics');
|
|
const workflowIdCol = context.escape.columnName('workflowId');
|
|
const placeholder = getParamPlaceholder(context);
|
|
await context.queryRunner.query(
|
|
`DELETE FROM ${statsTable} WHERE ${workflowIdCol} IN (${placeholder}, ${getParamPlaceholder(context, 2)}, ${getParamPlaceholder(context, 3)})`,
|
|
[workflowId1, workflowId2, workflowId3],
|
|
);
|
|
await context.queryRunner.release();
|
|
|
|
// Rollback for next test
|
|
await undoLastSingleMigration();
|
|
});
|
|
|
|
it('should preserve existing workflow statistics data and populate workflowName during migration', async () => {
|
|
// The database is already in pre-migration state from the previous test's rollback
|
|
let context = createTestMigrationContext(dataSource);
|
|
const workflowId = nanoid();
|
|
const testWorkflowName = 'Statistics Test Workflow';
|
|
|
|
// Create workflow and statistics before migration
|
|
await insertTestWorkflow(context, workflowId, testWorkflowName);
|
|
await insertWorkflowStatistics(context, workflowId, 'manual_success', 100);
|
|
await insertWorkflowStatistics(context, workflowId, 'production_success', 200);
|
|
await insertWorkflowStatistics(context, workflowId, 'production_error', 5);
|
|
|
|
// Verify initial data (workflowName column doesn't exist yet)
|
|
const beforeMigration = await getWorkflowStatistics(context, workflowId);
|
|
expect(beforeMigration).toHaveLength(3);
|
|
await context.queryRunner.release();
|
|
|
|
// Run the actual migration
|
|
await runSingleMigration(MIGRATION_NAME);
|
|
|
|
// Create new context after migration (since runSingleMigration reinitializes the connection)
|
|
dataSource = Container.get(DataSource);
|
|
context = createTestMigrationContext(dataSource);
|
|
|
|
// Verify data is preserved after migration and workflowName is populated
|
|
const afterMigration = await getWorkflowStatisticsWithName(context, workflowId);
|
|
expect(afterMigration).toHaveLength(3);
|
|
|
|
// Verify specific values including workflowName
|
|
const manualSuccess = afterMigration.find((s) => s.name === 'manual_success');
|
|
const productionSuccess = afterMigration.find((s) => s.name === 'production_success');
|
|
const productionError = afterMigration.find((s) => s.name === 'production_error');
|
|
|
|
expect(manualSuccess?.count).toBe(100);
|
|
expect(manualSuccess?.workflowName).toBe(testWorkflowName);
|
|
expect(productionSuccess?.count).toBe(200);
|
|
expect(productionSuccess?.workflowName).toBe(testWorkflowName);
|
|
expect(productionError?.count).toBe(5);
|
|
expect(productionError?.workflowName).toBe(testWorkflowName);
|
|
|
|
// Cleanup - delete statistics first (they have FK), then workflow
|
|
const statsTable = context.escape.tableName('workflow_statistics');
|
|
const workflowIdCol = context.escape.columnName('workflowId');
|
|
const placeholder = getParamPlaceholder(context);
|
|
await context.queryRunner.query(
|
|
`DELETE FROM ${statsTable} WHERE ${workflowIdCol} = ${placeholder}`,
|
|
[workflowId],
|
|
);
|
|
await deleteWorkflow(context, workflowId);
|
|
|
|
await context.queryRunner.release();
|
|
});
|
|
|
|
it('should delete orphaned statistics during rollback before restoring FK constraint', async () => {
|
|
// Run the migration first (beforeEach only runs up to, not including, the target migration)
|
|
await runSingleMigration(MIGRATION_NAME);
|
|
|
|
// Create context AFTER migration runs
|
|
dataSource = Container.get(DataSource);
|
|
let context = createTestMigrationContext(dataSource);
|
|
|
|
// Create workflows with statistics using unique stat names to avoid conflicts
|
|
const orphanedWorkflowId1 = nanoid();
|
|
const orphanedWorkflowId2 = nanoid();
|
|
const keepWorkflowId = nanoid();
|
|
const uniqueStatName1 = `orphan_stat_1_${nanoid()}`;
|
|
const uniqueStatName2 = `orphan_stat_2_${nanoid()}`;
|
|
const uniqueStatName3 = `keep_stat_${nanoid()}`;
|
|
|
|
await insertTestWorkflow(context, orphanedWorkflowId1, 'Workflow to Delete 1');
|
|
await insertTestWorkflow(context, orphanedWorkflowId2, 'Workflow to Delete 2');
|
|
await insertTestWorkflow(context, keepWorkflowId, 'Workflow to Keep');
|
|
|
|
await insertWorkflowStatisticsWithName(
|
|
context,
|
|
orphanedWorkflowId1,
|
|
uniqueStatName1,
|
|
100,
|
|
'Workflow to Delete 1',
|
|
);
|
|
await insertWorkflowStatisticsWithName(
|
|
context,
|
|
orphanedWorkflowId2,
|
|
uniqueStatName2,
|
|
200,
|
|
'Workflow to Delete 2',
|
|
);
|
|
await insertWorkflowStatisticsWithName(
|
|
context,
|
|
keepWorkflowId,
|
|
uniqueStatName3,
|
|
300,
|
|
'Workflow to Keep',
|
|
);
|
|
|
|
// Delete some workflows to create orphaned statistics
|
|
await deleteWorkflow(context, orphanedWorkflowId1);
|
|
await deleteWorkflow(context, orphanedWorkflowId2);
|
|
|
|
// Verify orphaned statistics still exist after workflow deletion (no FK constraint)
|
|
const beforeRollback1 = await getWorkflowStatisticsByName(context, uniqueStatName1);
|
|
expect(beforeRollback1).toHaveLength(1);
|
|
expect(beforeRollback1[0].workflowId).toBe(orphanedWorkflowId1);
|
|
|
|
const beforeRollback2 = await getWorkflowStatisticsByName(context, uniqueStatName2);
|
|
expect(beforeRollback2).toHaveLength(1);
|
|
expect(beforeRollback2[0].workflowId).toBe(orphanedWorkflowId2);
|
|
|
|
// Verify non-orphaned statistics still exist
|
|
const beforeRollbackKeep = await getWorkflowStatistics(context, keepWorkflowId);
|
|
expect(beforeRollbackKeep).toHaveLength(1);
|
|
|
|
await context.queryRunner.release();
|
|
|
|
// Rollback the migration - orphaned statistics should be deleted
|
|
await undoLastSingleMigration();
|
|
|
|
// Create new context after rollback
|
|
dataSource = Container.get(DataSource);
|
|
context = createTestMigrationContext(dataSource);
|
|
|
|
// Verify orphaned statistics were deleted during rollback (workflowName column no longer exists)
|
|
const statsTable = context.escape.tableName('workflow_statistics');
|
|
const nameCol = context.escape.columnName('name');
|
|
const workflowIdCol = context.escape.columnName('workflowId');
|
|
const placeholder = getParamPlaceholder(context);
|
|
|
|
const afterRollback1 = await context.queryRunner.query(
|
|
`SELECT ${workflowIdCol} as "workflowId" FROM ${statsTable} WHERE ${nameCol} = ${placeholder}`,
|
|
[uniqueStatName1],
|
|
);
|
|
expect(afterRollback1).toHaveLength(0);
|
|
|
|
const afterRollback2 = await context.queryRunner.query(
|
|
`SELECT ${workflowIdCol} as "workflowId" FROM ${statsTable} WHERE ${nameCol} = ${placeholder}`,
|
|
[uniqueStatName2],
|
|
);
|
|
expect(afterRollback2).toHaveLength(0);
|
|
|
|
// Verify non-orphaned statistics still exist after rollback
|
|
const afterRollbackKeep = await getWorkflowStatistics(context, keepWorkflowId);
|
|
expect(afterRollbackKeep).toHaveLength(1);
|
|
expect(afterRollbackKeep[0].workflowId).toBe(keepWorkflowId);
|
|
expect(afterRollbackKeep[0].count).toBe(300);
|
|
|
|
// Cleanup - reuse the existing variable declarations
|
|
await context.queryRunner.query(
|
|
`DELETE FROM ${statsTable} WHERE ${workflowIdCol} = ${placeholder}`,
|
|
[keepWorkflowId],
|
|
);
|
|
await deleteWorkflow(context, keepWorkflowId);
|
|
|
|
await context.queryRunner.release();
|
|
});
|
|
|
|
it('should reset overflowing values to 0 during rollback before converting BIGINT to INTEGER (PostgreSQL only)', async () => {
|
|
// Run the migration to enable BIGINT columns
|
|
await runSingleMigration(MIGRATION_NAME);
|
|
|
|
// Create context AFTER migration runs
|
|
dataSource = Container.get(DataSource);
|
|
let context = createTestMigrationContext(dataSource);
|
|
|
|
// Skip this test for SQLite - SQLite handles INTEGER overflow differently
|
|
if (!context.isPostgres) {
|
|
await context.queryRunner.release();
|
|
return;
|
|
}
|
|
|
|
const workflowId = nanoid();
|
|
const testWorkflowName = 'Overflow Test Workflow';
|
|
const uniqueStatName = `overflow_stat_${nanoid()}`;
|
|
|
|
await insertTestWorkflow(context, workflowId, testWorkflowName);
|
|
|
|
// Insert statistics with values exceeding INTEGER max (2147483647)
|
|
const tableName = context.escape.tableName('workflow_statistics');
|
|
const workflowIdColumn = context.escape.columnName('workflowId');
|
|
const nameColumn = context.escape.columnName('name');
|
|
const countColumn = context.escape.columnName('count');
|
|
const rootCountColumn = context.escape.columnName('rootCount');
|
|
const latestEventColumn = context.escape.columnName('latestEvent');
|
|
const workflowNameColumn = context.escape.columnName('workflowName');
|
|
|
|
const overflowValue = '3000000000'; // Exceeds INTEGER max
|
|
const placeholders = getParamPlaceholders(context, 6);
|
|
await context.queryRunner.query(
|
|
`INSERT INTO ${tableName} (${workflowIdColumn}, ${nameColumn}, ${countColumn}, ${rootCountColumn}, ${latestEventColumn}, ${workflowNameColumn}) VALUES (${placeholders})`,
|
|
[workflowId, uniqueStatName, overflowValue, overflowValue, new Date(), testWorkflowName],
|
|
);
|
|
|
|
// Verify the large values were inserted
|
|
const placeholder = getParamPlaceholder(context);
|
|
const beforeRollback = await context.queryRunner.query(
|
|
`SELECT ${countColumn} as "count", ${rootCountColumn} as "rootCount" FROM ${tableName} WHERE ${nameColumn} = ${placeholder}`,
|
|
[uniqueStatName],
|
|
);
|
|
expect(beforeRollback).toHaveLength(1);
|
|
expect(Number(beforeRollback[0].count)).toBeGreaterThan(2147483647);
|
|
expect(Number(beforeRollback[0].rootCount)).toBeGreaterThan(2147483647);
|
|
|
|
await context.queryRunner.release();
|
|
|
|
// Rollback the migration - should reset overflowing values to 0 on PostgreSQL
|
|
await undoLastSingleMigration();
|
|
|
|
// Create new context after rollback
|
|
dataSource = Container.get(DataSource);
|
|
context = createTestMigrationContext(dataSource);
|
|
|
|
// Verify values were reset to 0 on PostgreSQL (preventing overflow errors)
|
|
const afterRollback = await context.queryRunner.query(
|
|
`SELECT ${countColumn} as "count", ${rootCountColumn} as "rootCount" FROM ${tableName} WHERE ${nameColumn} = ${placeholder}`,
|
|
[uniqueStatName],
|
|
);
|
|
expect(afterRollback).toHaveLength(1);
|
|
expect(afterRollback[0].count).toBe(0);
|
|
expect(afterRollback[0].rootCount).toBe(0);
|
|
|
|
// Cleanup
|
|
await context.queryRunner.query(
|
|
`DELETE FROM ${tableName} WHERE ${nameColumn} = ${placeholder}`,
|
|
[uniqueStatName],
|
|
);
|
|
await deleteWorkflow(context, workflowId);
|
|
|
|
await context.queryRunner.release();
|
|
});
|
|
});
|