diff --git a/pgmanage/app/include/OmniDatabase/MariaDB.py b/pgmanage/app/include/OmniDatabase/MariaDB.py index 5d0745d76..135694c6b 100644 --- a/pgmanage/app/include/OmniDatabase/MariaDB.py +++ b/pgmanage/app/include/OmniDatabase/MariaDB.py @@ -1296,3 +1296,6 @@ def GetDDL(self, p_schema, p_table, p_object, p_type): def GetAutocompleteValues(self, p_columns, p_filter): return None + + def QueryTableDefinition(self, table=None): + return self.v_connection.Query("SHOW FULL COLUMNS FROM {0}".format(table), True) \ No newline at end of file diff --git a/pgmanage/app/include/OmniDatabase/MySQL.py b/pgmanage/app/include/OmniDatabase/MySQL.py index 8532292fb..16e978ce3 100644 --- a/pgmanage/app/include/OmniDatabase/MySQL.py +++ b/pgmanage/app/include/OmniDatabase/MySQL.py @@ -1244,3 +1244,6 @@ def GetDDL(self, p_schema, p_table, p_object, p_type): def GetAutocompleteValues(self, p_columns, p_filter): return None + + def QueryTableDefinition(self, table=None): + return self.v_connection.Query("SHOW FULL COLUMNS FROM {0}".format(table), True) diff --git a/pgmanage/app/include/Spartacus/Database.py b/pgmanage/app/include/Spartacus/Database.py index 6e71db000..3dd683768 100644 --- a/pgmanage/app/include/Spartacus/Database.py +++ b/pgmanage/app/include/Spartacus/Database.py @@ -54,6 +54,7 @@ pass try: import pymysql + from pymysql.constants import CLIENT v_supported_rdbms.append('MySQL') v_supported_rdbms.append('MariaDB') except ImportError: @@ -1947,6 +1948,7 @@ def Open(self, p_autocommit=True): password=self.v_password, autocommit=p_autocommit, read_default_file='~/.my.cnf', + client_flag=CLIENT.MULTI_STATEMENTS, **self.connection_params ) self.v_cur = self.v_con.cursor() @@ -2332,6 +2334,7 @@ def Open(self, p_autocommit=True): password=self.v_password, autocommit=p_autocommit, read_default_file='~/.my.cnf', + client_flag=CLIENT.MULTI_STATEMENTS, **self.connection_params, ) self.v_cur = self.v_con.cursor() diff --git a/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/SchemaEditorTab.vue b/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/SchemaEditorTab.vue index d1c201180..23eb7423e 100644 --- a/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/SchemaEditorTab.vue +++ b/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/SchemaEditorTab.vue @@ -61,6 +61,25 @@ import axios from 'axios' import { showToast } from '../notification_control' import { settingsStore } from '../stores/stores_initializer' + +function formatDefaultValue(defaultValue, dataType, table) { + if (!defaultValue) return null + if (defaultValue.trim().toLowerCase() == 'null') return null + + let textTypesMap = ['CHAR', 'VARCHAR', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT', + 'TEXT', 'CHARACTER', 'NCHAR', 'NVARCHAR', + 'CHARACTER VARYING', + ] + + if (textTypesMap.includes(dataType.toUpperCase())) { + const stringValue = defaultValue.toString() + return stringValue + } + + // If no conversion matches, return raw value + return table.client.raw(defaultValue); +} + export default { name: "SchemaEditor", props: { @@ -252,17 +271,20 @@ export default { if(changes.drops.length) table.dropColumns(changes.drops) changes.typeChanges.forEach((coldef) => { - if(coldef.dataType === 'autoincrement') { + if (coldef.dataType === 'autoincrement') { table.increments(coldef.name).alter() - }else { - table.specificType(coldef.name, coldef.dataType).defaultTo(coldef.defaultValue).alter({alterNullable : false}) + } else { + let formattedDefault = formatDefaultValue(coldef.defaultValue, coldef.dataType, table) + table.specificType(coldef.name, coldef.dataType).defaultTo(formattedDefault).alter({ alterNullable: false }) + coldef.skipDefaults = true } }) - changes.defaults.forEach(function(coldef) { - if (coldef.defaultValue !== '') { - table.specificType(coldef.name, coldef.dataType).alter().defaultTo(table.client.raw(coldef.defaultValue)).alter({alterNullable : false, alterType: false}) - } + changes.defaults.forEach(function (coldef) { + if (!!coldef?.skipDefaults) return + let formattedDefault = formatDefaultValue(coldef.defaultValue, coldef.dataType, table) + table.specificType(coldef.name, coldef.dataType).alter().defaultTo(formattedDefault).alter({ alterNullable: false, alterType: false }) + // FIXME: this does not work, figure out how to do drop default via Knex. // else { // table.specificType(coldef.name, coldef.dataType).defaultTo().alter({alterNullable : false, alterType: false}) @@ -270,7 +292,11 @@ export default { }) changes.nullableChanges.forEach((coldef) => { - coldef.nullable ? table.setNullable(coldef.name) : table.dropNullable(coldef.name) + if (table.client.dialect === "mysql") { + coldef.nullable ? table.setNullable(coldef) : table.dropNullable(coldef) + } else { + coldef.nullable ? table.setNullable(coldef.name) : table.dropNullable(coldef.name) + } }) // FIXME: commenting generates drop default - how to avoid this? @@ -295,7 +321,7 @@ export default { tabledef.columns.forEach((coldef) => { // use Knex's magic to create a proper auto-incrementing column in database-agnostic way let col = coldef.dataType === 'autoincrement' ? - table.increments(coldef.name) : + table.increments(coldef.name, {primaryKey: false}) : table.specificType(coldef.name, coldef.dataType) coldef.nullable ? col.nullable() : col.notNullable() diff --git a/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/TreeMariaDB.vue b/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/TreeMariaDB.vue index dcbdc33fd..37205b96c 100644 --- a/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/TreeMariaDB.vue +++ b/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/TreeMariaDB.vue @@ -117,13 +117,7 @@ export default { label: "Create Table", icon: "fas cm-all fa-edit", onClick: () => { - tabSQLTemplate( - "Create Table", - this.templates.create_table.replace( - "#schema_name#", - this.getParentNode(this.selectedNode).title - ) - ); + tabsStore.createSchemaEditorTab(this.selectedNode, "create", "mysql") }, }, ], @@ -191,17 +185,10 @@ export default { icon: "fas cm-all fa-list", children: [ { - label: "Alter Table (SQL)", + label: "Alter Table", icon: "fas cm-all fa-edit", onClick: () => { - tabSQLTemplate( - "Alter Table", - this.templates.alter_table.replace( - "#table_name#", - `${this.getParentNodeDeep(this.selectedNode, 2).title}.${this.selectedNode.title - }` - ) - ); + tabsStore.createSchemaEditorTab(this.selectedNode, "alter", "mysql") }, }, { @@ -796,30 +783,35 @@ export default { icon: "fas node-all fa-cog node-procedure-list", type: "procedure_list", contextMenu: "cm_procedures", + database: node.data.database }); this.insertNode(node, "Functions", { icon: "fas node-all fa-cog node-function-list", type: "function_list", contextMenu: "cm_functions", + database: node.data.database }); this.insertNode(node, "Views", { icon: "fas node-all fa-eye node-view-list", type: "view_list", contextMenu: "cm_views", + database: node.data.database }); this.insertNode(node, "Sequences", { icon: "fas node-all fa-sort-numeric-down node-sequence-list", type: "sequence_list", contextMenu: "cm_sequences", + database: node.data.database }); this.insertNode(node, "Tables", { icon: "fas node-all fa-th node-table-list", type: "table_list", contextMenu: "cm_tables", + database: node.data.database }); }, getTablesMariadb(node) { @@ -839,6 +831,7 @@ export default { icon: "fas node-all fa-table node-table", type: "table", contextMenu: "cm_table", + database: node.data.database }); }, null); }) @@ -859,29 +852,34 @@ export default { icon: "fas node-all fa-thumbtack node-index", type: "indexes", contextMenu: "cm_indexes", + database: node.data.database }); this.insertNode(node, "Uniques", { icon: "fas node-all fa-key node-unique", type: "uniques", contextMenu: "cm_uniques", + database: node.data.database }); this.insertNode(node, "Foreign Keys", { icon: "fas node-all fa-key node-fkey", type: "foreign_keys", contextMenu: "cm_fks", + database: node.data.database }); this.insertNode(node, "Primary Key", { icon: "fas node-all fa-key node-pkey", type: "primary_key", contextMenu: "cm_pks", + database: node.data.database }); this.insertNode(node, `Columns (${resp.data.length})`, { icon: "fas node-all fa-columns node-column", type: "column_list", contextMenu: "cm_columns", + database: node.data.database }); const columns_node = this.getFirstChildNode(node); @@ -890,6 +888,7 @@ export default { icon: "fas node-all fa-columns node-column", type: "table_field", contextMenu: "cm_column", + database: node.data.database }); const table_field = this.getFirstChildNode(columns_node); @@ -931,6 +930,7 @@ export default { icon: "fas node-all fa-key node-pkey", type: "pk", contextMenu: "cm_pk", + database: node.data.database }); }); }) @@ -982,6 +982,7 @@ export default { icon: "fas node-all fa-key node-fkey", type: "foreign_key", contextMenu: "cm_fk", + database: node.data.database }); }, null); }) @@ -1055,6 +1056,7 @@ export default { icon: "fas node-all fa-key node-unique", type: "unique", contextMenu: "cm_unique", + database: node.data.database }); }); }) @@ -1104,6 +1106,7 @@ export default { type: "index", contextMenu: "cm_index", uniqueness: el.uniqueness, + database: node.data.database }); }); }) @@ -1155,6 +1158,7 @@ export default { icon: "fas node-all fa-sort-numeric-down node-sequence", type: "sequence", contextMenu: "cm_sequence", + database: node.data.database }, true ); @@ -1180,6 +1184,7 @@ export default { icon: "fas node-all fa-eye node-view", type: "view", contextMenu: "cm_view", + database: node.data.database }); }); }) @@ -1254,6 +1259,7 @@ export default { type: "function", id: el.id, contextMenu: "cm_function", + database: node.data.database }); }); }) @@ -1335,6 +1341,7 @@ export default { type: "procedure", contextMenu: "cm_procedure", id: el.id, + database: node.data.database }); }); }) diff --git a/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/TreeMysql.vue b/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/TreeMysql.vue index 0038e84f7..2c32cff45 100644 --- a/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/TreeMysql.vue +++ b/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/TreeMysql.vue @@ -118,13 +118,7 @@ export default { label: "Create Table", icon: "fas cm-all fa-edit", onClick: () => { - tabSQLTemplate( - "Create Table", - this.templates.create_table.replace( - "#schema_name#", - this.getParentNode(this.selectedNode).title - ) - ); + tabsStore.createSchemaEditorTab(this.selectedNode, "create", "mysql") }, }, ], @@ -192,17 +186,10 @@ export default { icon: "fas cm-all fa-list", children: [ { - label: "Alter Table (SQL)", + label: "Alter Table", icon: "fas cm-all fa-edit", onClick: () => { - tabSQLTemplate( - "Alter Table", - this.templates.alter_table.replace( - "#table_name#", - `${this.getParentNodeDeep(this.selectedNode, 2).title}.${this.selectedNode.title - }` - ) - ); + tabsStore.createSchemaEditorTab(this.selectedNode, "alter", "mysql") }, }, { @@ -744,24 +731,28 @@ export default { icon: "fas node-all fa-cog node-procedure-list", type: "procedure_list", contextMenu: "cm_procedures", + database: node.data.database }); this.insertNode(node, "Functions", { icon: "fas node-all fa-cog node-function-list", type: "function_list", contextMenu: "cm_functions", + database: node.data.database }); this.insertNode(node, "Views", { icon: "fas node-all fa-eye node-view-list", type: "view_list", contextMenu: "cm_views", + database: node.data.database }); this.insertNode(node, "Tables", { icon: "fas node-all fa-th node-table-list", type: "table_list", contextMenu: "cm_tables", + database: node.data.database }); }, getTablesMysql(node) { @@ -781,6 +772,7 @@ export default { icon: "fas node-all fa-table node-table", type: "table", contextMenu: "cm_table", + database: node.data.database }); }, null); }) @@ -801,29 +793,34 @@ export default { icon: "fas node-all fa-thumbtack node-index", type: "indexes", contextMenu: "cm_indexes", + database: node.data.database }); this.insertNode(node, "Uniques", { icon: "fas node-all fa-key node-unique", type: "uniques", contextMenu: "cm_uniques", + database: node.data.database }); this.insertNode(node, "Foreign Keys", { icon: "fas node-all fa-key node-fkey", type: "foreign_keys", contextMenu: "cm_fks", + database: node.data.database }); this.insertNode(node, "Primary Key", { icon: "fas node-all fa-key node-pkey", type: "primary_key", contextMenu: "cm_pks", + database: node.data.database }); this.insertNode(node, `Columns (${resp.data.length})`, { icon: "fas node-all fa-columns node-column", type: "column_list", contextMenu: "cm_columns", + database: node.data.database }); const columns_node = this.getFirstChildNode(node); @@ -833,6 +830,7 @@ export default { icon: "fas node-all fa-columns node-column", type: "table_field", contextMenu: "cm_column", + database: node.data.database }); const table_field = this.getFirstChildNode(columns_node); @@ -876,6 +874,7 @@ export default { icon: "fas node-all fa-key node-pkey", type: "pk", contextMenu: "cm_pk", + database: node.data.database }); }); }) @@ -926,6 +925,7 @@ export default { icon: "fas node-all fa-key node-fkey", type: "foreign_key", contextMenu: "cm_fk", + database: node.data.database }); }, null); }) @@ -999,6 +999,7 @@ export default { icon: "fas node-all fa-key node-unique", type: "unique", contextMenu: "cm_unique", + database: node.data.database }); }); }) @@ -1050,6 +1051,7 @@ export default { type: "index", contextMenu: "cm_index", uniqueness: el.uniqueness, + database: node.data.database }); }); }) @@ -1099,6 +1101,7 @@ export default { icon: "fas node-all fa-eye node-view", type: "view", contextMenu: "cm_view", + database: node.data.database }); }); }) @@ -1173,6 +1176,7 @@ export default { type: "function", id: el.id, contextMenu: "cm_function", + database: node.data.database }); }); }) @@ -1254,6 +1258,7 @@ export default { type: "procedure", contextMenu: "cm_procedure", id: el.id, + database: node.data.database }); }); }) diff --git a/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/dialect-data.js b/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/dialect-data.js index c58755286..94dae1be6 100644 --- a/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/dialect-data.js +++ b/pgmanage/app/static/assets/js/pgmanage_frontend/src/components/dialect-data.js @@ -1,6 +1,8 @@ import TableCompiler_SQLite3 from 'knex/lib/dialects/sqlite3/schema/sqlite-tablecompiler' +import TableCompiler_MySQL from 'knex/lib/dialects/mysql/schema/mysql-tablecompiler' import { identity, flatten } from 'lodash' +// TODO: Mysql - add PrimaryKey and Comment handling in alter table export default Object.freeze({ 'postgres': { @@ -48,5 +50,46 @@ export default Object.freeze({ }, ], }, + 'mysql': { + dataTypes: [ + 'bit', 'int', 'int unsigned', 'integer', 'integer unsigned', 'tinyint', 'tinyint unsigned', + 'smallint', 'smallint unsigned', 'mediumint', 'mediumint unsigned', 'bigint', 'bigint unsigned', + 'float', 'double', 'double precision', 'dec', 'decimal', 'numeric', 'fixed', 'bool', 'boolean', 'date', 'datetime', 'timestamp', 'time', 'year', 'char', 'nchar', 'national char', 'varchar', 'nvarchar', 'national varchar', 'text', 'tinytext', 'mediumtext', 'blob', 'longtext', 'tinyblob', 'mediumblob', 'longblob', 'enum', 'set', 'json', 'binary', 'varbinary', 'geometry', 'point', 'linestring', 'polygon', 'multipoint', 'multilinestring', 'multipolygon', 'geometrycollection', + ], + hasSchema: false, + hasComments: true, + formatterDialect: 'mysql', + api_endpoints: { + table_definition_url: "/get_table_definition_mysql/" + }, + overrides: [ + () => { + TableCompiler_MySQL.prototype._setNullableState = function ( + column, + isNullable + ) { + const nullability = isNullable ? "NULL" : "NOT NULL"; + const columnType = column.dataType; + const sql = `alter table ${this.tableName()} modify ${this.formatter.wrap( + column.name + )} ${columnType} ${nullability}`; + return this.pushQuery({ + sql: sql, + }); + }; + }, + () => { + TableCompiler_MySQL.prototype.renameColumn = function (from, to) { + const table = this.tableName(); + const wrapped = + this.formatter.wrap(from) + " TO " + this.formatter.wrap(to); + const sql = `ALTER TABLE ${table} RENAME COLUMN ${wrapped};`; + this.pushQuery({ + sql: sql, + }); + }; + }, + ], + }, }); \ No newline at end of file diff --git a/pgmanage/app/static/assets/js/pgmanage_frontend/src/stores/tabs.js b/pgmanage/app/static/assets/js/pgmanage_frontend/src/stores/tabs.js index 0cc16d7ce..74ec12fa7 100644 --- a/pgmanage/app/static/assets/js/pgmanage_frontend/src/stores/tabs.js +++ b/pgmanage/app/static/assets/js/pgmanage_frontend/src/stores/tabs.js @@ -650,7 +650,11 @@ const useTabsStore = defineStore("tabs", { this.selectTab(tab); }, createSchemaEditorTab(node, mode, dialect) { - let tableName = node.title.replace(/^"(.*)"$/, "$1"); + let tableName = + dialect === "mysql" + ? `${node.data.database}.${node.title}` + : node.title.replace(/^"(.*)"$/, "$1"); + let tabTitle = mode === "alter" ? `Alter: ${tableName}` : "New Table"; const tab = this.addTab({ @@ -672,8 +676,11 @@ const useTabsStore = defineStore("tabs", { tab.metaData.treeNode = node; tab.metaData.databaseIndex = this.selectedPrimaryTab?.metaData?.selectedDatabaseIndex; + tab.metaData.databaseName = - this.selectedPrimaryTab?.metaData?.selectedDatabase; + dialect === "mysql" + ? node.data.database + : this.selectedPrimaryTab?.metaData?.selectedDatabase; this.selectTab(tab); }, diff --git a/pgmanage/app/urls.py b/pgmanage/app/urls.py index 035ff998a..d2bf56d56 100644 --- a/pgmanage/app/urls.py +++ b/pgmanage/app/urls.py @@ -238,6 +238,7 @@ re_path(r'^template_select_mysql/', views.tree_mysql.template_select, name='template_select'), re_path(r'^template_insert_mysql/', views.tree_mysql.template_insert, name='template_insert'), re_path(r'^template_update_mysql/', views.tree_mysql.template_update, name='template_update'), + re_path(r'^get_table_definition_mysql/', views.tree_mysql.get_table_definition, name='get_table_definition'), #TREE_MARIADB re_path(r'^get_tree_info_mariadb/', views.tree_mariadb.get_tree_info, name='get_tree_info'), diff --git a/pgmanage/app/views/tree_mysql.py b/pgmanage/app/views/tree_mysql.py index 1e4cb247a..19ef7a478 100644 --- a/pgmanage/app/views/tree_mysql.py +++ b/pgmanage/app/views/tree_mysql.py @@ -525,3 +525,28 @@ def template_update(request, v_database): v_return["v_data"] = {"v_template": v_template} return JsonResponse(v_return) + + +@user_authenticated +@database_required_new(check_timeout=False, open_connection=True) +def get_table_definition(request, database): + data = request.data + table = data["table"] + + columns = [] + try: + q_definition = database.QueryTableDefinition(table) + for col in q_definition.Rows: + column_data = { + "name": col["Field"], + "data_type": col["Type"], + "default_value": col["Default"], + "nullable": col["Null"] == "YES", + "is_primary": bool(col["Key"]), + "comment": col["Comment"], + } + columns.append(column_data) + except Exception as exc: + return JsonResponse(data={"data": str(exc)}, status=400) + + return JsonResponse(data={"data": columns})