import varDatabaseDbHandler from "./varDatabaseDbHandler"; import generateColumnDescription from "./generateColumnDescription"; import handleTableForeignKey from "./handle-table-foreign-key"; import dropAllForeignKeys from "./drop-all-foreign-keys"; import createTableHandleTableRecord from "./create-table-handle-table-record"; import defaultFieldsRegexp from "../../functions/dsql/default-fields-regexp"; import handleIndexescreateDbFromSchema from "../createDbFromSchema/handle-indexes"; import _ from "lodash"; import { writeUpdatedDbSchema } from "../createDbFromSchema/grab-required-database-schemas"; import normalizeText from "../../utils/normalize-text"; /** * # Update table function */ export default async function updateTable({ dbFullName, tableName, tableFields, userId, dbSchema, tableIndexes, tableSchema, clone, recordedDbEntry, isMain, }) { /** * Initialize * ========================================== * @description Initial setup */ let errorLogs = []; /** * @description Initialize table info array. This value will be * changing depending on if a field is renamed or not. */ let upToDateTableFieldsArray = _.cloneDeep(tableFields); /** * @type {string[]} * @description Table update query string array */ const updateTableQueryArray = []; /** * @description Push the query initial value */ updateTableQueryArray.push(`ALTER TABLE \`${dbFullName}\`.\`${tableName}\``); /** * @description Grab Table Record */ if (!recordedDbEntry && !isMain) { throw new Error("Recorded Db entry not found!"); } let tableID = await createTableHandleTableRecord({ recordedDbEntry, tableSchema, update: true, isMain, }); if (!tableID && !isMain) { throw new Error("Recorded Table entry not found!"); } /** * Handle Table updates * * @description Try to undate table, catch error if anything goes wrong */ try { /** * Handle MYSQL Table Indexes * =================================================== * @description Iterate through each table index(if available) * and perform operations */ const allExistingIndexes = await varDatabaseDbHandler({ queryString: `SHOW INDEXES FROM \`${dbFullName}\`.\`${tableName}\` WHERE Index_comment LIKE '%schema_index%'`, }); for (let f = 0; f < allExistingIndexes.length; f++) { const { Key_name } = allExistingIndexes[f]; try { const existingKeyInSchema = tableIndexes === null || tableIndexes === void 0 ? void 0 : tableIndexes.find((indexObject) => indexObject.alias === Key_name); if (!existingKeyInSchema) throw new Error(`This Index(${Key_name}) Has been Deleted!`); } catch (error) { /** * @description Drop Index: This happens when the MYSQL index is not * present in the datasquirel DB schema */ await varDatabaseDbHandler({ queryString: `ALTER TABLE \`${dbFullName}\`.\`${tableName}\` DROP INDEX \`${Key_name}\``, }); } } /** * Handle DATASQUIREL Table Indexes * =================================================== * @description Iterate through each datasquirel schema * table index(if available), and perform operations */ if (tableIndexes && tableIndexes[0]) { handleIndexescreateDbFromSchema({ dbFullName, indexes: tableIndexes, tableName, }); } /** * Handle MYSQL Foreign Keys * =================================================== * @description Iterate through each datasquirel schema * table index(if available), and perform operations */ const allForeignKeys = await varDatabaseDbHandler({ queryString: `SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = '${dbFullName}' AND TABLE_NAME='${tableName}' AND CONSTRAINT_TYPE='FOREIGN KEY'`, }); if (allForeignKeys) { for (let c = 0; c < allForeignKeys.length; c++) { const { CONSTRAINT_NAME } = allForeignKeys[c]; /** * @description Skip if Key is the PRIMARY Key */ if (CONSTRAINT_NAME.match(/PRIMARY/)) continue; /** * @description Drop all foreign Keys to avoid MYSQL errors when adding/updating * Foreign keys */ const dropForeignKey = await varDatabaseDbHandler({ queryString: `ALTER TABLE \`${dbFullName}\`.\`${tableName}\` DROP FOREIGN KEY \`${CONSTRAINT_NAME}\``, }); } } /** * Handle MYSQL Unique Fields * =================================================== * @description Find all existing unique field constraints * and remove them */ const allUniqueConstraints = await varDatabaseDbHandler({ queryString: normalizeText(`SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS \ WHERE CONSTRAINT_SCHEMA = '${dbFullName}' AND TABLE_NAME='${tableName}' AND \ CONSTRAINT_TYPE='UNIQUE'`), }); if (allUniqueConstraints) { for (let c = 0; c < allUniqueConstraints.length; c++) { const { CONSTRAINT_NAME } = allUniqueConstraints[c]; const dropUniqueConstraint = await varDatabaseDbHandler({ queryString: `ALTER TABLE \`${dbFullName}\`.\`${tableName}\` DROP INDEX \`${CONSTRAINT_NAME}\``, }); } } /** * Handle MYSQL Columns (Fields) * =================================================== * @description Now handle all fields/columns */ let allExistingColumns = await varDatabaseDbHandler({ queryString: `SHOW COLUMNS FROM \`${dbFullName}\`.\`${tableName}\``, }); /** * @type {string[]} * @description Updated column names Array */ const updatedColumnsArray = []; /** * @description Iterate through every existing column */ for (let e = 0; e < allExistingColumns.length; e++) { const { Field } = allExistingColumns[e]; if (Field.match(defaultFieldsRegexp)) continue; /** * @description This finds out whether the fieldName corresponds with the MSQL Field name * if the fildName doesn't match any MYSQL Field name, the field is deleted. */ let existingEntry = upToDateTableFieldsArray.find((column) => column.fieldName === Field || column.originName === Field); if (existingEntry) { /** * @description Check if Field name has been updated */ if (existingEntry.updatedField && existingEntry.fieldName) { updatedColumnsArray.push(existingEntry.fieldName); const renameColumn = await varDatabaseDbHandler({ queryString: `ALTER TABLE \`${dbFullName}\`.\`${tableName}\` RENAME COLUMN \`${existingEntry.originName}\` TO \`${existingEntry.fieldName}\``, }); console.log(`Column Renamed from "${existingEntry.originName}" to "${existingEntry.fieldName}"`); /** * Update Db Schema * =================================================== * @description Update Db Schema after renaming column */ try { const updatedSchemaData = _.cloneDeep(dbSchema); const targetTableIndex = updatedSchemaData.tables.findIndex((table) => table.tableName === tableName); const targetFieldIndex = updatedSchemaData.tables[targetTableIndex].fields.findIndex((field) => field.fieldName === existingEntry.fieldName); delete updatedSchemaData.tables[targetTableIndex] .fields[targetFieldIndex]["originName"]; delete updatedSchemaData.tables[targetTableIndex] .fields[targetFieldIndex]["updatedField"]; /** * @description Set New Table Fields Array */ upToDateTableFieldsArray = updatedSchemaData.tables[targetTableIndex].fields; if (userId) { writeUpdatedDbSchema({ dbSchema: updatedSchemaData, userId, }); } allExistingColumns = await varDatabaseDbHandler({ queryString: `SHOW COLUMNS FROM \`${dbFullName}\`.\`${tableName}\``, }); } catch (error) { console.log("Update table error =>", error.message); } //////////////////////////////////////// } //////////////////////////////////////// continue; //////////////////////////////////////// } else { await varDatabaseDbHandler({ queryString: `ALTER TABLE \`${dbFullName}\`.\`${tableName}\` DROP COLUMN \`${Field}\``, }); } } /** * Handle DATASQUIREL schema fields for current table * =================================================== * @description Iterate through each field object and * perform operations */ for (let i = 0; i < upToDateTableFieldsArray.length; i++) { const column = upToDateTableFieldsArray[i]; // const prevColumn = upToDateTableFieldsArray[i - 1]; // const nextColumn = upToDateTableFieldsArray[i + 1]; const { fieldName, dataType, foreignKey } = column; if (!fieldName) continue; if (defaultFieldsRegexp.test(fieldName)) continue; let updateText = ""; const existingColumnIndex = allExistingColumns === null || allExistingColumns === void 0 ? void 0 : allExistingColumns.findIndex((_column, _index) => _column.Field === fieldName); const existingColumn = existingColumnIndex >= 0 ? allExistingColumns[existingColumnIndex] : undefined; let { fieldEntryText } = generateColumnDescription({ columnData: column, }); /** * @description Modify Column(Field) if it already exists * in MYSQL database */ if (existingColumn === null || existingColumn === void 0 ? void 0 : existingColumn.Field) { const { Field, Type } = existingColumn; updateText += `MODIFY COLUMN ${fieldEntryText}`; // if ( // Field === fieldName && // dataType?.toUpperCase() === Type.toUpperCase() // ) { // } else { // updateText += `MODIFY COLUMN ${fieldEntryText}`; // } } else { /** * @description Append new column to the end of existing columns */ updateText += `ADD COLUMN ${fieldEntryText}`; } /** * @description Pust SQL code snippet to updateTableQueryArray Array * Add a comma(,) to separate from the next snippet */ if (updateText.match(/./)) { updateTableQueryArray.push(updateText + ","); } } /** * @description Construct final SQL query by combning all SQL snippets in * updateTableQueryArray Arry, and trimming the final comma(,) */ const updateTableQuery = updateTableQueryArray .filter((q) => Boolean(q.match(/./))) .join(" ") .replace(/,$/, ""); //////////////////////////////////////// /** * @description Check if SQL snippets array has more than 1 entries * This is because 1 entry means "ALTER TABLE table_name" only, without any * Alter directives like "ADD COLUMN" or "MODIFY COLUMN" */ if (updateTableQueryArray.length > 1) { const updateTable = await varDatabaseDbHandler({ queryString: updateTableQuery, }); /** * # Handle Foreign Keys */ await dropAllForeignKeys({ dbFullName, tableName }); for (let i = 0; i < upToDateTableFieldsArray.length; i++) { const { fieldName, foreignKey } = upToDateTableFieldsArray[i]; if (!clone && foreignKey && fieldName) { await handleTableForeignKey({ dbFullName, errorLogs, foreignKey, fieldName, tableName, }); } } } else { /** * @description If only 1 SQL snippet is left in updateTableQueryArray, this * means that no updates have been made to the table */ } return tableID; } catch (error) { console.log('Error in "updateTable" shell function =>', error.message); return tableID; } }