quick-erd
Version:
quick and easy text-based ERD + code generator for migration, query, typescript types and orm entity
341 lines (340 loc) • 10.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseCreateTable = parseCreateTable;
const string_1 = require("../utils/string");
function parseCreateTable(sql) {
const startIdx = sql.indexOf('(');
const endIdx = sql.lastIndexOf(') ENGINE=');
sql = sql.slice(startIdx + 1, endIdx).trim();
const field_list = [];
const primary_key_set = new Set();
const unique_key_set = new Set();
const foreign_key_map = new Map();
for (;;) {
const field = parseStatement(sql);
sql = field.rest;
if (field.is_skip === false) {
if (field.is_primary_key === true) {
primary_key_set.add(field.name);
}
else if (field.is_unique_key) {
unique_key_set.add(field.fields);
}
else if (field.is_foreign_key === true) {
foreign_key_map.set(field.field, {
type: '>0-',
table: field.ref_table,
field: field.ref_field,
});
}
else {
field_list.push({
name: field.name,
type: field.type,
is_primary_key: field.is_primary_key,
is_unsigned: field.unsigned,
is_zerofill: false,
is_null: !field.not_null,
is_unique: false,
references: undefined,
default_value: field.default_value,
collate: field.collate,
});
}
}
if (sql.startsWith(',')) {
sql = sql.slice(1).trim();
continue;
}
if (!sql) {
break;
}
throw new Error(`unknown tokens: ${JSON.stringify(sql)}`);
}
for (const name of primary_key_set) {
const field = field_list.find(field => field.name === name);
if (field) {
field.is_primary_key = true;
}
}
for (const unique_fields of unique_key_set) {
for (const field of field_list) {
if (unique_fields.includes(field.name)) {
field.is_unique = true;
}
}
}
for (const [name, ref] of foreign_key_map.entries()) {
const field = field_list.find(field => field.name === name);
if (field) {
field.references = ref;
}
}
return field_list;
}
function parseDefaultValue(sql) {
if (sql[0] === '"') {
const end = sql.indexOf('"', 1);
return sql.slice(0, end + 1);
}
if (sql[0] === "'") {
const end = sql.indexOf("'", 1);
return sql.slice(0, end + 1);
}
if (sql[0] === '`') {
const end = sql.indexOf('`', 1);
return sql.slice(0, end + 1);
}
const match = sql.match(/[\w-_()]+/);
if (!match)
throw new Error('failed to parse default value');
return match[0];
}
function nextPart(sql) {
const startIdx = sql.indexOf(' ');
const endIdx = (0, string_1.firstIndexOf)(sql, [' ', ','], startIdx + 1);
sql = sql.slice(endIdx).trim();
return sql;
}
function nextStatement(sql) {
const idx = sql.indexOf(',');
if (idx === -1) {
return '';
}
return sql.slice(idx).trim();
}
function parseStatement(sql) {
/* check named key */
const is_skip = sql.startsWith('KEY ');
if (is_skip) {
sql = nextStatement(sql);
return { is_skip, rest: sql };
}
/* parse primary key */
const is_primary_key = sql.startsWith('PRIMARY KEY');
if (is_primary_key) {
return parsePrimaryKeyStatement(sql);
}
/* parse unique key */
const is_unique_key = sql.startsWith('UNIQUE KEY');
if (is_unique_key) {
return parseUniqueKeyStatement(sql);
}
/* parse foreign key constraint */
const is_constraint = sql.startsWith('CONSTRAINT');
if (is_constraint) {
return parseConstraintStatement(sql);
}
return parseColumnStatement(sql);
}
function parseColumnStatement(sql) {
/* parse field name */
const result = parseName(sql);
const name = result.name;
sql = result.rest;
/* parse field type */
let endIdx = (0, string_1.firstIndexOf)(sql, [' ', ',']);
if (endIdx === -1)
endIdx = sql.length;
let type = sql.slice(0, endIdx);
type = toDataType(type);
sql = sql.slice(endIdx).trim();
/* parse unsigned */
const unsigned = sql.startsWith('unsigned');
if (unsigned) {
sql = sql.slice('unsigned'.length).trim();
}
let not_null = false;
let auto_inc = false;
let default_value = undefined;
let collate = undefined;
for (;;) {
/* parse text collation (encoding) */
if (sql.startsWith('COLLATE')) {
sql = sql.slice('COLLATE'.length).trim();
const match = sql.match(/^[\w-]+/);
if (match) {
collate = match[0];
sql = sql.slice(collate.length).trim();
}
continue;
}
/* parse not null */
if (sql.startsWith('NOT NULL')) {
not_null = true;
sql = sql.slice('NOT NULL'.length).trim();
continue;
}
/* parse null */
if (sql.startsWith('NULL')) {
not_null = false;
sql = sql.slice('NULL'.length).trim();
continue;
}
/* parse default value */
if (sql.startsWith('DEFAULT')) {
sql = sql.slice('DEFAULT'.length).trim();
default_value = parseDefaultValue(sql);
sql = sql.slice(default_value.length).trim();
continue;
}
/* parse on update */
if (sql.startsWith('ON UPDATE')) {
sql = sql.slice(3);
sql = nextPart(sql);
continue;
}
/* parse auto increment */
if (sql.startsWith('AUTO_INCREMENT')) {
auto_inc = true;
sql = sql.slice('AUTO_INCREMENT'.length).trim();
continue;
}
const has_comment = sql.startsWith('COMMENT');
if (has_comment) {
sql = parseComment(sql);
continue;
}
break;
}
return {
is_skip: false,
is_primary_key: false,
is_unique_key: false,
is_foreign_key: false,
name,
type,
unsigned,
not_null,
auto_inc,
default_value,
collate,
rest: sql,
};
}
function parsePrimaryKeyStatement(sql) {
sql = sql.slice('PRIMARY KEY'.length).trim();
const { name, rest } = parseNameInBracket(sql, 'PRIMARY KEY');
sql = rest;
if (sql && !sql.startsWith(',')) {
throw new Error(`unknown tokens after PRIMARY KEY: ${JSON.stringify(sql)}`);
}
return { is_skip: false, is_primary_key: true, name, rest: sql };
}
function parseUniqueKeyStatement(sql) {
sql = sql.slice('UNIQUE KEY'.length).trim();
/* parse unique key name */
let result = parseName(sql);
sql = result.rest.trim();
/* parse column names */
// TODO parse multiple columns
result = parseNameInBracket(sql, 'UNIQUE KEY');
sql = result.rest.replace(/using hash/i, '').trim();
if (sql && !sql.startsWith(',')) {
throw new Error(`unknown tokens after UNIQUE KEY: ${JSON.stringify(sql)}`);
}
const fields = [result.name];
return {
is_skip: false,
is_primary_key: false,
is_unique_key: true,
fields,
rest: sql,
};
}
function parseConstraintStatement(sql) {
sql = sql.slice('CONSTRAINT'.length).trim();
/* parse constrain name */
let result = parseName(sql);
sql = result.rest.trim();
/* parse constraint type */
const is_foreign_key = sql.startsWith('FOREIGN KEY');
if (!is_foreign_key) {
throw new Error('unknown type of constraint');
}
sql = sql.slice('FOREIGN KEY'.length).trim();
/* parse own field name */
result = parseNameInBracket(sql, 'FOREIGN KEY own field');
const field = result.name;
sql = result.rest.trim();
/* parse 'REFERENCES' keyword */
const is_references = sql.startsWith('REFERENCES');
if (!is_references) {
throw new Error("missing 'REFERENCES' in foreign key constraint");
}
sql = sql.slice('REFERENCES'.length).trim();
/* parse reference table name */
result = parseName(sql);
const ref_table = result.name;
sql = result.rest.trim();
/* parse reference field name */
result = parseNameInBracket(sql, 'FOREIGN KEY reference field name');
const ref_field = result.name;
sql = result.rest.trim();
return {
is_skip: false,
is_primary_key: false,
is_unique_key: false,
is_foreign_key: true,
field,
ref_table,
ref_field,
rest: sql,
};
}
function parseComment(sql) {
sql = sql.slice('COMMENT'.length).trim();
if (!sql.startsWith("'")) {
throw new Error(`missing starting "'" for comment`);
}
sql = sql.slice(1);
for (; sql.length > 0;) {
if (sql.startsWith("''")) {
sql = sql.slice(2);
continue;
}
if (sql[0] === '\\') {
sql = sql.slice(2);
continue;
}
if (sql[0] === "'") {
sql = sql.slice(1);
return sql.trim();
}
sql = sql.slice(1);
}
throw new Error(`missing ending "'" for comment`);
}
function parseName(sql) {
const startIdx = sql.indexOf('`');
if (startIdx !== 0) {
const tokens = sql.slice(0, startIdx);
throw new Error(`unknown tokens: ${JSON.stringify(tokens)}`);
}
const endIdx = sql.indexOf('`', startIdx + 1);
const name = sql.slice(startIdx + 1, endIdx);
sql = sql.slice(endIdx + 1).trim();
return { name, rest: sql };
}
function parseNameInBracket(sql, context) {
if (!sql.startsWith('(')) {
throw new Error(`missing '(' for ${context}`);
}
sql = sql.slice(1);
const result = parseName(sql);
sql = result.rest;
if (!sql.startsWith(')')) {
throw new Error(`missing ')' for ${context}`);
}
sql = sql.slice(1);
return { name: result.name, rest: sql };
}
function toDataType(type) {
if (type.includes('character varying')) {
return 'string';
}
if (type.includes('timestamp')) {
return 'timestamp';
}
return type;
}