Version command, operand to option, hashsum fixes
This commit is contained in:
parent
99461688cd
commit
24b35e2e90
|
@ -46,6 +46,7 @@ Database versioning commands:
|
||||||
* **init**: Initialize an empty workspace.
|
* **init**: Initialize an empty workspace.
|
||||||
* **pull**: Incorporates database routines changes into workspace.
|
* **pull**: Incorporates database routines changes into workspace.
|
||||||
* **push**: Apply changes into database.
|
* **push**: Apply changes into database.
|
||||||
|
* **version**: Creates a new version.
|
||||||
|
|
||||||
Local server management commands:
|
Local server management commands:
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,9 @@ const docker = require('./docker');
|
||||||
* Dumps structure and fixtures from remote.
|
* Dumps structure and fixtures from remote.
|
||||||
*/
|
*/
|
||||||
class Dump {
|
class Dump {
|
||||||
get myOpts() {
|
get localOpts() {
|
||||||
return {
|
return {
|
||||||
|
operand: 'remote',
|
||||||
alias: {
|
alias: {
|
||||||
remote: 'r'
|
remote: 'r'
|
||||||
},
|
},
|
||||||
|
|
72
myvc-pull.js
72
myvc-pull.js
|
@ -6,8 +6,9 @@ const shajs = require('sha.js');
|
||||||
const nodegit = require('nodegit');
|
const nodegit = require('nodegit');
|
||||||
|
|
||||||
class Pull {
|
class Pull {
|
||||||
get myOpts() {
|
get localOpts() {
|
||||||
return {
|
return {
|
||||||
|
operand: 'remote',
|
||||||
alias: {
|
alias: {
|
||||||
force: 'f',
|
force: 'f',
|
||||||
checkout: 'c'
|
checkout: 'c'
|
||||||
|
@ -68,6 +69,14 @@ class Pull {
|
||||||
|
|
||||||
console.log(`Incorporating routine changes.`);
|
console.log(`Incorporating routine changes.`);
|
||||||
|
|
||||||
|
const exporters = [
|
||||||
|
new Exporter('function'),
|
||||||
|
new Exporter('procedure'),
|
||||||
|
new Exporter('view'),
|
||||||
|
new Exporter('trigger'),
|
||||||
|
new Exporter('event')
|
||||||
|
];
|
||||||
|
|
||||||
for (const exporter of exporters)
|
for (const exporter of exporters)
|
||||||
await exporter.init();
|
await exporter.init();
|
||||||
|
|
||||||
|
@ -75,40 +84,47 @@ class Pull {
|
||||||
if (!await fs.pathExists(exportDir))
|
if (!await fs.pathExists(exportDir))
|
||||||
await fs.mkdir(exportDir);
|
await fs.mkdir(exportDir);
|
||||||
|
|
||||||
|
// Initialize SHA data
|
||||||
|
|
||||||
|
let newShaSums = {};
|
||||||
|
let oldShaSums;
|
||||||
|
const shaFile = `${opts.workspace}/.shasums.json`;
|
||||||
|
|
||||||
|
if (await fs.pathExists(shaFile))
|
||||||
|
oldShaSums = JSON.parse(await fs.readFile(shaFile, 'utf8'));
|
||||||
|
|
||||||
|
// Delete old schemas
|
||||||
|
|
||||||
const schemas = await fs.readdir(exportDir);
|
const schemas = await fs.readdir(exportDir);
|
||||||
for (const schema of schemas) {
|
for (const schema of schemas) {
|
||||||
if (opts.schemas.indexOf(schema) == -1)
|
if (opts.schemas.indexOf(schema) == -1)
|
||||||
await fs.remove(`${exportDir}/${schema}`, {recursive: true});
|
await fs.remove(`${exportDir}/${schema}`, {recursive: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
let shaSums;
|
// Export objects to SQL files
|
||||||
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}`;
|
newShaSums[schema] = {};
|
||||||
|
|
||||||
|
let schemaDir = `${exportDir}/${schema}`;
|
||||||
if (!await fs.pathExists(schemaDir))
|
if (!await fs.pathExists(schemaDir))
|
||||||
await fs.mkdir(schemaDir);
|
await fs.mkdir(schemaDir);
|
||||||
|
|
||||||
let schemaSums = shaSums[schema];
|
|
||||||
if (!schemaSums) schemaSums = shaSums[schema] = {};
|
|
||||||
|
|
||||||
for (const exporter of exporters) {
|
for (const exporter of exporters) {
|
||||||
const objectType = exporter.objectType;
|
const objectType = exporter.objectType;
|
||||||
|
const newSums = newShaSums[schema][objectType] = {};
|
||||||
|
let oldSums = {};
|
||||||
|
try {
|
||||||
|
oldSums = oldShaSums[schema][objectType];
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
let objectSums = schemaSums[objectType];
|
await exporter.export(conn, exportDir, schema, newSums, oldSums);
|
||||||
if (!objectSums) objectSums = schemaSums[objectType] = {};
|
|
||||||
|
|
||||||
await exporter.export(conn, exportDir, schema, objectSums);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(shaFile, JSON.stringify(shaSums, null, ' '));
|
// Save SHA data
|
||||||
|
|
||||||
|
await fs.writeFile(shaFile, JSON.stringify(newShaSums, null, ' '));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +145,7 @@ class Exporter {
|
||||||
this.formatter = require(`${templateDir}.js`);
|
this.formatter = require(`${templateDir}.js`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async export(conn, exportDir, schema, shaSums) {
|
async export(conn, exportDir, schema, newSums, oldSums) {
|
||||||
const [res] = await conn.query(this.query, [schema]);
|
const [res] = await conn.query(this.query, [schema]);
|
||||||
if (!res.length) return;
|
if (!res.length) return;
|
||||||
|
|
||||||
|
@ -167,30 +183,14 @@ class Exporter {
|
||||||
const shaSum = shajs('sha256')
|
const shaSum = shajs('sha256')
|
||||||
.update(JSON.stringify(sql))
|
.update(JSON.stringify(sql))
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
shaSums[routineName] = shaSum;
|
newSums[routineName] = shaSum;
|
||||||
|
|
||||||
let changed = true;
|
if (oldSums[routineName] !== shaSum)
|
||||||
|
|
||||||
if (await fs.pathExists(routineFile)) {
|
|
||||||
const currentSql = await fs.readFile(routineFile, 'utf8');
|
|
||||||
changed = shaSums[routineName] !== shaSum;;
|
|
||||||
}
|
|
||||||
if (changed) {
|
|
||||||
await fs.writeFile(routineFile, sql);
|
await fs.writeFile(routineFile, sql);
|
||||||
shaSums[routineName] = shaSum;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exporters = [
|
|
||||||
new Exporter('function'),
|
|
||||||
new Exporter('procedure'),
|
|
||||||
new Exporter('view'),
|
|
||||||
new Exporter('trigger'),
|
|
||||||
new Exporter('event')
|
|
||||||
];
|
|
||||||
|
|
||||||
module.exports = Pull;
|
module.exports = Pull;
|
||||||
|
|
||||||
if (require.main === module)
|
if (require.main === module)
|
||||||
|
|
15
myvc-push.js
15
myvc-push.js
|
@ -10,8 +10,9 @@ const nodegit = require('nodegit');
|
||||||
* @property {Boolean} user Whether to change current user version
|
* @property {Boolean} user Whether to change current user version
|
||||||
*/
|
*/
|
||||||
class Push {
|
class Push {
|
||||||
get myOpts() {
|
get localOpts() {
|
||||||
return {
|
return {
|
||||||
|
operand: 'remote',
|
||||||
alias: {
|
alias: {
|
||||||
force: 'f',
|
force: 'f',
|
||||||
user: 'u'
|
user: 'u'
|
||||||
|
@ -252,9 +253,11 @@ class Push {
|
||||||
`INSERT INTO versionUser
|
`INSERT INTO versionUser
|
||||||
SET code = ?,
|
SET code = ?,
|
||||||
user = ?,
|
user = ?,
|
||||||
${column} = ?
|
${column} = ?,
|
||||||
|
updated = NOW()
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
${column} = VALUES(${column})`,
|
${column} = VALUES(${column}),
|
||||||
|
updated = VALUES(updated)`,
|
||||||
[
|
[
|
||||||
opts.code,
|
opts.code,
|
||||||
user,
|
user,
|
||||||
|
@ -265,9 +268,11 @@ class Push {
|
||||||
await this.conn.query(
|
await this.conn.query(
|
||||||
`INSERT INTO version
|
`INSERT INTO version
|
||||||
SET code = ?,
|
SET code = ?,
|
||||||
${column} = ?
|
${column} = ?,
|
||||||
|
updated = NOW()
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
${column} = VALUES(${column})`,
|
${column} = VALUES(${column}),
|
||||||
|
updated = VALUES(updated)`,
|
||||||
[
|
[
|
||||||
opts.code,
|
opts.code,
|
||||||
value
|
value
|
||||||
|
|
|
@ -16,7 +16,7 @@ const Server = require('./server/server');
|
||||||
* @property {Boolean} random Whether to use a random container name
|
* @property {Boolean} random Whether to use a random container name
|
||||||
*/
|
*/
|
||||||
class Run {
|
class Run {
|
||||||
get myOpts() {
|
get localOpts() {
|
||||||
return {
|
return {
|
||||||
alias: {
|
alias: {
|
||||||
ci: 'c',
|
ci: 'c',
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
|
||||||
|
const MyVC = require('./myvc');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new version.
|
||||||
|
*/
|
||||||
|
class Version {
|
||||||
|
mainOpt = 'name';
|
||||||
|
get localOpts() {
|
||||||
|
return {
|
||||||
|
operand: 'name',
|
||||||
|
name: {
|
||||||
|
name: 'n'
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
remote: 'production'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(myvc, opts) {
|
||||||
|
const verionsDir =`${opts.workspace}/versions`;
|
||||||
|
let versionDir;
|
||||||
|
let versionName = opts.name;
|
||||||
|
|
||||||
|
// Fetch last version number
|
||||||
|
|
||||||
|
const conn = await myvc.dbConnect();
|
||||||
|
const version = await myvc.fetchDbVersion() || {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await conn.query('START TRANSACTION');
|
||||||
|
|
||||||
|
const [[row]] = await conn.query(
|
||||||
|
`SELECT lastNumber FROM version WHERE code = ? FOR UPDATE`,
|
||||||
|
[opts.code]
|
||||||
|
);
|
||||||
|
const lastVersion = row && row.lastNumber;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Database information:`
|
||||||
|
+ `\n -> Version: ${version.number}`
|
||||||
|
+ `\n -> Last version: ${lastVersion}`
|
||||||
|
);
|
||||||
|
|
||||||
|
let newVersion = lastVersion ? parseInt(lastVersion) + 1 : 1;
|
||||||
|
newVersion = String(newVersion).padStart(opts.versionDigits, '0');
|
||||||
|
|
||||||
|
// Get version name
|
||||||
|
|
||||||
|
const versionNames = new Set();
|
||||||
|
const versionDirs = await fs.readdir(verionsDir);
|
||||||
|
for (const versionNameDir of versionDirs) {
|
||||||
|
const split = versionNameDir.split('-');
|
||||||
|
const versionName = split[1];
|
||||||
|
if (versionName) versionNames.add(versionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!versionName) {
|
||||||
|
let attempts;
|
||||||
|
const maxAttempts = 1000;
|
||||||
|
|
||||||
|
for (attempts = 0; attempts < maxAttempts; attempts++) {
|
||||||
|
versionName = randomName();
|
||||||
|
if (!versionNames.has(versionName)) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempts === maxAttempts)
|
||||||
|
throw new Error(`Cannot create a unique version name after ${attempts} attempts`);
|
||||||
|
} else {
|
||||||
|
const isNameValid = typeof versionName === 'string'
|
||||||
|
&& /^[a-zA-Z0-9]+$/.test(versionName);
|
||||||
|
if (!isNameValid)
|
||||||
|
throw new Error('Version name can only contain letters or numbers');
|
||||||
|
if (versionNames.has(versionName))
|
||||||
|
throw new Error('Version with same name already exists');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create version
|
||||||
|
|
||||||
|
const versionFolder = `${newVersion}-${versionName}`;
|
||||||
|
versionDir = `${verionsDir}/${versionFolder}`;
|
||||||
|
|
||||||
|
await conn.query(
|
||||||
|
`UPDATE version SET lastNumber = ? WHERE code = ?`,
|
||||||
|
[newVersion, opts.code]
|
||||||
|
);
|
||||||
|
await fs.mkdir(versionDir);
|
||||||
|
console.log(`New version folder created: ${versionFolder}`);
|
||||||
|
|
||||||
|
await conn.query('COMMIT');
|
||||||
|
} catch (err) {
|
||||||
|
await conn.query('ROLLBACK');
|
||||||
|
if (versionDir && await fs.pathExists(versionDir))
|
||||||
|
await fs.remove(versionDir, {recursive: true});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomName() {
|
||||||
|
const color = random(colors);
|
||||||
|
let plant = random(plants);
|
||||||
|
plant = plant.charAt(0).toUpperCase() + plant.slice(1);
|
||||||
|
return color + plant;
|
||||||
|
}
|
||||||
|
|
||||||
|
function random(array) {
|
||||||
|
return array[Math.floor(Math.random() * array.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
'aqua',
|
||||||
|
'azure',
|
||||||
|
'black',
|
||||||
|
'blue',
|
||||||
|
'bronze',
|
||||||
|
'brown',
|
||||||
|
'chocolate',
|
||||||
|
'crimson',
|
||||||
|
'golden',
|
||||||
|
'gray',
|
||||||
|
'green',
|
||||||
|
'lime',
|
||||||
|
'maroon',
|
||||||
|
'navy',
|
||||||
|
'orange',
|
||||||
|
'pink',
|
||||||
|
'purple',
|
||||||
|
'red',
|
||||||
|
'salmon',
|
||||||
|
'silver',
|
||||||
|
'teal',
|
||||||
|
'turquoise',
|
||||||
|
'yellow',
|
||||||
|
'wheat',
|
||||||
|
'white'
|
||||||
|
];
|
||||||
|
|
||||||
|
const plants = [
|
||||||
|
'anthurium',
|
||||||
|
'aralia',
|
||||||
|
'arborvitae',
|
||||||
|
'asparagus',
|
||||||
|
'aspidistra',
|
||||||
|
'bamboo',
|
||||||
|
'birch',
|
||||||
|
'carnation',
|
||||||
|
'camellia',
|
||||||
|
'cataractarum',
|
||||||
|
'chico',
|
||||||
|
'chrysanthemum',
|
||||||
|
'cordyline',
|
||||||
|
'cyca',
|
||||||
|
'cymbidium',
|
||||||
|
'dendro',
|
||||||
|
'dracena',
|
||||||
|
'erica',
|
||||||
|
'eucalyptus',
|
||||||
|
'fern',
|
||||||
|
'galax',
|
||||||
|
'gerbera',
|
||||||
|
'hydrangea',
|
||||||
|
'ivy',
|
||||||
|
'laurel',
|
||||||
|
'lilium',
|
||||||
|
'mastic',
|
||||||
|
'medeola',
|
||||||
|
'monstera',
|
||||||
|
'moss',
|
||||||
|
'oak',
|
||||||
|
'orchid',
|
||||||
|
'palmetto',
|
||||||
|
'paniculata',
|
||||||
|
'phormium',
|
||||||
|
'raphis',
|
||||||
|
'roebelini',
|
||||||
|
'rose',
|
||||||
|
'ruscus',
|
||||||
|
'salal',
|
||||||
|
'tulip'
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = Version;
|
||||||
|
|
||||||
|
if (require.main === module)
|
||||||
|
new MyVC().run(Version);
|
|
@ -1,4 +1,5 @@
|
||||||
versionSchema: myvc
|
versionSchema: myvc
|
||||||
|
versionDigits: 5
|
||||||
schemas:
|
schemas:
|
||||||
- myvc
|
- myvc
|
||||||
fixtures:
|
fixtures:
|
||||||
|
|
25
myvc.js
25
myvc.js
|
@ -57,6 +57,7 @@ class MyVC {
|
||||||
'init',
|
'init',
|
||||||
'pull',
|
'pull',
|
||||||
'push',
|
'push',
|
||||||
|
'version',
|
||||||
'dump',
|
'dump',
|
||||||
'start',
|
'start',
|
||||||
'run'
|
'run'
|
||||||
|
@ -69,13 +70,16 @@ class MyVC {
|
||||||
command = new Klass();
|
command = new Klass();
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandOpts = getopts(argv, command.myOpts);
|
const commandOpts = getopts(argv, command.localOpts);
|
||||||
Object.assign(cliOpts, commandOpts);
|
Object.assign(cliOpts, commandOpts);
|
||||||
|
|
||||||
for (const opt in cliOpts) {
|
for (const opt in cliOpts)
|
||||||
if (opt.length > 1 || opt == '_')
|
if (opt.length > 1 || opt == '_')
|
||||||
opts[opt] = cliOpts[opt];
|
opts[opt] = cliOpts[opt];
|
||||||
}
|
|
||||||
|
const operandToOpt = command.localOpts.operand;
|
||||||
|
if (opts._.length >= 2 && operandToOpt)
|
||||||
|
opts[operandToOpt] = opts._[1];
|
||||||
|
|
||||||
parameter('Workspace:', opts.workspace);
|
parameter('Workspace:', opts.workspace);
|
||||||
parameter('Remote:', opts.remote || 'local');
|
parameter('Remote:', opts.remote || 'local');
|
||||||
|
@ -155,6 +159,11 @@ class MyVC {
|
||||||
this.opts = opts;
|
this.opts = opts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async unload() {
|
||||||
|
if (this.conn)
|
||||||
|
await this.conn.end();
|
||||||
|
}
|
||||||
|
|
||||||
async dbConnect() {
|
async dbConnect() {
|
||||||
if (!this.conn)
|
if (!this.conn)
|
||||||
this.conn = await this.createConnection();
|
this.conn = await this.createConnection();
|
||||||
|
@ -165,11 +174,6 @@ class MyVC {
|
||||||
return await mysql.createConnection(this.opts.dbConfig);
|
return await mysql.createConnection(this.opts.dbConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
async unload() {
|
|
||||||
if (this.conn)
|
|
||||||
await this.conn.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchDbVersion() {
|
async fetchDbVersion() {
|
||||||
const {opts} = this;
|
const {opts} = this;
|
||||||
|
|
||||||
|
@ -201,6 +205,7 @@ class MyVC {
|
||||||
const changesMap = new Map();
|
const changesMap = new Map();
|
||||||
|
|
||||||
async function pushChanges(diff) {
|
async function pushChanges(diff) {
|
||||||
|
if (!diff) return;
|
||||||
const patches = await diff.patches();
|
const patches = await diff.patches();
|
||||||
|
|
||||||
for (const patch of patches) {
|
for (const patch of patches) {
|
||||||
|
@ -230,9 +235,7 @@ class MyVC {
|
||||||
}
|
}
|
||||||
|
|
||||||
await pushChanges(await this.getUnstaged(repo));
|
await pushChanges(await this.getUnstaged(repo));
|
||||||
|
await pushChanges(await this.getStaged(repo));
|
||||||
const stagedDiff = await this.getStaged(repo);
|
|
||||||
if (stagedDiff) await pushChanges(stagedDiff);
|
|
||||||
|
|
||||||
return changes.sort((a, b) => {
|
return changes.sort((a, b) => {
|
||||||
if (b.mark != a.mark)
|
if (b.mark != a.mark)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "myvc",
|
"name": "myvc",
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"author": "Verdnatura Levante SL",
|
"author": "Verdnatura Levante SL",
|
||||||
"description": "MySQL Version Control",
|
"description": "MySQL Version Control",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
|
|
||||||
CREATE TABLE `version` (
|
CREATE TABLE `version` (
|
||||||
`code` varchar(255) NOT NULL,
|
`code` VARCHAR(255) NOT NULL,
|
||||||
`number` char(11) NULL DEFAULT NULL,
|
`number` CHAR(11) NULL DEFAULT NULL,
|
||||||
`gitCommit` varchar(255) NULL DEFAULT NULL,
|
`gitCommit` VARCHAR(255) NULL DEFAULT NULL,
|
||||||
`updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
`updated` DATETIME NOT NULL DEFAULT NULL
|
||||||
) ENGINE=InnoDB;
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
ALTER TABLE `version`
|
ALTER TABLE `version`
|
||||||
ADD PRIMARY KEY (`code`);
|
ADD PRIMARY KEY (`code`);
|
||||||
|
|
||||||
CREATE TABLE `versionUser` (
|
CREATE TABLE `versionUser` (
|
||||||
`code` varchar(255) NOT NULL,
|
`code` VARCHAR(255) NOT NULL,
|
||||||
`user` varchar(255) NOT NULL,
|
`user` VARCHAR(255) NOT NULL,
|
||||||
`number` char(11) NULL DEFAULT NULL,
|
`number` CHAR(11) NULL DEFAULT NULL,
|
||||||
`gitCommit` varchar(255) NULL DEFAULT NULL,
|
`gitCommit` VARCHAR(255) NULL DEFAULT NULL,
|
||||||
`updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
`updated` DATETIME NOT NULL DEFAULT NULL,
|
||||||
|
`lastNumber` CHAR(11) NULL DEFAULT NULL,
|
||||||
) ENGINE=InnoDB;
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
ALTER TABLE `versionUser`
|
ALTER TABLE `versionUser`
|
||||||
|
|
Loading…
Reference in New Issue