myt/myt-push.js

576 lines
18 KiB
JavaScript
Raw Permalink Normal View History

2022-12-21 13:17:50 +00:00
const Myt = require('./myt');
const Command = require('./lib/command');
2020-12-02 07:35:26 +00:00
const fs = require('fs-extra');
2020-12-04 10:53:11 +00:00
const nodegit = require('nodegit');
const ExporterEngine = require('./lib/exporter-engine');
const connExt = require('./lib/conn');
const repoExt = require('./lib/repo');
const SqlString = require('sqlstring');
2020-12-04 09:15:29 +00:00
/**
* Pushes changes to remote.
*/
class Push extends Command {
static usage = {
description: 'Apply changes into database',
params: {
force: 'Answer yes to all questions',
commit: 'Wether to save the commit SHA into database',
sums: 'Save SHA sums of pushed objects',
triggers: 'Wether to exclude triggers, used to generate local DB'
},
operand: 'remote'
};
static opts = {
alias: {
force: 'f',
commit: 'c',
sums: 's',
triggers: 't'
},
boolean: [
'force',
'commit',
'sums',
'triggers'
]
};
2020-12-02 07:35:26 +00:00
2022-12-21 13:17:50 +00:00
async run(myt, opts) {
const conn = await myt.dbConnect();
2020-12-02 07:35:26 +00:00
this.conn = conn;
2022-06-09 09:42:03 +00:00
if (opts.remote == 'local')
2022-04-30 00:33:41 +00:00
opts.commit = true;
// Obtain exclusive lock
const [[row]] = await conn.query(
2022-12-21 13:17:50 +00:00
`SELECT GET_LOCK('myt_push', 30) getLock`);
const pingTimeout = setInterval(async() => {
try {
await conn.ping()
} catch (e) {}
}, 60 * 1000);
if (!row.getLock) {
let isUsed = 0;
if (row.getLock == 0) {
const [[row]] = await conn.query(
2022-12-21 13:17:50 +00:00
`SELECT IS_USED_LOCK('myt_push') isUsed`);
isUsed = row.isUsed;
}
throw new Error(`Cannot obtain exclusive lock, used by connection ${isUsed}`);
}
async function releaseLock() {
2022-12-21 13:17:50 +00:00
await conn.query(`DO RELEASE_LOCK('myt_push')`);
}
try {
2022-12-21 13:17:50 +00:00
await this.push(myt, opts, conn);
} catch(err) {
try {
await releaseLock();
} catch (e) {}
throw err;
} finally {
clearInterval(pingTimeout);
}
await releaseLock();
}
2022-12-21 13:17:50 +00:00
async push(myt, opts, conn) {
const pushConn = await myt.createConnection();
2022-02-02 04:05:31 +00:00
// Get database version
2022-12-21 13:17:50 +00:00
const version = await myt.fetchDbVersion() || {};
2020-12-02 07:35:26 +00:00
console.log(
`Database information:`
+ `\n -> Version: ${version.number}`
+ `\n -> Commit: ${version.gitCommit}`
);
if (!version.number)
version.number = String('0').padStart(opts.versionDigits, '0');
2020-12-05 21:50:45 +00:00
if (!/^[0-9]*$/.test(version.number))
throw new Error('Wrong database version');
2020-12-02 07:35:26 +00:00
// Prevent push to production by mistake
2020-12-04 16:30:26 +00:00
if (opts.remote == 'production') {
2020-12-02 07:35:26 +00:00
console.log(
'\n ( ( ) ( ( ) ) '
+ '\n )\\ ))\\ ) ( /( )\\ ) ( ))\\ ) ( /( ( /( '
+ '\n(()/(()/( )\\()|()/( ( )\\ ) /(()/( )\\()) )\\())'
+ '\n /(_))(_)|(_)\\ /(_)) )\\ (((_) ( )(_))(_)|(_)\\ ((_)\\ '
+ '\n(_))(_)) ((_|_))_ _ ((_))\\___(_(_()|__)) ((_) _((_)'
+ '\n| _ \\ _ \\ / _ \\| \\| | | ((/ __|_ _|_ _| / _ \\| \\| |'
+ '\n| _/ /| (_) | |) | |_| || (__ | | | | | (_) | . |'
+ '\n|_| |_|_\\ \\___/|___/ \\___/ \\___| |_| |___| \\___/|_|\\_|'
+ '\n'
);
if (!opts.force) {
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const answer = await new Promise(resolve => {
rl.question('Are you sure? (Default: no) [yes|no] ', resolve);
});
rl.close();
if (answer !== 'yes')
throw new Error('Changes aborted');
}
}
// Apply versions
2020-12-02 07:35:26 +00:00
console.log('Applying versions.');
let nChanges = 0;
let silent = true;
const versionsDir = opts.versionsDir;
2020-12-02 07:35:26 +00:00
function logVersion(version, name, error) {
console.log('', version.bold, name);
}
function logScript(type, message, error) {
console.log(' ', type.bold, message);
2020-12-02 07:35:26 +00:00
}
function isUndoScript(script) {
return /\.undo\.sql$/.test(script);
}
2020-12-02 07:35:26 +00:00
2023-01-25 17:24:02 +00:00
const skipFiles = new Set([
'README.md',
'.archive'
]);
2020-12-02 07:35:26 +00:00
if (await fs.pathExists(versionsDir)) {
const versionDirs = await fs.readdir(versionsDir);
for (const versionDir of versionDirs) {
2023-01-25 17:24:02 +00:00
if (skipFiles.has(versionDir)) continue;
2020-12-02 07:35:26 +00:00
2022-12-21 13:17:50 +00:00
const dirVersion = myt.parseVersionDir(versionDir);
if (!dirVersion) {
logVersion('[?????]'.yellow, versionDir,
`Wrong directory name.`
);
2020-12-02 07:35:26 +00:00
continue;
}
const versionNumber = dirVersion.number;
const versionName = dirVersion.name;
2020-12-02 07:35:26 +00:00
if (versionNumber.length != version.number.length) {
logVersion('[*****]'.gray, versionDir,
`Bad version length, should have ${version.number.length} characters.`
);
2020-12-02 07:35:26 +00:00
continue;
}
const scriptsDir = `${versionsDir}/${versionDir}`;
const scripts = await fs.readdir(scriptsDir);
const [versionLog] = await conn.query(
`SELECT file FROM versionLog
WHERE code = ?
AND number = ?
AND errorNumber IS NULL`,
[opts.code, versionNumber]
);
for (const script of scripts)
if (!isUndoScript(script)
&& versionLog.findIndex(x => x.file == script) === -1) {
silent = false;
break;
}
if (silent) continue;
logVersion(`[${versionNumber}]`.cyan, versionName);
2020-12-02 07:35:26 +00:00
for (const script of scripts) {
if (!/^[0-9]{2}-[a-zA-Z0-9_]+(.undo)?\.sql$/.test(script)) {
logScript('[W]'.yellow, script, `Wrong file name.`);
2020-12-02 07:35:26 +00:00
continue;
}
if (isUndoScript(script))
continue;
2020-12-02 07:35:26 +00:00
const [[row]] = await conn.query(
`SELECT errorNumber FROM versionLog
WHERE code = ?
AND number = ?
AND file = ?`,
[
opts.code,
versionNumber,
script
]
);
const apply = !row || row.errorNumber;
const actionMsg = apply ? '[+]'.green : '[I]'.blue;
logScript(actionMsg, script);
if (!apply) continue;
let err;
try {
await connExt.queryFromFile(pushConn,
2022-02-02 04:05:31 +00:00
`${scriptsDir}/${script}`);
} catch (e) {
err = e;
}
await conn.query(
`INSERT INTO versionLog
SET code = ?,
number = ?,
file = ?,
user = USER(),
updated = NOW(),
errorNumber = ?,
errorMessage = ?
ON DUPLICATE KEY UPDATE
updated = VALUES(updated),
user = VALUES(user),
errorNumber = VALUES(errorNumber),
errorMessage = VALUES(errorMessage)`,
[
opts.code,
versionNumber,
script,
err && err.errno,
err && err.message
]
);
if (err) throw err;
2020-12-02 07:35:26 +00:00
nChanges++;
}
2022-02-07 14:54:24 +00:00
await this.updateVersion('number', versionNumber);
2020-12-02 07:35:26 +00:00
}
}
// Apply routines
2020-12-02 07:35:26 +00:00
console.log('Applying changed routines.');
2022-03-17 21:45:05 +00:00
const gitExists = await fs.pathExists(`${opts.workspace}/.git`);
2020-12-02 07:35:26 +00:00
let nRoutines = 0;
const changes = await this.changedRoutines(version.gitCommit);
2020-12-02 07:35:26 +00:00
const routines = [];
for (const change of changes)
if (change.isRoutine)
2021-10-27 10:56:25 +00:00
routines.push([
change.schema,
change.name,
change.type.name
]);
2020-12-02 07:35:26 +00:00
if (routines.length) {
await conn.query(
`DROP TEMPORARY TABLE IF EXISTS tProcsPriv`
);
await conn.query(
`CREATE TEMPORARY TABLE tProcsPriv
ENGINE = MEMORY
SELECT * FROM mysql.procs_priv
2021-10-27 10:56:25 +00:00
WHERE (Db, Routine_name, Routine_type) IN (?)`,
2020-12-02 07:35:26 +00:00
[routines]
);
}
const engine = new ExporterEngine(conn, opts);
2022-02-02 04:05:31 +00:00
await engine.init();
async function finalize() {
await engine.saveInfo();
if (routines.length) {
await conn.query('FLUSH PRIVILEGES');
await conn.query(`DROP TEMPORARY TABLE tProcsPriv`);
}
}
for (const change of changes)
try {
if (opts.triggers && change.type.name === 'TRIGGER')
continue;
2022-02-02 04:05:31 +00:00
const schema = change.schema;
const name = change.name;
const type = change.type.name.toLowerCase();
const fullPath = `${opts.routinesDir}/${change.path}.sql`;
2020-12-04 09:15:29 +00:00
const exists = await fs.pathExists(fullPath);
2022-02-02 04:05:31 +00:00
let newSql;
if (exists)
newSql = await fs.readFile(fullPath, 'utf8');
const oldSql = await engine.fetchRoutine(type, schema, name);
const oldSum = engine.getShaSum(type, schema, name);
const isMockFn = type == 'function'
&& schema == opts.versionSchema
&& opts.remote == 'local'
&& opts.mockDate
&& opts.mockFunctions
&& opts.mockFunctions.indexOf(name) !== -1;
const ignore = newSql == oldSql || isMockFn;
2022-02-02 04:05:31 +00:00
let statusMsg;
if (exists && !oldSql)
statusMsg = '[+]'.green;
else if (!exists)
statusMsg = '[-]'.red;
else
statusMsg = '[·]'.yellow;
2022-02-02 04:05:31 +00:00
let actionMsg;
if (ignore)
2022-02-02 04:05:31 +00:00
actionMsg = '[I]'.blue;
else
actionMsg = '[A]'.green;
2022-02-02 04:05:31 +00:00
2020-12-02 07:35:26 +00:00
const typeMsg = `[${change.type.abbr}]`[change.type.color];
console.log('',
(statusMsg + actionMsg).bold,
typeMsg.bold,
change.fullName
);
2020-12-02 07:35:26 +00:00
if (!ignore) {
const scapedSchema = SqlString.escapeId(schema, true);
if (exists) {
if (change.type.name === 'VIEW')
await pushConn.query(`USE ${scapedSchema}`);
await connExt.multiQuery(pushConn, newSql);
if (change.isRoutine) {
await conn.query(
`INSERT IGNORE INTO mysql.procs_priv
SELECT * FROM tProcsPriv
WHERE Db = ?
AND Routine_name = ?
AND Routine_type = ?`,
[schema, name, change.type.name]
);
}
if (opts.sums || oldSum || (opts.sumViews && type === 'view'))
await engine.fetchShaSum(type, schema, name);
} else {
const escapedName =
scapedSchema + '.' +
SqlString.escapeId(name, true);
const query = `DROP ${change.type.name} IF EXISTS ${escapedName}`;
await pushConn.query(query);
2022-02-02 04:05:31 +00:00
engine.deleteShaSum(type, schema, name);
}
nRoutines++;
2020-12-02 07:35:26 +00:00
}
} catch (err) {
try {
await finalize();
} catch (e) {
2022-03-30 11:20:58 +00:00
console.error(e);
}
throw err;
2020-12-02 07:35:26 +00:00
}
await finalize();
2020-12-02 07:35:26 +00:00
if (nRoutines > 0) {
console.log(` -> ${nRoutines} routines have changed.`);
} else
console.log(` -> No routines changed.`);
2021-10-16 06:44:19 +00:00
2022-04-30 00:33:41 +00:00
if (gitExists && opts.commit) {
2022-03-17 21:45:05 +00:00
const repo = await nodegit.Repository.open(this.opts.workspace);
const head = await repo.getHeadCommit();
2021-10-16 06:44:19 +00:00
2022-04-04 18:53:18 +00:00
if (head && version.gitCommit !== head.sha())
2022-03-17 21:45:05 +00:00
await this.updateVersion('gitCommit', head.sha());
}
// End
2022-02-02 04:05:31 +00:00
await pushConn.end();
2020-12-02 07:35:26 +00:00
}
2022-02-07 14:54:24 +00:00
async updateVersion(column, value) {
column = SqlString.escapeId(column, true);
await this.conn.query(
`INSERT INTO version
SET code = ?,
${column} = ?,
updated = NOW()
ON DUPLICATE KEY UPDATE
${column} = VALUES(${column}),
updated = VALUES(updated)`,
[
2022-02-07 14:54:24 +00:00
this.opts.code,
value
]
);
2020-12-02 07:35:26 +00:00
}
async changedRoutines(commitSha) {
const repo = await this.myt.openRepo();
const changes = [];
const changesMap = new Map();
async function pushChanges(diff) {
if (!diff) return;
const patches = await diff.patches();
for (const patch of patches) {
const path = patch.newFile().path();
const match = path.match(/^routines\/(.+)\.sql$/);
if (!match) continue;
let change = changesMap.get(match[1]);
if (!change) {
change = {path: match[1]};
changes.push(change);
changesMap.set(match[1], change);
}
change.mark = patch.isDeleted() ? '-' : '+';
}
}
const head = await repo.getHeadCommit();
if (head && commitSha) {
let commit;
let notFound;
try {
commit = await repo.getCommit(commitSha);
notFound = false;
} catch (err) {
if (err.errorFunction == 'Commit.lookup')
notFound = true;
else
throw err;
}
if (notFound) {
console.warn(`Database commit not found, trying git fetch`.yellow);
await repo.fetchAll();
commit = await repo.getCommit(commitSha);
}
const commitTree = await commit.getTree();
const headTree = await head.getTree();
const diff = await headTree.diff(commitTree);
await pushChanges(diff);
}
await pushChanges(await repoExt.getUnstaged(repo));
await pushChanges(await repoExt.getStaged(repo));
const routines = [];
for (const change of changes)
routines.push(new Routine(change));
return routines.sort((a, b) => {
if (b.mark != a.mark)
return b.mark == '-' ? -1 : 1;
if (b.type.name !== a.type.name) {
if (b.type.name == 'VIEW')
return -1;
if (a.type.name == 'VIEW')
return 1;
}
return a.path.localeCompare(b.path);
});
}
2020-12-02 07:35:26 +00:00
}
2020-12-04 09:15:29 +00:00
const typeMap = {
events: {
name: 'EVENT',
abbr: 'EVNT',
color: 'cyan'
},
functions: {
name: 'FUNCTION',
abbr: 'FUNC',
color: 'cyan'
},
procedures: {
name: 'PROCEDURE',
abbr: 'PROC',
color: 'yellow'
},
triggers: {
name: 'TRIGGER',
abbr: 'TRIG',
color: 'blue'
},
views: {
name: 'VIEW',
abbr: 'VIEW',
color: 'magenta'
},
};
const routineTypes = new Set([
'FUNCTION',
'PROCEDURE'
]);
2020-12-04 09:15:29 +00:00
class Routine {
constructor(change) {
const path = change.path;
const split = path.split('/');
const schema = split[0];
const type = typeMap[split[1]];
const name = split[2];
2022-06-09 09:42:03 +00:00
if (split.length !== 3 || !type)
throw new Error(`Wrong routine path for '${path}', check that the sql file is located in the correct directory`);
2020-12-04 09:15:29 +00:00
Object.assign(this, {
path,
mark: change.mark,
type,
schema,
name,
fullName: `${schema}.${name}`,
2021-10-27 10:56:25 +00:00
isRoutine: routineTypes.has(type.name)
2020-12-04 09:15:29 +00:00
});
}
}
2020-12-02 07:35:26 +00:00
module.exports = Push;
if (require.main === module)
2022-12-21 13:17:50 +00:00
new Myt().run(Push);