sql-ddl-to-json-schema
Version:
Parse and convert SQL DDL statements to a JSON Schema.
752 lines (751 loc) • 25.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Table = void 0;
const utils_1 = require("../../../../shared/utils");
const table_options_1 = require("./table-options");
const column_1 = require("./column");
const fulltext_index_1 = require("./fulltext-index");
const spatial_index_1 = require("./spatial-index");
const foreign_key_1 = require("./foreign-key");
const unique_key_1 = require("./unique-key");
const primary_key_1 = require("./primary-key");
const _1 = require(".");
/**
* Class to represent a table as parsed from SQL.
*/
class Table {
database;
name;
columns;
options;
fulltextIndexes;
spatialIndexes;
foreignKeys;
uniqueKeys;
indexes;
primaryKey;
/**
* Creates a table from a JSON def.
*
* @param json JSON format parsed from SQL.
* @param database Database to assign table to.
*/
static fromCommonDef(json, database) {
if (json.id === 'P_CREATE_TABLE_COMMON') {
const def = json.def;
const table = new Table();
table.database = database;
table.name = def.table;
if (def.tableOptions) {
table.options = table_options_1.TableOptions.fromDef(def.tableOptions);
}
const createDefinitions = def.columnsDef.def;
createDefinitions.forEach((createDefinition) => {
/**
* If table create definition is about adding a column.
*/
if ((0, utils_1.isDefined)(createDefinition.def.column)) {
const column = column_1.Column.fromDef(createDefinition);
table.addColumn(column);
}
else if ((0, utils_1.isDefined)(createDefinition.def.fulltextIndex)) {
/**
* If table create definition is about adding a fulltext index.
*/
table.pushFulltextIndex(fulltext_index_1.FulltextIndex.fromDef(createDefinition));
}
else if ((0, utils_1.isDefined)(createDefinition.def.spatialIndex)) {
/**
* If table create definition is about adding a spatial index.
*/
table.pushSpatialIndex(spatial_index_1.SpatialIndex.fromDef(createDefinition));
}
else if ((0, utils_1.isDefined)(createDefinition.def.foreignKey)) {
/**
* If table create definition is about adding a foreign key.
*/
table.pushForeignKey(foreign_key_1.ForeignKey.fromDef(createDefinition));
}
else if ((0, utils_1.isDefined)(createDefinition.def.uniqueKey)) {
/**
* If table create definition is about adding an unique key.
*/
table.pushUniqueKey(unique_key_1.UniqueKey.fromDef(createDefinition));
}
else if ((0, utils_1.isDefined)(createDefinition.def.primaryKey)) {
/**
* If table create definition is about adding a primary key.
*/
table.setPrimaryKey(primary_key_1.PrimaryKey.fromDef(createDefinition));
}
else if ((0, utils_1.isDefined)(createDefinition.def.index)) {
/**
* If table create definition is about adding an index.
*/
table.pushIndex(_1.Index.fromDef(createDefinition));
}
});
return table;
}
throw new TypeError(`Unknown json id to build table from: ${json.id}`);
}
/**
* Creates a table from a JSON def.
*
* @param json JSON format parsed from SQL.
* @param tables Already existing tables.
*/
static fromAlikeDef(json, tables = []) {
if (json.id === 'P_CREATE_TABLE_LIKE') {
const def = json.def;
const alikeTable = tables.find((t) => t.name === def.like);
if (!alikeTable) {
// throw new Error(`Trying to "CREATE TABLE LIKE" unexisting table ${def.like}.`);
return undefined;
}
const table = alikeTable.clone();
table.name = def.table;
return table;
}
throw new TypeError(`Unknown json id to build table from: ${json.id}`);
}
/**
* JSON casting of this object calls this method.
*/
toJSON() {
const json = {
name: this.name,
columns: (this.columns ?? []).map((c) => c.toJSON()),
};
if ((0, utils_1.isDefined)(this.primaryKey)) {
json.primaryKey = this.primaryKey.toJSON();
}
if ((0, utils_1.isDefined)(this.foreignKeys) && this.foreignKeys.length) {
json.foreignKeys = this.foreignKeys.map((k) => k.toJSON());
}
if ((0, utils_1.isDefined)(this.uniqueKeys) && this.uniqueKeys.length) {
json.uniqueKeys = this.uniqueKeys.map((k) => k.toJSON());
}
if ((0, utils_1.isDefined)(this.indexes) && this.indexes.length) {
json.indexes = this.indexes.map((i) => i.toJSON());
}
if ((0, utils_1.isDefined)(this.spatialIndexes) && this.spatialIndexes.length) {
json.spatialIndexes = this.spatialIndexes.map((i) => i.toJSON());
}
if ((0, utils_1.isDefined)(this.fulltextIndexes) && this.fulltextIndexes.length) {
json.fulltextIndexes = this.fulltextIndexes.map((i) => i.toJSON());
}
if ((0, utils_1.isDefined)(this.options)) {
json.options = this.options.toJSON();
}
return json;
}
/**
* Create a deep clone of this model.
*/
clone() {
const table = new Table();
table.database = this.database;
table.name = this.name;
table.columns = (this.columns ?? []).map((c) => c.clone());
if ((0, utils_1.isDefined)(this.options)) {
table.options = this.options.clone();
}
if ((0, utils_1.isDefined)(this.primaryKey)) {
table.primaryKey = this.primaryKey.clone();
}
if ((0, utils_1.isDefined)(this.uniqueKeys) && this.uniqueKeys.length) {
table.uniqueKeys = this.uniqueKeys.map((key) => key.clone());
}
if ((0, utils_1.isDefined)(this.foreignKeys) && this.foreignKeys.length) {
table.foreignKeys = this.foreignKeys.map((key) => key.clone());
}
if ((0, utils_1.isDefined)(this.fulltextIndexes) && this.fulltextIndexes.length) {
table.fulltextIndexes = this.fulltextIndexes.map((index) => index.clone());
}
if ((0, utils_1.isDefined)(this.spatialIndexes) && this.spatialIndexes.length) {
table.spatialIndexes = this.spatialIndexes.map((index) => index.clone());
}
if ((0, utils_1.isDefined)(this.indexes) && this.indexes.length) {
table.indexes = this.indexes.map((index) => index.clone());
}
return table;
}
/**
* Get table with given name.
*
* @param name Table name.
*/
getTable(name) {
return this.database.getTable(name);
}
/**
* Get tables from database.
*/
getTables() {
return this.database.getTables();
}
/**
* Setter for database.
*
* @param database Database instance.
*/
setDatabase(database) {
this.database = database;
}
/**
* Rename table.
*
* @param newName New table name.
*/
renameTo(newName) {
this.database.tables.forEach((t) => {
(t.foreignKeys ?? [])
.filter((k) => k.referencesTable(this))
.forEach((k) => k.updateReferencedTableName(newName));
});
this.name = newName;
}
/**
* Add a column to columns array, in a given position.
*
* @param column Column to be added.
* @param position Position object.
*/
addColumn(column, position) {
/**
* Should not add column with same name.
*/
if (this.getColumn(column.name)) {
return;
}
/**
* Validate if there are any other autoincrement
* columns, as there should be only one.
*/
if (column.options &&
column.options.autoincrement &&
(this.columns ?? []).some((c) => c.options && c.options.autoincrement)) {
return;
}
/**
* Do not allow adding column with primary
* key if table already has primary key.
*/
if (this.primaryKey && column.options && column.options.primary) {
return;
}
if (!(0, utils_1.isArray)(this.columns)) {
this.columns = [];
}
if (!(0, utils_1.isDefined)(position)) {
this.columns.push(column);
}
else if (!position.after) {
this.columns.unshift(column);
}
else {
const refColumn = this.columns.find((c) => c.name === position.after);
if (!refColumn) {
return;
}
const pos = this.columns.indexOf(refColumn);
const end = this.columns.splice(pos + 1);
this.columns.push(column);
this.columns = this.columns.concat(end);
}
this.extractColumnKeys(column);
}
/**
* Extract column keys like PrimaryKey, ForeignKey,
* UniqueKey and add them to this table instance.
*
* @param column Column to be extracted.
*/
extractColumnKeys(column) {
const primaryKey = column.extractPrimaryKey();
const foreignKey = column.extractForeignKey();
const uniqueKey = column.extractUniqueKey();
if (primaryKey) {
this.setPrimaryKey(primaryKey);
}
if (foreignKey) {
this.pushForeignKey(foreignKey);
}
if (uniqueKey) {
this.pushUniqueKey(uniqueKey);
}
}
/**
* Move a column to a given position. Returns whether operation was successful.
*
* @param column One of this table columns.
* @param position Position object.
*/
moveColumn(column, position) {
if (!(0, utils_1.isDefined)(this.columns) || !(0, utils_1.isDefined)(position)) {
return false;
}
if (!this.columns.includes(column)) {
return false;
}
let refColumn;
/**
* First of all, validate if 'after' column, if any, exists.
*/
if (position.after) {
refColumn = this.getColumn(position.after);
if (!refColumn) {
return false;
}
}
let pos = this.columns.indexOf(column);
let end = this.columns.splice(pos);
end.shift();
this.columns = this.columns.concat(end);
if (position.after) {
if (!refColumn) {
return false;
}
pos = this.columns.indexOf(refColumn);
end = this.columns.splice(pos + 1);
this.columns.push(column);
this.columns = this.columns.concat(end);
}
else {
this.columns.unshift(column);
}
return true;
}
/**
* Rename column and references to it. Returns whether operation was successful.
*
* @param column Column being renamed.
* @param newName New name of column.
*/
renameColumn(column, newName) {
if (!(this.columns ?? []).includes(column)) {
return false;
}
/**
* Rename references to column.
*/
this.getTables().forEach((table) => {
(table.foreignKeys ?? [])
.filter((k) => k.referencesTable(this))
.forEach((k) => k.renameColumn(column, newName));
});
(this.fulltextIndexes ?? []).forEach((i) => i.renameColumn(column, newName));
(this.spatialIndexes ?? []).forEach((i) => i.renameColumn(column, newName));
(this.indexes ?? []).forEach((i) => i.renameColumn(column, newName));
(this.uniqueKeys ?? []).forEach((k) => k.renameColumn(column, newName));
if (this.primaryKey) {
this.primaryKey.renameColumn(column, newName);
}
column.name = newName;
return true;
}
/**
* Get column position object.
*
* @param column Column.
*/
getColumnPosition(column) {
const index = (this.columns ?? []).indexOf(column);
/**
* First column.
*/
if (index === 0) {
return { after: null };
}
/**
* Elsewhere.
*/
const refColumn = (this.columns ?? [])[index - 1];
return { after: refColumn.name };
}
/**
* Drops table's primary key.
*/
dropPrimaryKey() {
if (!this.primaryKey) {
return;
}
const tableColumns = this.primaryKey.getColumnsFromTable(this);
/**
* Should not drop primary key if pk column has autoincrement.
* https://github.com/duartealexf/sql-ddl-to-json-schema/issues/14
*/
if (tableColumns.some((c) => c.options && c.options.autoincrement)) {
return;
}
delete this.primaryKey;
}
/**
* Drops a column from table.
*
* @param column Column to be dropped.
*/
dropColumn(column) {
/**
* Validate whether there is a reference to given column.
* https://github.com/duartealexf/sql-ddl-to-json-schema/issues/12
*/
const hasReference = this.getTables().some((t) => (t.foreignKeys ?? []).some((k) => k.referencesTableAndColumn(this, column)));
if (hasReference) {
return;
}
if (!(0, utils_1.isDefined)(this.columns)) {
return;
}
/**
* Should not drop the last column of table.
* https://github.com/duartealexf/sql-ddl-to-json-schema/issues/13
*/
if (this.columns.length === 1) {
return;
}
const pos = this.columns.indexOf(column);
const end = this.columns.splice(pos);
end.shift();
this.columns = this.columns.concat(end);
/**
* Remove column from indexes. Also remove
* the index if removed column was last.
*
* https://github.com/duartealexf/sql-ddl-to-json-schema/issues/8
*/
if ((0, utils_1.isDefined)(this.fulltextIndexes) && this.fulltextIndexes.length) {
this.fulltextIndexes.forEach((index) => {
if (index.dropColumn(column.name) && !index.columns.length) {
this.dropIndexByInstance(index);
}
});
}
if ((0, utils_1.isDefined)(this.spatialIndexes) && this.spatialIndexes.length) {
this.spatialIndexes.forEach((index) => {
if (index.dropColumn(column.name) && !index.columns.length) {
this.dropIndexByInstance(index);
}
});
}
if ((0, utils_1.isDefined)(this.indexes) && this.indexes.length) {
this.indexes.forEach((index) => {
if (index.dropColumn(column.name) && !index.columns.length) {
this.dropIndexByInstance(index);
}
});
}
if ((0, utils_1.isDefined)(this.uniqueKeys) && this.uniqueKeys.length) {
this.uniqueKeys.forEach((key) => {
if (key.dropColumn(column.name) && !key.columns.length) {
this.dropIndexByInstance(key);
}
});
}
if ((0, utils_1.isDefined)(this.foreignKeys) && this.foreignKeys.length) {
this.foreignKeys.forEach((key) => {
if (key.dropColumn(column.name) && !key.columns.length) {
this.dropForeignKey(key);
}
});
}
if ((0, utils_1.isDefined)(this.primaryKey)) {
if (this.primaryKey.dropColumn(column.name) && !this.primaryKey.columns?.length) {
delete this.primaryKey;
}
}
}
/**
* Drops an index from table.
*
* @param index Index to be dropped.
*/
dropIndexByInstance(index) {
const type = this.getIndexTypeByInstance(index);
if (!(0, utils_1.isDefined)(type) || !(0, utils_1.isDefined)(this[type])) {
return;
}
const indexes = this[type];
const pos = indexes.indexOf(index);
const end = indexes.splice(pos);
end.shift();
this[type] = indexes.concat(end);
}
/**
* Drops a foreign key from table.
*
* @param foreignKey Foreign key to be dropped.
*/
dropForeignKey(foreignKey) {
if (!(0, utils_1.isDefined)(this.foreignKeys)) {
return;
}
const pos = this.foreignKeys.indexOf(foreignKey);
const end = this.foreignKeys.splice(pos);
end.shift();
this.foreignKeys = this.foreignKeys.concat(end);
}
/**
* Get index by name.
*
* @param name Index name.
*/
getIndexByName(name) {
const type = this.getIndexTypeByName(name);
if (!type) {
// throw new Error(`Trying to reference an unexsisting index ${name} on table ${this.name}`);
return undefined;
}
const indexes = this[type];
if (!(0, utils_1.isArray)(indexes)) {
return undefined;
}
const result = indexes.find((index) => index.name === name);
return result ?? undefined;
}
/**
* Get which index array is storing a given index.
*
* @param indez
*/
getIndexTypeByInstance(index) {
const props = [
'uniqueKeys',
'indexes',
'fulltextIndexes',
'spatialIndexes',
];
const type = props.find((prop) => (this[prop] ?? []).some((i) => i === index));
return type;
}
/**
* Get which index array is storing a given index.
*
* @param indez
*/
getIndexTypeByName(name) {
const props = [
'uniqueKeys',
'indexes',
'fulltextIndexes',
'spatialIndexes',
];
const type = props.find((prop) => (this[prop] ?? []).some((i) => i.name === name));
return type;
}
/**
* Get column by name.
*
* @param name Column name.
*/
getColumn(name) {
return (this.columns ?? []).find((c) => c.name === name);
}
/**
* Get foreign key by name.
*
* @param name Foreign key name.
*/
getForeignKey(name) {
return (this.foreignKeys ?? []).find((k) => k.name === name);
}
/**
* Whether there is a foreign key with given name in table.
*
* @param name Foreign key name.
*/
hasForeignKey(name) {
return (this.foreignKeys ?? []).some((k) => k.name === name);
}
/**
* Setter for table's primary key.
*
* @param primaryKey Primary key.
*/
setPrimaryKey(primaryKey) {
/**
* Should not add primary key over another one.
*/
if (this.primaryKey) {
return;
}
/**
* Validate columns referenced by primary key.
*/
if (!primaryKey.hasAllColumnsFromTable(this)) {
return;
}
/**
* Make necessary changes in columns.
*/
(primaryKey.columns ?? []).forEach((indexCol) => {
if (!indexCol.column) {
return;
}
const column = this.getColumn(indexCol.column);
if (!column || !column.options) {
return;
}
column.options.nullable = false;
});
this.primaryKey = primaryKey;
}
/**
* Push a fulltext index to fulltextIndexes array.
*
* @param fulltextIndex Index to be pushed.
*/
pushFulltextIndex(fulltextIndex) {
/**
* Should not add index or key with same name.
* https://github.com/duartealexf/sql-ddl-to-json-schema/issues/15
*/
if (fulltextIndex.name && this.getIndexByName(fulltextIndex.name)) {
return;
}
/**
* Validate columns referenced by fulltext index.
*/
if (!fulltextIndex.hasAllColumnsFromTable(this)) {
return;
}
if (!(0, utils_1.isDefined)(this.fulltextIndexes)) {
this.fulltextIndexes = [];
}
this.fulltextIndexes.push(fulltextIndex);
}
/**
* Push a spatial index to spatialIndexes array.
*
* @param spatialIndex Index to be pushed.
*/
pushSpatialIndex(spatialIndex) {
/**
* Should not add index or key with same name.
* https://github.com/duartealexf/sql-ddl-to-json-schema/issues/15
*/
if (spatialIndex.name && this.getIndexByName(spatialIndex.name)) {
return;
}
/**
* Validate columns referenced by spatial index.
*/
if (!spatialIndex.hasAllColumnsFromTable(this)) {
return;
}
if (!(0, utils_1.isDefined)(this.spatialIndexes)) {
this.spatialIndexes = [];
}
this.spatialIndexes.push(spatialIndex);
}
/**
* Push an unique key to uniqueKeys array.
*
* @param uniqueKey UniqueKey to be pushed.
*/
pushUniqueKey(uniqueKey) {
/**
* Should not add index or key with same name.
* https://github.com/duartealexf/sql-ddl-to-json-schema/issues/15
*/
if (uniqueKey.name && this.getIndexByName(uniqueKey.name)) {
return;
}
/**
* Validate columns referenced by unique key.
*/
if (!uniqueKey.hasAllColumnsFromTable(this)) {
return;
}
/**
* If index column length is not set, set it to full column size.
*
* "If no length is specified, the whole column will be indexed."
* https://mariadb.com/kb/en/library/create-table/#index-types
*/
uniqueKey.setIndexSizeFromTable(this);
if (!(0, utils_1.isDefined)(this.uniqueKeys)) {
this.uniqueKeys = [];
}
this.uniqueKeys.push(uniqueKey);
}
/**
* Push a foreign key to foreignKeys array.
*
* @param foreignKey ForeignKey to be pushed.
*/
pushForeignKey(foreignKey) {
/**
* Should not add index or key with same name.
* https://github.com/duartealexf/sql-ddl-to-json-schema/issues/15
*/
if (foreignKey.name && this.getIndexByName(foreignKey.name)) {
return;
}
/**
* Validate if referenced table exists.
*
* UPDATE:
* Since DDLs can run with FOREIGN_KEY_CHECKS disabled, this has been disabled.
* @see https://github.com/duartealexf/sql-ddl-to-json-schema/issues/27
* ~ duartealexf
*/
// const referencedTable = foreignKey.getReferencedTable(this.getTables());
// if (!referencedTable) { return; }
/**
* Validate columns.
*
* UPDATE:
* Since DDLs can run with FOREIGN_KEY_CHECKS disabled, this has been disabled.
* @see https://github.com/duartealexf/sql-ddl-to-json-schema/issues/27
* ~ duartealexf
*/
// const hasAllColumnsFromThisTable = foreignKey.hasAllColumnsFromTable(this);
// const hasAllColumnsFromReference = foreignKey.hasAllColumnsFromRefTable(referencedTable);
// if (!hasAllColumnsFromThisTable || !hasAllColumnsFromReference) { return; }
/**
* If index column length is not set, set it to full column size.
*
* "If no length is specified, the whole column will be indexed."
* https://mariadb.com/kb/en/library/create-table/#index-types
*/
foreignKey.setIndexSizeFromTable(this);
if (!(0, utils_1.isDefined)(this.foreignKeys)) {
this.foreignKeys = [];
}
this.foreignKeys.push(foreignKey);
}
/**
* Push an index to indexes array.
*
* @param index Index to be pushed.
*/
pushIndex(index) {
/**
* Should not add index or key with same name.
* https://github.com/duartealexf/sql-ddl-to-json-schema/issues/15
*/
if (index.name && this.getIndexByName(index.name)) {
return;
}
/**
* Validate columns referenced by index.
*/
if (!index.hasAllColumnsFromTable(this)) {
return;
}
/**
* If index column length is not set, set it to full column size.
*
* "If no length is specified, the whole column will be indexed."
* https://mariadb.com/kb/en/library/create-table/#index-types
*/
index.setIndexSizeFromTable(this);
if (!(0, utils_1.isDefined)(this.indexes)) {
this.indexes = [];
}
this.indexes.push(index);
}
}
exports.Table = Table;