Skip to content
Merged
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
63 changes: 63 additions & 0 deletions __tests__/database/safe-migration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const rule = require('../../src/rules/database/safe-migration');

describe('Safe Migration Rule', () => {
it('labels for additive migrations without risky ops', () => {
const files = [{ filename: 'migrations/001_add_users.sql', patch: '+ ALTER TABLE users ADD COLUMN age INT;' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('safe-migration');
});

it('labels for CREATE INDEX operations', () => {
const files = [{ filename: 'db/migrations/002_add_index.sql', patch: '+ CREATE INDEX idx_email ON users(email);' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('safe-migration');
});

it('labels for CREATE TABLE operations', () => {
const files = [{ filename: 'database/migrations/003_create.sql', patch: '+ CREATE TABLE posts (id INT);' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toContain('safe-migration');
});

it('does not label when risky ops are present', () => {
const files = [{ filename: 'migrations/002_drop_table.sql', patch: '+ DROP TABLE users;' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('does not label when ALTER DROP is present', () => {
const files = [{ filename: 'migrations/003_alter.sql', patch: '+ ALTER TABLE users DROP COLUMN age;' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('does not label when no migrations found', () => {
const files = [{ filename: 'src/app.ts', patch: '+ const x = 1;' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('does not label when migration has no patch', () => {
const files = [{ filename: 'migrations/004_test.sql' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('does not label when only risky ops without additive', () => {
const files = [{ filename: 'migrations/005_truncate.sql', patch: '+ TRUNCATE TABLE users;' }];
const labels = rule({ files, pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('handles empty files array', () => {
const labels = rule({ files: [], pr: {}, enableDebug: false });
expect(labels).toEqual([]);
});

it('handles debug mode', () => {
const files = [{ filename: 'migrations/006_add.sql', patch: '+ ADD COLUMN name VARCHAR(255);' }];
expect(() => rule({ files, pr: {}, enableDebug: true })).not.toThrow();
});
});


52 changes: 52 additions & 0 deletions src/rules/database/safe-migration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Safe Migration Detection Rule
*
* Adds `safe-migration` when migration files are present and no risky/schema
* keywords are detected (i.e., only additive changes like ADD COLUMN/INDEX).
*/

module.exports = function safeMigrationRule({ files, pr, enableDebug }) {
const labels = [];

const isMigration = (name) => /(^|\/)(migrations|db\/migrations|database\/migrations)\//i.test(name) || /\.sql$/i.test(name);
const risky = /(DROP\s+TABLE|TRUNCATE\s+TABLE|ALTER\s+TABLE\s+[^;]*\s+DROP\b|RENAME\s+COLUMN|MODIFY\s+COLUMN)/i;
const additive = /(CREATE\s+INDEX|ADD\s+COLUMN|CREATE\s+TABLE|ADD\s+CONSTRAINT)/i;

let hasMigration = false;
let hasRisky = false;
let hasAdditive = false;

for (const file of files || []) {
const name = (file && file.filename ? String(file.filename) : '').toLowerCase();
if (!name || !isMigration(name)) continue;
hasMigration = true;
const patch = file && file.patch ? String(file.patch) : '';
if (patch) {
if (risky.test(patch)) hasRisky = true;
if (additive.test(patch)) hasAdditive = true;
}
}

if (hasMigration && !hasRisky && hasAdditive) {
labels.push('safe-migration');
}

if (enableDebug) {
console.log(`[Safe Migration Rule] hasMigration=${hasMigration} risky=${hasRisky} additive=${hasAdditive} → ${labels.join(', ') || 'none'}`);
}

return labels;
};

module.exports.metadata = {
name: 'Safe Migration Detection',
description: 'Detects additive-only DB migrations (safe to apply)',
labels: [
{ name: 'safe-migration', color: '0E8A16', description: 'Additive database migration detected' }
],
author: 'pr-auto-labeler',
version: '1.0.0',
category: 'database'
};


Loading