SHA sum, export definer, don't stop on errors

This commit is contained in:
Juan Ferrer 2021-10-22 16:11:03 +02:00
parent 1566847ecd
commit 99461688cd
16 changed files with 3507 additions and 1568 deletions

View File

@ -44,7 +44,7 @@ otherwise indicated, the default remote is *local*.
Database versioning commands: Database versioning commands:
* **init**: Initialize an empty workspace. * **init**: Initialize an empty workspace.
* **pull**: Export database routines into workspace. * **pull**: Incorporates database routines changes into workspace.
* **push**: Apply changes into database. * **push**: Apply changes into database.
Local server management commands: Local server management commands:
@ -163,4 +163,5 @@ Create a lock during push to avoid collisions.
* [Git](https://git-scm.com/) * [Git](https://git-scm.com/)
* [nodejs](https://nodejs.org/) * [nodejs](https://nodejs.org/)
* [NodeGit](https://www.nodegit.org/)
* [docker](https://www.docker.com/) * [docker](https://www.docker.com/)

View File

@ -1,6 +1,6 @@
DROP EVENT IF EXISTS `<%- schema %>`.`<%- name %>`; DROP EVENT IF EXISTS <%- schema %>.<%- name %>;
DELIMITER $$ DELIMITER $$
CREATE DEFINER=`root`@`%` EVENT `<%- schema %>`.`<%- name %>` CREATE DEFINER=<%- definer %> EVENT <%- schema %>.<%- name %>
ON SCHEDULE EVERY <%- intervalValue %> <%- intervalField %> ON SCHEDULE EVERY <%- intervalValue %> <%- intervalField %>
ON COMPLETION <%- onCompletion %> ON COMPLETION <%- onCompletion %>
<% if (status == 'ENABLED') { %>ENABLE<% } else { %>DISABLE<% } %> <% if (status == 'ENABLED') { %>ENABLE<% } else { %>DISABLE<% } %>

View File

@ -1,17 +1,18 @@
SELECT SELECT
EVENT_NAME `name`, `EVENT_NAME` AS `name`,
DEFINER `definer`, `DEFINER` AS `definer`,
EVENT_DEFINITION `body`, `EVENT_DEFINITION` AS `body`,
EVENT_TYPE `type`, `EVENT_TYPE` AS `type`,
EXECUTE_AT `execute_at`, `EXECUTE_AT` AS `execute_at`,
INTERVAL_VALUE `intervalValue`, `INTERVAL_VALUE` AS `intervalValue`,
INTERVAL_FIELD `intervalField`, `INTERVAL_FIELD` AS `intervalField`,
STARTS `starts`, `STARTS` AS `starts`,
ENDS `ends`, `ENDS` AS `ends`,
STATUS `status`, `STATUS` AS `status`,
ON_COMPLETION `onCompletion`, `ON_COMPLETION` AS `onCompletion`,
EVENT_COMMENT `comment`, `EVENT_COMMENT` AS `comment`,
LAST_ALTERED `modified` `LAST_ALTERED` AS `modified`
FROM information_schema.EVENTS FROM `information_schema`.`EVENTS`
WHERE EVENT_SCHEMA = ? WHERE `EVENT_SCHEMA` = ?
ORDER BY `name`

View File

@ -1,6 +1,6 @@
DROP FUNCTION IF EXISTS `<%- schema %>`.`<%- name %>`; DROP FUNCTION IF EXISTS <%- schema %>.<%- name %>;
DELIMITER $$ DELIMITER $$
CREATE DEFINER='root'@'%' FUNCTION `<%- schema %>`.`<%- name %>`(<%- paramList %>) CREATE DEFINER=<%- definer %> FUNCTION <%- schema %>.<%- name %>(<%- paramList %>)
RETURNS <%- returns %> RETURNS <%- returns %>
<% if (isDeterministic != 'NO') { %>DETERMINISTIC<% } else { %>NOT DETERMINISTIC<% } %> <% if (isDeterministic != 'NO') { %>DETERMINISTIC<% } else { %>NOT DETERMINISTIC<% } %>
<%- body %>$$ <%- body %>$$

View File

@ -2,10 +2,11 @@
SELECT SELECT
`name`, `name`,
`definer`, `definer`,
`param_list` paramList, `param_list` AS `paramList`,
`returns`, `returns`,
`is_deterministic` isDeterministic, `is_deterministic` AS `isDeterministic`,
`body`, `body`,
`modified` `modified`
FROM mysql.proc FROM `mysql`.`proc`
WHERE `db` = ? AND `type` = 'FUNCTION' WHERE `db` = ? AND `type` = 'FUNCTION'
ORDER BY `name`

View File

@ -1,5 +1,5 @@
DROP PROCEDURE IF EXISTS `<%- schema %>`.`<%- name %>`; DROP PROCEDURE IF EXISTS <%- schema %>.<%- name %>;
DELIMITER $$ DELIMITER $$
CREATE DEFINER='root'@'%' PROCEDURE `<%- schema %>`.`<%- name %>`(<%- paramList %>) CREATE DEFINER=<%- definer %> PROCEDURE <%- schema %>.<%- name %>(<%- paramList %>)
<%- body %>$$ <%- body %>$$
DELIMITER ; DELIMITER ;

View File

@ -2,8 +2,9 @@
SELECT SELECT
`name`, `name`,
`definer`, `definer`,
`param_list` paramList, `param_list` AS `paramList`,
`body`, `body`,
`modified` `modified`
FROM mysql.proc FROM `mysql`.`proc`
WHERE db = ? AND type = 'PROCEDURE' WHERE `db` = ? AND `type` = 'PROCEDURE'
ORDER BY `name`

View File

@ -1,6 +1,6 @@
DROP TRIGGER IF EXISTS `<%- schema %>`.`<%- name %>`; DROP TRIGGER IF EXISTS <%- schema %>.<%- name %>;
DELIMITER $$ DELIMITER $$
CREATE DEFINER=`root`@`%` TRIGGER `<%- schema %>`.`<%- name %>` CREATE DEFINER=<%- definer %> TRIGGER <%- schema %>.<%- name %>
<%- actionTiming %> <%- actionType %> ON `<%- table %>` <%- actionTiming %> <%- actionType %> ON `<%- table %>`
FOR EACH ROW FOR EACH ROW
<%- body %>$$ <%- body %>$$

View File

@ -1,11 +1,12 @@
SELECT SELECT
TRIGGER_NAME `name`, `TRIGGER_NAME` AS `name`,
DEFINER `definer`, `DEFINER` AS `definer`,
ACTION_TIMING `actionTiming`, `ACTION_TIMING` AS `actionTiming`,
EVENT_MANIPULATION `actionType`, `EVENT_MANIPULATION` AS `actionType`,
EVENT_OBJECT_TABLE `table`, `EVENT_OBJECT_TABLE` AS `table`,
ACTION_STATEMENT `body`, `ACTION_STATEMENT` AS `body`,
CREATED `modified` `CREATED` AS `modified`
FROM information_schema.TRIGGERS FROM `information_schema`.`TRIGGERS`
WHERE TRIGGER_SCHEMA = ? WHERE `TRIGGER_SCHEMA` = ?
ORDER BY `name`

View File

@ -1,5 +1,5 @@
CREATE OR REPLACE DEFINER = `root`@`%` CREATE OR REPLACE DEFINER=<%- definer %>
SQL SECURITY <%- securityType %> SQL SECURITY <%- securityType %>
VIEW `<%- schema %>`.`<%- name %>` VIEW <%- schema %>.<%- name %>
AS <%- definition %><% if (checkOption != 'NONE') { %> AS <%- definition %><% if (checkOption != 'NONE') { %>
WITH CASCADED CHECK OPTION<% } %> WITH CASCADED CHECK OPTION<% } %>

View File

@ -1,10 +1,11 @@
SELECT SELECT
TABLE_NAME `name`, `TABLE_NAME` AS `name`,
VIEW_DEFINITION `definition`, `VIEW_DEFINITION` AS `definition`,
CHECK_OPTION `checkOption`, `CHECK_OPTION` AS `checkOption`,
IS_UPDATABLE `isUpdatable`, `IS_UPDATABLE` AS `isUpdatable`,
DEFINER `definer`, `DEFINER` AS `definer`,
SECURITY_TYPE `securityType` `SECURITY_TYPE` AS `securityType`
FROM information_schema.VIEWS FROM `information_schema`.`VIEWS`
WHERE TABLE_SCHEMA = ? WHERE `TABLE_SCHEMA` = ?
ORDER BY `name`

View File

@ -2,27 +2,72 @@
const MyVC = require('./myvc'); const MyVC = require('./myvc');
const fs = require('fs-extra'); const fs = require('fs-extra');
const ejs = require('ejs'); const ejs = require('ejs');
const shajs = require('sha.js');
const nodegit = require('nodegit'); const nodegit = require('nodegit');
class Pull { class Pull {
get myOpts() {
return {
alias: {
force: 'f',
checkout: 'c'
}
};
}
async run(myvc, opts) { async run(myvc, opts) {
const conn = await myvc.dbConnect(); const conn = await myvc.dbConnect();
/* const repo = await myvc.openRepo();
const version = await myvc.fetchDbVersion();
let repo;
if (version && version.gitCommit) { if (!opts.force) {
console.log(version); async function hasChanges(diff) {
repo = await nodegit.Repository.open(opts.workspace); if (diff)
const commit = await repo.getCommit(version.gitCommit); for (const patch of await diff.patches()) {
const now = parseInt(new Date().getTime() / 1000); const match = patch
const branch = await nodegit.Branch.create(repo, .newFile()
`myvc_${now}`, commit, () => {}); .path()
await repo.checkoutBranch(branch); .match(/^routines\/(.+)\.sql$/);
if (match) return true;
}
return false;
}
// Check for unstaged changes
const unstagedDiff = await myvc.getUnstaged(repo);
if (await hasChanges(unstagedDiff))
throw new Error('You have unstaged changes, save them before pull');
// Check for staged changes
const stagedDiff = await myvc.getStaged(repo);
if (await hasChanges(stagedDiff))
throw new Error('You have staged changes, save them before pull');
} }
return; // Checkout to remote commit
*/
if (opts.checkout) {
const version = await myvc.fetchDbVersion();
if (version && version.gitCommit) {
const now = parseInt(new Date().toJSON());
const branchName = `myvc-pull_${now}`;
console.log(`Creating branch '${branchName}' from database commit.`);
const commit = await repo.getCommit(version.gitCommit);
const branch = await nodegit.Branch.create(repo,
`myvc-pull_${now}`, commit, () => {});
await repo.checkoutBranch(branch);
}
}
// Export routines to SQL files
console.log(`Incorporating routine changes.`);
for (const exporter of exporters) for (const exporter of exporters)
await exporter.init(); await exporter.init();
@ -36,26 +81,45 @@ class Pull {
await fs.remove(`${exportDir}/${schema}`, {recursive: true}); await fs.remove(`${exportDir}/${schema}`, {recursive: true});
} }
let shaSums;
const shaFile = `${opts.workspace}/.shasums.json`;
if (await fs.pathExists(shaFile))
shaSums = JSON.parse(await fs.readFile(shaFile, 'utf8'));
else
shaSums = {};
for (const schema of opts.schemas) { for (const schema of opts.schemas) {
let schemaDir = `${exportDir}/${schema}`; let schemaDir = `${exportDir}/${schema}`;
if (!await fs.pathExists(schemaDir)) if (!await fs.pathExists(schemaDir))
await fs.mkdir(schemaDir); await fs.mkdir(schemaDir);
for (const exporter of exporters) let schemaSums = shaSums[schema];
await exporter.export(conn, exportDir, schema); if (!schemaSums) schemaSums = shaSums[schema] = {};
for (const exporter of exporters) {
const objectType = exporter.objectType;
let objectSums = schemaSums[objectType];
if (!objectSums) objectSums = schemaSums[objectType] = {};
await exporter.export(conn, exportDir, schema, objectSums);
}
} }
await fs.writeFile(shaFile, JSON.stringify(shaSums, null, ' '));
} }
} }
class Exporter { class Exporter {
constructor(objectName) { constructor(objectType) {
this.objectName = objectName; this.objectType = objectType;
this.dstDir = `${objectName}s`; this.dstDir = `${objectType}s`;
} }
async init() { async init() {
const templateDir = `${__dirname}/exporters/${this.objectName}`; const templateDir = `${__dirname}/exporters/${this.objectType}`;
this.query = await fs.readFile(`${templateDir}.sql`, 'utf8'); this.query = await fs.readFile(`${templateDir}.sql`, 'utf8');
const templateFile = await fs.readFile(`${templateDir}.ejs`, 'utf8'); const templateFile = await fs.readFile(`${templateDir}.ejs`, 'utf8');
@ -65,7 +129,7 @@ class Exporter {
this.formatter = require(`${templateDir}.js`); this.formatter = require(`${templateDir}.js`);
} }
async export(conn, exportDir, schema) { async export(conn, exportDir, schema, shaSums) {
const [res] = await conn.query(this.query, [schema]); const [res] = await conn.query(this.query, [schema]);
if (!res.length) return; if (!res.length) return;
@ -90,17 +154,31 @@ class Exporter {
if (this.formatter) if (this.formatter)
this.formatter(params, schema) this.formatter(params, schema)
params.schema = schema; const routineName = params.name;
const split = params.definer.split('@');
params.schema = conn.escapeId(schema);
params.name = conn.escapeId(routineName, true);
params.definer =
`${conn.escapeId(split[0], true)}@${conn.escapeId(split[1], true)}`;
const sql = this.template(params); const sql = this.template(params);
const routineFile = `${routineDir}/${params.name}.sql`; const routineFile = `${routineDir}/${routineName}.sql`;
const shaSum = shajs('sha256')
.update(JSON.stringify(sql))
.digest('hex');
shaSums[routineName] = shaSum;
let changed = true; let changed = true;
if (await fs.pathExists(routineFile)) { if (await fs.pathExists(routineFile)) {
const currentSql = await fs.readFile(routineFile, 'utf8'); const currentSql = await fs.readFile(routineFile, 'utf8');
changed = currentSql !== sql; changed = shaSums[routineName] !== shaSum;;
} }
if (changed) if (changed) {
await fs.writeFile(routineFile, sql); await fs.writeFile(routineFile, sql);
shaSums[routineName] = shaSum;
}
} }
} }
} }

View File

@ -177,15 +177,25 @@ class Push {
console.log('', actionMsg.bold, typeMsg.bold, change.fullName); console.log('', actionMsg.bold, typeMsg.bold, change.fullName);
if (exists) try {
await this.queryFromFile(pushConn, `routines/${change.path}.sql`); const scapedSchema = pushConn.escapeId(change.schema, true);
else {
const escapedName =
conn.escapeId(change.schema, true) + '.' +
conn.escapeId(change.name, true);
const query = `DROP ${change.type.name} IF EXISTS ${escapedName}`; if (exists) {
await conn.query(query); await pushConn.query(`USE ${scapedSchema}`);
await this.queryFromFile(pushConn, `routines/${change.path}.sql`);
} else {
const escapedName =
scapedSchema + '.' +
pushConn.escapeId(change.name, true);
const query = `DROP ${change.type.name} IF EXISTS ${escapedName}`;
await pushConn.query(query);
}
} catch (err) {
if (err.sqlState)
console.warn('Warning:'.yellow, err.message);
else
throw err;
} }
nRoutines++; nRoutines++;

51
myvc.js
View File

@ -196,15 +196,9 @@ class MyVC {
} }
async changedRoutines(commitSha) { async changedRoutines(commitSha) {
const {opts} = this; const repo = await this.openRepo();
if (!await fs.pathExists(`${opts.workspace}/.git`))
throw new Error ('Git not initialized');
const changes = []; const changes = [];
const changesMap = new Map(); const changesMap = new Map();
const repo = await nodegit.Repository.open(opts.workspace);
const head = await repo.getHeadCommit();
async function pushChanges(diff) { async function pushChanges(diff) {
const patches = await diff.patches(); const patches = await diff.patches();
@ -224,7 +218,7 @@ class MyVC {
} }
} }
// Committed const head = await repo.getHeadCommit();
if (head && commitSha) { if (head && commitSha) {
const commit = await repo.getCommit(commitSha); const commit = await repo.getCommit(commitSha);
@ -235,15 +229,30 @@ class MyVC {
await pushChanges(diff); await pushChanges(diff);
} }
// Unstaged await pushChanges(await this.getUnstaged(repo));
const diff = await nodegit.Diff.indexToWorkdir(repo, null, { const stagedDiff = await this.getStaged(repo);
flags: nodegit.Diff.OPTION.SHOW_UNTRACKED_CONTENT if (stagedDiff) await pushChanges(stagedDiff);
| nodegit.Diff.OPTION.RECURSE_UNTRACKED_DIRS
return changes.sort((a, b) => {
if (b.mark != a.mark)
return b.mark == '-' ? 1 : -1;
return a.path.localeCompare(b.path);
}); });
await pushChanges(diff); }
// Staged async openRepo() {
const {opts} = this;
if (!await fs.pathExists(`${opts.workspace}/.git`))
throw new Error ('Git not initialized');
return await nodegit.Repository.open(opts.workspace);
}
async getStaged(repo) {
const head = await repo.getHeadCommit();
try { try {
const emptyTree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; const emptyTree = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
@ -251,15 +260,17 @@ class MyVC {
? head.getTree() ? head.getTree()
: nodegit.Tree.lookup(repo, emptyTree) : nodegit.Tree.lookup(repo, emptyTree)
); );
const stagedDiff = await nodegit.Diff.treeToIndex(repo, headTree, null); return await nodegit.Diff.treeToIndex(repo, headTree, null);
await pushChanges(stagedDiff);
} catch (err) { } catch (err) {
console.warn('Cannot fetch staged changes:', err.message); console.warn('Cannot fetch staged changes:', err.message);
} }
}
return changes.sort(
(a, b) => b.mark == '-' && b.mark != a.mark ? 1 : -1 async getUnstaged(repo) {
); return await nodegit.Diff.indexToWorkdir(repo, null, {
flags: nodegit.Diff.OPTION.SHOW_UNTRACKED_CONTENT
| nodegit.Diff.OPTION.RECURSE_UNTRACKED_DIRS
});
} }
async cachedChanges() { async cachedChanges() {

4758
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "myvc", "name": "myvc",
"version": "1.1.10", "version": "1.1.11",
"author": "Verdnatura Levante SL", "author": "Verdnatura Levante SL",
"description": "MySQL Version Control", "description": "MySQL Version Control",
"license": "GPL-3.0", "license": "GPL-3.0",
@ -18,10 +18,10 @@
"ini": "^1.3.8", "ini": "^1.3.8",
"mysql2": "^2.2.5", "mysql2": "^2.2.5",
"nodegit": "^0.27.0", "nodegit": "^0.27.0",
"require-yaml": "0.0.1" "require-yaml": "0.0.1",
"sha.js": "^2.4.11"
}, },
"main": "index.js", "main": "index.js",
"devDependencies": {},
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },