152 lines
6.5 KiB
JavaScript
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); });
|