Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
416 lines
14 KiB
JavaScript
416 lines
14 KiB
JavaScript
/**
|
|
* Distribute the boards table (renamed from channels) in Citus
|
|
* This migration should run after 20250930000001_rename_channels_to_boards.cjs
|
|
* Dependencies: tenants, categories, tickets, tags, tag_definitions must be distributed first
|
|
*/
|
|
|
|
exports.config = { transaction: false };
|
|
|
|
exports.up = async function(knex) {
|
|
// Check if we're in recovery mode (read replica/standby)
|
|
const inRecovery = await knex.raw(`SELECT pg_is_in_recovery() as in_recovery`);
|
|
|
|
if (inRecovery.rows[0].in_recovery) {
|
|
console.log('Database is in recovery mode (read replica). Skipping Citus distribution.');
|
|
console.log('This migration must run on the primary/coordinator node.');
|
|
return;
|
|
}
|
|
|
|
// Check if Citus is enabled
|
|
const citusEnabled = await knex.raw(`
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_extension WHERE extname = 'citus'
|
|
) as enabled
|
|
`);
|
|
|
|
if (!citusEnabled.rows[0].enabled) {
|
|
console.log('Citus not enabled, skipping boards table distribution');
|
|
return;
|
|
}
|
|
|
|
console.log('Distributing boards table (renamed from channels)...');
|
|
|
|
// Check if boards table exists
|
|
const boardsExists = await knex.schema.hasTable('boards');
|
|
if (!boardsExists) {
|
|
console.log('boards table does not exist yet - base migration may not have run');
|
|
return;
|
|
}
|
|
|
|
// Check if channels table is still distributed (shouldn't be if migration ran)
|
|
const channelsDistributed = await knex.raw(`
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_dist_partition
|
|
WHERE logicalrelid = 'channels'::regclass
|
|
) as distributed
|
|
`);
|
|
|
|
if (channelsDistributed.rows[0].distributed) {
|
|
console.log('Undistributing old channels table...');
|
|
try {
|
|
// First, drop all foreign keys referencing channels
|
|
const foreignKeys = await knex.raw(`
|
|
SELECT DISTINCT
|
|
tc.table_name,
|
|
tc.constraint_name
|
|
FROM information_schema.table_constraints tc
|
|
JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_name)
|
|
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
AND ccu.table_name = 'channels'
|
|
`);
|
|
|
|
for (const fk of foreignKeys.rows) {
|
|
console.log(` Dropping ${fk.constraint_name} from ${fk.table_name}...`);
|
|
await knex.raw(`ALTER TABLE ${fk.table_name} DROP CONSTRAINT IF EXISTS ${fk.constraint_name}`);
|
|
}
|
|
|
|
// Check for any remaining FKs from channels to other tables (like channels -> tenants)
|
|
const channelsFKs = await knex.raw(`
|
|
SELECT
|
|
conname as constraint_name,
|
|
confrelid::regclass as referenced_table
|
|
FROM pg_constraint
|
|
WHERE conrelid = 'channels'::regclass
|
|
AND contype = 'f'
|
|
`);
|
|
|
|
for (const fk of channelsFKs.rows) {
|
|
console.log(` Dropping ${fk.constraint_name} from channels (references ${fk.referenced_table})...`);
|
|
await knex.raw(`ALTER TABLE channels DROP CONSTRAINT IF EXISTS ${fk.constraint_name}`);
|
|
}
|
|
|
|
// Use cascade option to handle any remaining FK dependencies
|
|
await knex.raw(`SELECT undistribute_table('channels', cascade_via_foreign_keys=>true)`);
|
|
console.log(' ✓ Undistributed channels table');
|
|
} catch (error) {
|
|
console.log(` - Could not undistribute channels: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Check if boards table is already distributed
|
|
const boardsDistributed = await knex.raw(`
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_dist_partition
|
|
WHERE logicalrelid = 'boards'::regclass
|
|
) as distributed
|
|
`);
|
|
|
|
if (boardsDistributed.rows[0].distributed) {
|
|
console.log(' boards table already distributed');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log(' Capturing foreign key constraints for boards...');
|
|
|
|
// Manually capture FKs instead of using utility
|
|
const capturedFKs = await knex.raw(`
|
|
SELECT
|
|
conname as constraint_name,
|
|
pg_get_constraintdef(c.oid) as definition
|
|
FROM pg_constraint c
|
|
JOIN pg_namespace n ON n.oid = c.connamespace
|
|
WHERE c.conrelid = 'boards'::regclass
|
|
AND c.contype = 'f'
|
|
`);
|
|
|
|
console.log(' Dropping foreign key constraints for boards...');
|
|
for (const fk of capturedFKs.rows) {
|
|
try {
|
|
await knex.raw(`ALTER TABLE boards DROP CONSTRAINT IF EXISTS ${fk.constraint_name}`);
|
|
console.log(` ✓ Dropped FK: ${fk.constraint_name}`);
|
|
} catch (e) {
|
|
console.log(` - Could not drop ${fk.constraint_name}: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
// Drop unique constraints with CASCADE
|
|
console.log(' Dropping unique constraints for boards...');
|
|
const uniqueConstraints = await knex.raw(`
|
|
SELECT conname
|
|
FROM pg_constraint
|
|
WHERE conrelid = 'boards'::regclass
|
|
AND contype = 'u'
|
|
`);
|
|
|
|
for (const constraint of uniqueConstraints.rows) {
|
|
try {
|
|
await knex.raw(`ALTER TABLE boards DROP CONSTRAINT ${constraint.conname} CASCADE`);
|
|
console.log(` ✓ Dropped constraint: ${constraint.conname} with CASCADE`);
|
|
} catch (e) {
|
|
console.log(` - Could not drop ${constraint.conname}: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
// Drop check constraints (except not null)
|
|
console.log(' Dropping check constraints for boards...');
|
|
const checkConstraints = await knex.raw(`
|
|
SELECT conname
|
|
FROM pg_constraint
|
|
WHERE conrelid = 'boards'::regclass
|
|
AND contype = 'c'
|
|
AND conname NOT LIKE '%_not_null'
|
|
`);
|
|
|
|
for (const constraint of checkConstraints.rows) {
|
|
try {
|
|
await knex.raw(`ALTER TABLE boards DROP CONSTRAINT ${constraint.conname} CASCADE`);
|
|
console.log(` ✓ Dropped check constraint: ${constraint.conname}`);
|
|
} catch (e) {
|
|
console.log(` - Could not drop check ${constraint.conname}: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
// Drop triggers if any
|
|
console.log(' Dropping triggers for boards...');
|
|
const triggers = await knex.raw(`
|
|
SELECT tgname
|
|
FROM pg_trigger
|
|
WHERE tgrelid = 'boards'::regclass
|
|
AND tgisinternal = false
|
|
`);
|
|
|
|
for (const trigger of triggers.rows) {
|
|
try {
|
|
await knex.raw(`DROP TRIGGER IF EXISTS ${trigger.tgname} ON boards`);
|
|
console.log(` ✓ Dropped trigger: ${trigger.tgname}`);
|
|
} catch (e) {
|
|
console.log(` - Could not drop trigger ${trigger.tgname}: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
// Distribute the boards table
|
|
console.log(' Distributing boards table...');
|
|
try {
|
|
await knex.raw(`SELECT create_distributed_table('boards', 'tenant', colocate_with => 'tenants')`);
|
|
} catch (e) {
|
|
// If colocation fails, try without it
|
|
console.log(` Colocation not available, distributing without it...`);
|
|
await knex.raw(`SELECT create_distributed_table('boards', 'tenant')`);
|
|
}
|
|
console.log(' ✓ Distributed boards table');
|
|
|
|
// Recreate check constraints
|
|
console.log(' Recreating check constraints for boards...');
|
|
await knex.raw(`
|
|
ALTER TABLE boards
|
|
ADD CONSTRAINT boards_category_type_check
|
|
CHECK (category_type IN ('custom', 'itil'))
|
|
`);
|
|
|
|
await knex.raw(`
|
|
ALTER TABLE boards
|
|
ADD CONSTRAINT boards_priority_type_check
|
|
CHECK (priority_type IN ('custom', 'itil'))
|
|
`);
|
|
console.log(' ✓ Recreated check constraints');
|
|
|
|
// Recreate indexes
|
|
console.log(' Recreating indexes for boards...');
|
|
await knex.raw(`
|
|
CREATE INDEX IF NOT EXISTS idx_boards_tenant_category_type
|
|
ON boards(tenant, category_type)
|
|
`);
|
|
|
|
await knex.raw(`
|
|
CREATE INDEX IF NOT EXISTS idx_boards_tenant_priority_type
|
|
ON boards(tenant, priority_type)
|
|
`);
|
|
console.log(' ✓ Recreated indexes');
|
|
|
|
// Recreate foreign keys
|
|
console.log(' Recreating foreign keys for boards...');
|
|
for (const fk of capturedFKs.rows) {
|
|
try {
|
|
await knex.raw(`ALTER TABLE boards ADD CONSTRAINT ${fk.constraint_name} ${fk.definition}`);
|
|
console.log(` ✓ Recreated FK: ${fk.constraint_name}`);
|
|
} catch (e) {
|
|
console.log(` - Could not recreate ${fk.constraint_name}: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
console.log('\n✓ boards table distributed successfully');
|
|
|
|
} catch (error) {
|
|
console.error(` ✗ Failed to distribute boards table: ${error.message}`);
|
|
throw error;
|
|
}
|
|
|
|
// Distribute standard_boards as a reference table
|
|
console.log('\nDistributing standard_boards table...');
|
|
|
|
const standardBoardsExists = await knex.schema.hasTable('standard_boards');
|
|
if (standardBoardsExists) {
|
|
const standardBoardsDistributed = await knex.raw(`
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_dist_partition
|
|
WHERE logicalrelid = 'standard_boards'::regclass
|
|
) as distributed
|
|
`);
|
|
|
|
if (!standardBoardsDistributed.rows[0].distributed) {
|
|
try {
|
|
// Drop check constraints before distribution
|
|
console.log(' Dropping check constraints for standard_boards...');
|
|
const checkConstraints = await knex.raw(`
|
|
SELECT conname
|
|
FROM pg_constraint
|
|
WHERE conrelid = 'standard_boards'::regclass
|
|
AND contype = 'c'
|
|
AND conname NOT LIKE '%_not_null'
|
|
`);
|
|
|
|
for (const constraint of checkConstraints.rows) {
|
|
try {
|
|
await knex.raw(`ALTER TABLE standard_boards DROP CONSTRAINT ${constraint.conname} CASCADE`);
|
|
console.log(` ✓ Dropped check constraint: ${constraint.conname}`);
|
|
} catch (e) {
|
|
console.log(` - Could not drop check ${constraint.conname}: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
// Create reference table
|
|
await knex.raw(`SELECT create_reference_table('standard_boards')`);
|
|
console.log(' ✓ Created standard_boards as reference table');
|
|
|
|
// Recreate check constraints
|
|
console.log(' Recreating check constraints for standard_boards...');
|
|
await knex.raw(`
|
|
ALTER TABLE standard_boards
|
|
ADD CONSTRAINT standard_boards_category_type_check
|
|
CHECK (category_type IN ('custom', 'itil'))
|
|
`);
|
|
|
|
await knex.raw(`
|
|
ALTER TABLE standard_boards
|
|
ADD CONSTRAINT standard_boards_priority_type_check
|
|
CHECK (priority_type IN ('custom', 'itil'))
|
|
`);
|
|
console.log(' ✓ Recreated check constraints');
|
|
|
|
} catch (error) {
|
|
console.error(` ✗ Failed to distribute standard_boards: ${error.message}`);
|
|
// Don't throw - this is not critical
|
|
}
|
|
} else {
|
|
console.log(' standard_boards already distributed');
|
|
}
|
|
}
|
|
|
|
// Now update foreign keys in related tables to reference boards instead of channels
|
|
console.log('\nUpdating foreign keys in related tables to reference boards...');
|
|
|
|
const relatedTables = ['categories', 'tickets', 'tag_definitions'];
|
|
|
|
for (const table of relatedTables) {
|
|
try {
|
|
const tableExists = await knex.schema.hasTable(table);
|
|
if (!tableExists) {
|
|
console.log(` ${table} table does not exist, skipping`);
|
|
continue;
|
|
}
|
|
|
|
const hasBoardId = await knex.schema.hasColumn(table, 'board_id');
|
|
if (!hasBoardId) {
|
|
console.log(` ${table} does not have board_id column, skipping`);
|
|
continue;
|
|
}
|
|
|
|
const isDistributed = await knex.raw(`
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_dist_partition
|
|
WHERE logicalrelid = '${table}'::regclass
|
|
) as distributed
|
|
`);
|
|
|
|
if (!isDistributed.rows[0].distributed) {
|
|
console.log(` ${table} is not distributed, skipping FK update`);
|
|
continue;
|
|
}
|
|
|
|
console.log(` Updating ${table} foreign keys...`);
|
|
|
|
// Check if foreign key to boards already exists
|
|
const existingFK = await knex.raw(`
|
|
SELECT conname
|
|
FROM pg_constraint
|
|
WHERE conrelid = '${table}'::regclass
|
|
AND confrelid = 'boards'::regclass
|
|
AND contype = 'f'
|
|
`);
|
|
|
|
if (existingFK.rows.length === 0) {
|
|
// Create foreign key to boards
|
|
try {
|
|
await knex.raw(`
|
|
ALTER TABLE ${table}
|
|
ADD CONSTRAINT ${table}_board_fkey
|
|
FOREIGN KEY (tenant, board_id)
|
|
REFERENCES boards(tenant, board_id)
|
|
`);
|
|
console.log(` ✓ Added foreign key from ${table} to boards`);
|
|
} catch (e) {
|
|
console.log(` - Could not add FK from ${table} to boards: ${e.message}`);
|
|
}
|
|
} else {
|
|
console.log(` - Foreign key from ${table} to boards already exists`);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.log(` - Error updating ${table}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
console.log('\n✓ Boards table distribution and FK updates completed');
|
|
};
|
|
|
|
exports.down = async function(knex) {
|
|
const citusEnabled = await knex.raw(`
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_extension WHERE extname = 'citus'
|
|
) as enabled
|
|
`);
|
|
|
|
if (!citusEnabled.rows[0].enabled) {
|
|
return;
|
|
}
|
|
|
|
console.log('Undistributing boards table...');
|
|
|
|
try {
|
|
const isDistributed = await knex.raw(`
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_dist_partition
|
|
WHERE logicalrelid = 'boards'::regclass
|
|
) as distributed
|
|
`);
|
|
|
|
if (isDistributed.rows[0].distributed) {
|
|
await knex.raw(`SELECT undistribute_table('boards')`);
|
|
console.log(' ✓ Undistributed boards table');
|
|
}
|
|
} catch (error) {
|
|
console.error(` ✗ Failed to undistribute boards: ${error.message}`);
|
|
}
|
|
|
|
// If channels table exists, re-distribute it
|
|
const channelsExists = await knex.schema.hasTable('channels');
|
|
if (channelsExists) {
|
|
console.log('Re-distributing channels table...');
|
|
try {
|
|
const channelsDistributed = await knex.raw(`
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM pg_dist_partition
|
|
WHERE logicalrelid = 'channels'::regclass
|
|
) as distributed
|
|
`);
|
|
|
|
if (!channelsDistributed.rows[0].distributed) {
|
|
await knex.raw(`SELECT create_distributed_table('channels', 'tenant', colocate_with => 'tenants')`);
|
|
console.log(' ✓ Re-distributed channels table');
|
|
}
|
|
} catch (error) {
|
|
console.error(` ✗ Failed to re-distribute channels: ${error.message}`);
|
|
}
|
|
}
|
|
}; |