Files
sap-erp/scripts/migrate-app-data.js
T
John 69b4e68baf
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s
first commit
2026-09-23 17:31:02 +05:30

152 lines
6.5 KiB
JavaScript

// One-time migration: copy this app's own data out of the SAP database
// (Test_MI-NewDB2) into the new dedicated app database (SAP-ERP), now that
// all stores read/write the new DB going forward. Safe to re-run — every
// insert is guarded by "does this row already exist in NEW?" before writing.
'use strict';
require('dotenv').config();
const sql = require('mssql');
const oldCfg = {
server: process.env.SQL_HOST, port: parseInt(process.env.SQL_PORT),
user: process.env.SQL_USER, password: process.env.SQL_PASSWORD,
database: process.env.SQL_DATABASE,
options: { encrypt: true, trustServerCertificate: true },
};
const newCfg = {
server: process.env.APP_SQL_HOST, port: parseInt(process.env.APP_SQL_PORT),
user: process.env.APP_SQL_USER, password: process.env.APP_SQL_PASSWORD,
database: process.env.APP_SQL_DATABASE,
options: { encrypt: true, trustServerCertificate: true },
};
async function getColumns(pool, table) {
const r = await pool.request().input('t', table).query(`
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = @t ORDER BY ORDINAL_POSITION
`);
return r.recordset.map(row => row.COLUMN_NAME);
}
async function getIdentityColumn(pool, table) {
const r = await pool.request().input('t', table).query(`
SELECT c.name FROM sys.identity_columns c
JOIN sys.tables t ON t.object_id = c.object_id
WHERE t.name = @t
`);
return r.recordset[0]?.name || null;
}
async function getPkColumns(pool, table) {
const r = await pool.request().input('t', table).query(`
SELECT c.name FROM sys.indexes i
JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
JOIN sys.tables t ON t.object_id = i.object_id
WHERE t.name = @t AND i.is_primary_key = 1
ORDER BY ic.key_ordinal
`);
return r.recordset.map(row => row.name);
}
// Generic copy: every row from oldTable -> newTable, skipping rows whose PK
// already exists in the new table. Preserves IDENTITY values when present.
// Only copies columns that exist in BOTH schemas — a handful of app tables
// have old, no-longer-referenced columns from past ad-hoc ALTERs that never
// made it into the current bootstrap()'s CREATE/ALTER logic; skipping them
// is correct since current code doesn't read/write them either way.
async function copyTable(oldPool, newPool, table) {
const oldCols = await getColumns(oldPool, table);
const newColsAll = await getColumns(newPool, table);
const newColSet = new Set(newColsAll);
const dropped = oldCols.filter(c => !newColSet.has(c));
if (dropped.length) console.log(` ${table}: skipping columns not in new schema: ${dropped.join(', ')}`);
const cols = oldCols.filter(c => newColSet.has(c));
const identityCol = await getIdentityColumn(newPool, table);
const pkCols = await getPkColumns(newPool, table);
const oldRows = (await oldPool.request().query(`SELECT * FROM [${table}]`)).recordset;
if (!oldRows.length) { console.log(` ${table}: nothing to copy (old table empty)`); return; }
let existingKeys = new Set();
if (pkCols.length) {
const newRows = (await newPool.request().query(`SELECT ${pkCols.map(c => `[${c}]`).join(',')} FROM [${table}]`)).recordset;
existingKeys = new Set(newRows.map(r => pkCols.map(c => String(r[c])).join('||')));
}
const rowsToInsert = oldRows.filter(row => {
if (!pkCols.length) return true;
return !existingKeys.has(pkCols.map(c => String(row[c])).join('||'));
});
const skipped = oldRows.length - rowsToInsert.length;
// SET IDENTITY_INSERT is a per-connection session setting, and separate
// pooled .request() calls can land on different physical connections
// (same class of bug as the SCOPE_IDENTITY() race elsewhere in this
// codebase) — so ON, every INSERT, and OFF must all run as ONE batch in
// ONE .query() call. SQL Server caps params per request at 2100, so rows
// are chunked to stay well under that.
let inserted = 0;
if (rowsToInsert.length) {
const maxParamsPerBatch = 1800;
const batchSize = Math.max(1, Math.floor(maxParamsPerBatch / (cols.length || 1)));
for (let start = 0; start < rowsToInsert.length; start += batchSize) {
const chunk = rowsToInsert.slice(start, start + batchSize);
const req = newPool.request();
const stmts = [];
if (identityCol) stmts.push(`SET IDENTITY_INSERT [${table}] ON;`);
chunk.forEach((row, ri) => {
const placeholders = cols.map((c, ci) => {
const p = `p${ri}_${ci}`;
req.input(p, row[c]);
return `@${p}`;
});
stmts.push(`INSERT INTO [${table}] (${cols.map(c => `[${c}]`).join(',')}) VALUES (${placeholders.join(',')});`);
});
if (identityCol) stmts.push(`SET IDENTITY_INSERT [${table}] OFF;`);
await req.query(stmts.join('\n'));
inserted += chunk.length;
}
}
console.log(` ${table}: inserted ${inserted}, skipped ${skipped} (already present)`);
}
async function migrateAdminCollision(oldPool, newPool) {
const cols = await getColumns(oldPool, 'ZCUST_USERS');
const oldAdmin = (await oldPool.request().query(`SELECT * FROM [ZCUST_USERS] WHERE USERNAME='admin'`)).recordset[0];
if (!oldAdmin) return;
const setCols = cols.filter(c => c !== 'ID');
const req = newPool.request();
setCols.forEach((c, i) => req.input(`p${i}`, oldAdmin[c]));
req.input('id', oldAdmin.ID);
await req.query(`UPDATE [ZCUST_USERS] SET ${setCols.map((c, i) => `[${c}]=@p${i}`).join(',')} WHERE ID=@id`);
console.log(` ZCUST_USERS: 'admin' row overwritten with OLD database's values (per user's choice)`);
}
async function main() {
const oldPool = await new sql.ConnectionPool(oldCfg).connect();
const newPool = await new sql.ConnectionPool(newCfg).connect();
console.log('Connected to both databases.\n');
const simpleTables = [
'ZWORK_ORDERS', 'ZBATCH_INTIMATIONS', 'ZREQUIREMENTS', 'ZITEM_PORTAL',
'pl_account_map', 'pl_overrides', 'pl_snapshots', 'cf_items',
'salary_uploads', 'salary_cost_config', 'salary_bbcapd_ratio', 'salary_dept_mapping',
'project_requests',
];
console.log('Copying tables...');
for (const t of simpleTables) {
try { await copyTable(oldPool, newPool, t); }
catch (e) { console.log(` ${t}: ERROR - ${e.message}`); }
}
console.log('\nZCUST_USERS (admin collision handled specially)...');
await migrateAdminCollision(oldPool, newPool);
await copyTable(oldPool, newPool, 'ZCUST_USERS'); // remaining non-colliding users
console.log('\nDone.');
process.exit(0);
}
main().catch(e => { console.error('FATAL', e); process.exit(1); });