schemax
Version:
SQL database schema extractor for Sqlite3, Mysql and PostgreSQL
154 lines (127 loc) • 4.7 kB
JavaScript
;
const mysql = require("mysql2/promise");
module.exports = {
conn: null,
async query (q, params) {
const [rows, fields] = await this.conn.query(q, params);
return rows;
},
async extract (options) {
const schemaOptions = JSON.parse(JSON.stringify(options));
schemaOptions.database = "information_schema";
delete schemaOptions.adapter; //mysql2 doesn't like unused props.
this.conn = await mysql.createConnection(schemaOptions);
await this.conn.connect();
const schema = {
vendor: "mysql",
adapter: "mysql2",
database: options.database,
tableCount: 0,
tables: {},
};
//tables:
let q = "select TABLE_NAME, TABLE_COMMENT, ENGINE from TABLES where TABLE_SCHEMA = ?";
const tablesResult = await this.query(q, [options.database]);
tablesResult.forEach(row => {
schema.tables[row["TABLE_NAME"]] = {
name: row.TABLE_NAME,
engine: row.ENGINE,
pks: [],
columnCount: 0,
columns: {},
indexes: {},
foreignKeys: {},
};
if (row.TABLE_COMMENT !== '')
schema.tables[row["TABLE_NAME"]].comment = row.TABLE_COMMENT;
});
//columns:
q = "select * from COLUMNS where TABLE_SCHEMA = ?";
const columnRows = await this.query(q, [options.database]);
columnRows.forEach(cr => {
const table = schema.tables[cr.TABLE_NAME];
table.columnCount++;
table.columns[cr.COLUMN_NAME] = {
name: cr.COLUMN_NAME,
position: cr.ORDINAL_POSITION,
default: cr.COLUMN_DEFAULT,
nullable : cr.IS_NULLABLE !== 'NO',
type: cr.COLUMN_TYPE,
};
//we created 2 columns that are unique together. we didn't set pk.
//mysql reports both columns as primary keys. don't know how to fix this.
if (cr.COLUMN_KEY === 'PRI') {
table.pks.push(cr.COLUMN_NAME);
table.columns[cr.COLUMN_NAME].isPK = true;
table.columns[cr.COLUMN_NAME].isAI = cr.EXTRA === 'auto_increment';
}
if (cr.CHARACTER_MAXIMUM_LENGTH)
table.columns[cr.COLUMN_NAME].lengthInChars = cr.CHARACTER_MAXIMUM_LENGTH;
if (cr.CHARACTER_OCTET_LENGTH)
table.columns[cr.COLUMN_NAME].lengthInBytes = cr.CHARACTER_OCTET_LENGTH;
if (cr.COLUMN_COMMENT !== '')
table.columns[cr.COLUMN_NAME].comment = cr.COLUMN_COMMENT;
});
//constraints:
q = `select
tc.CONSTRAINT_NAME, tc.TABLE_NAME, tc.CONSTRAINT_TYPE,
rc.UPDATE_RULE, rc.DELETE_RULE, rc.REFERENCED_TABLE_NAME
from TABLE_CONSTRAINTS tc
left join REFERENTIAL_CONSTRAINTS rc
on rc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME and
rc.TABLE_NAME = tc.TABLE_NAME and
rc.CONSTRAINT_SCHEMA = ?
where tc.CONSTRAINT_SCHEMA = ?`;
const constraintRows = await this.query(q, [options.database, options.database]);
constraintRows.forEach(r => {
const table = schema.tables[r.TABLE_NAME];
table.indexes[r.CONSTRAINT_NAME] = {
name: r.CONSTRAINT_NAME,
type: r.CONSTRAINT_TYPE,
unique: r.CONSTRAINT_TYPE != 'FOREIGN KEY',
columns: [],
}
if (r.CONSTRAINT_TYPE === 'FOREIGN KEY') {
table.foreignKeys[r.CONSTRAINT_NAME] = {
toTable: r.REFERENCED_TABLE_NAME,
update: r.UPDATE_RULE,
delete : r.DELETE_RULE,
columns: [],
}
}
});
//key column usage:
q = `select * from KEY_COLUMN_USAGE where CONSTRAINT_SCHEMA = ?`;
const keyColRows = await this.query(q, [options.database]);
keyColRows.forEach(r => {
const table = schema.tables[r.TABLE_NAME];
if (r.REFERENCED_TABLE_NAME != null) {
table.foreignKeys[r.CONSTRAINT_NAME].columns.push({
name: r.COLUMN_NAME,
toColumn: r.REFERENCED_COLUMN_NAME,
});
}
if (table.indexes[r.CONSTRAINT_NAME]) {
table.indexes[r.CONSTRAINT_NAME].columns.push(r.COLUMN_NAME);
}
});
//other indices:
q = `SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, NON_UNIQUE
FROM STATISTICS WHERE TABLE_SCHEMA = ? AND INDEX_NAME != 'PRIMARY'`;
const otherIndices = await this.query(q, [options.database]);
for (const r of otherIndices) {
if (! schema.tables[r.TABLE_NAME].indexes[r.INDEX_NAME]) {
schema.tables[r.TABLE_NAME].indexes[r.INDEX_NAME] = {
name: r.INDEX_NAME,
type: 'INDEX',
unique: r.NON_UNIQUE == 0,
columns: [],
}
}
schema.tables[r.TABLE_NAME].indexes[r.INDEX_NAME].columns.push(r.COLUMN_NAME);
}
this.conn.end();
this.conn = null;
return schema;
}
}