UNPKG

quick-erd

Version:

quick and easy text-based ERD + code generator for migration, query, typescript types and orm entity

297 lines (296 loc) 10.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseTableSchema = parseTableSchema; exports.parseCreateTable = parseCreateTable; const enum_1 = require("../core/enum"); function parseTableSchema(rows) { const table_rows = rows.filter(row => row.type === 'table' && !/^sqlite_stat\d+$/.test(row.name)); const index_rows = rows.filter(row => row.type === 'index'); // remove full-text-search extension fts5 internal tables for (let i = 0; i < table_rows.length; i++) { let table = table_rows[i]; let sql = table.sql.toLowerCase(); if (!(sql.includes('create virtual table') && sql.includes('using fts5'))) { continue; } let name = table.name; let names = [ `${name}_data`, `${name}_idx`, `${name}_content`, `${name}_docsize`, `${name}_config`, ]; for (let name of names) { let index = table_rows.findIndex(table => table.name == name); if (index != -1) { table_rows.splice(index, 1); } } } const table_list = []; table_rows.forEach(row => { const field_list = parseCreateTable(row.sql); if (!field_list) { throw new Error('Failed to parse table: ' + row.sql); } let is_virtual = row.sql.toLowerCase().includes('create virtual table'); let table = { name: row.name, field_list: field_list }; if (is_virtual) { table.is_virtual = true; } table_list.push(table); }); for (const index_row of index_rows) { if (!index_row.sql) continue; const index = parseCreateIndex(index_row.sql); const table = table_list.find(table => table.name === index?.table); const field = table?.field_list.find(field => field.name === index?.field); if (index?.is_unique && field) { field.is_unique = true; } } return table_list; } function parseCreateTable(sql) { const start = sql.indexOf('('); if (start === -1) return null; const end = sql.lastIndexOf(')'); if (end === -1) return null; sql = sql.substring(start + 1, end); const field_dict = {}; parseCreateColumns(sql).forEach(part => { let rest = part.trim(); let lower = rest.toLowerCase(); if (lower.startsWith('primary key')) { const [name] = parseNameInBracket(rest); const field = field_dict[name]; if (field) { field.is_primary_key = true; } return; } if (lower.startsWith('foreign key')) { const [fieldName, rest1] = parseNameInBracket(rest); rest = rest1.trim(); lower = rest.toLowerCase(); if (!lower.startsWith('references')) { return; } rest = rest.substring('references'.length).trim(); const [refTable, rest2] = parseName(rest); rest = rest2.trim(); const [refField] = parseNameInBracket(rest); const field = field_dict[fieldName]; if (!field) return; field.references = { table: refTable, field: refField, type: '>0-' }; return; } let is_null = true; rest = ' ' + rest + ' '; lower = rest.toLowerCase(); if (lower.includes(' not null ')) { is_null = false; const [before, after] = split(rest, lower, ' not null '); rest = before + ' ' + after; } else if (lower.includes(' null ')) { const [before, after] = split(rest, lower, ' null '); rest = before + ' ' + after; } let is_unique = false; if (lower.includes(' unique ')) { is_unique = true; const [before, after] = split(rest, lower, ' unique '); rest = before + ' ' + after; } rest = rest.trim(); const [name, rest1] = parseName(rest); rest = rest1.trim(); lower = rest.toLowerCase(); let type; if (lower.startsWith('references') || lower.startsWith('primary key')) { type = ''; } else { const [type2, rest2] = parseName(rest); type = type2; rest = rest2.trim(); const num = rest.match(/^(\d+\))/); if (num) { type += '(' + num[1]; rest = rest.slice(num[1].length); } lower = rest.toLowerCase(); } let is_primary_key = false; if (lower.includes('primary key')) { const start = lower.indexOf('primary key'); const end = start + 'primary key'.length; const before = rest.substring(0, start); const after = rest.substring(end); rest = (before + after).trim(); lower = rest.toLowerCase(); is_primary_key = true; } let references = undefined; if (lower.includes('references')) { const start = lower.indexOf('references'); const end = start + 'references'.length; const before = rest.substring(0, start); let after = rest.substring(end).trim(); const [table, rest3] = parseName(after); after = rest3.trim(); const [field, rest4] = parseNameInBracket(after); after = rest4.trim(); references = { table, field, type: '>0-' }; rest = before + after; lower = rest.toLowerCase(); } let default_value = undefined; if (lower.includes('default ')) { const start = lower.indexOf('default '); const end = start + 'default '.length; const before = rest.substring(0, start); const after = rest.substring(end).trim(); default_value = parseDefaultValue(after); if (default_value) { rest = before + after.slice(default_value.length); lower = rest.toLowerCase(); } } if (lower.match(/check.*in.*/)) { let match = rest.match(/check(.*)/i)?.[1].trim() || ''; if (match.startsWith('(') && match.endsWith(')')) { match = match.slice(1, match.length - 1); } const matches = match.match(/"?`?(.*?)"?`?\s*in\s*\((.*)\)/i); if (matches?.[1] === name) { type = 'enum(' + matches[2] + ')'; type = (0, enum_1.formatEnum)(type); } } field_dict[name] = { name, type: type.startsWith('enum(') ? type : type.toLowerCase(), is_primary_key, is_null, is_unique, is_unsigned: false, default_value, references, }; }); return Object.values(field_dict); } function parseCreateColumns(sql) { const columns = []; let buffer = ''; let level = 0; sql.split('').forEach(c => { switch (c) { case '(': level++; buffer += c; break; case ')': level--; buffer += c; break; case ',': if (level === 0) { columns.push(buffer); buffer = ''; } else { buffer += c; } break; default: buffer += c; } }); columns.push(buffer); return columns.map(column => column.trim()).filter(column => column); } function parseCreateIndex(sql) { // example: CREATE UNIQUE INDEX `user_username_unique` on `user` (`username`) let match = sql.match(/create unique index .* on \`?(.*?)\`? \(\`?(.*?)\`?\)/i); // example: CREATE UNIQUE INDEX "user_username_unique" on "user" (\n "username"\n) if (!match) match = sql.match(/create unique index .* on "?(.*?)"? \("?([.|\s|\S]*?)"?\)/i); if (!match) return null; const table = match[1]; let field = match[2].trim(); if (field.startsWith('"') && field.endsWith('"')) { field = field.slice(1, -1); } if (field.includes(',')) { return null; } return { table, field, is_unique: true }; } function firstIndexOf(string, patterns, offset = 0) { const index_list = patterns .map(pattern => string.indexOf(pattern, offset)) .filter(index => index !== -1) .sort((a, b) => a - b); return index_list.length === 0 ? -1 : index_list[0]; } 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); } return sql.match(/[\w-_()]+/)?.[0]; } function parseName(sql) { let start; let end; if (sql[0] === '"') { start = 1; end = sql.indexOf('"', 1); } else if (sql[0] === '`') { start = 1; end = sql.indexOf('`', 1); } else { start = 0; end = firstIndexOf(sql, [' ', '(']); } if (end === -1) { end = sql.length; } const name = sql.substring(start, end); const rest = sql.substring(end + 1); return [name, rest]; } function parseNameInBracket(sql) { const start = sql.indexOf('('); const end = sql.indexOf(')', start); const middle = sql.substring(start + 1, end).trim(); const after = sql.substring(end + 1); const [name] = parseName(middle); return [name, after]; } function split(sql, lower, separator) { const start = lower.indexOf(separator); const end = start + separator.length; const before = sql.substring(0, start); const after = sql.substring(end); return [before, after]; }