diff --git a/sql/data_dictionary.py b/sql/data_dictionary.py index 6e5bd19cdc..401e17c8df 100644 --- a/sql/data_dictionary.py +++ b/sql/data_dictionary.py @@ -89,6 +89,176 @@ def table_info(request): ) +@permission_required("sql.menu_data_dictionary", raise_exception=True) +def view_list(request): + """数据字典获取视图列表(仅MySQL)""" + return _dict_list(request, db_type_required="mysql", engine_method="get_views_list") + + +@permission_required("sql.menu_data_dictionary", raise_exception=True) +def view_info(request): + """数据字典获取视图详情(仅MySQL)""" + return _dict_detail( + request, + db_type_required="mysql", + engine_method="get_view_detail", + name_param="view_name", + engine_kwarg="view_name", + ) + + +@permission_required("sql.menu_data_dictionary", raise_exception=True) +def trigger_list(request): + """数据字典获取触发器列表(仅MySQL)""" + return _dict_list( + request, db_type_required="mysql", engine_method="get_triggers_list" + ) + + +@permission_required("sql.menu_data_dictionary", raise_exception=True) +def trigger_info(request): + """数据字典获取触发器详情(仅MySQL)""" + return _dict_detail( + request, + db_type_required="mysql", + engine_method="get_trigger_detail", + name_param="trigger_name", + engine_kwarg="trigger_name", + ) + + +@permission_required("sql.menu_data_dictionary", raise_exception=True) +def procedure_list(request): + """数据字典获取存储过程列表(仅MySQL)""" + return _dict_list( + request, db_type_required="mysql", engine_method="get_procedures_list" + ) + + +@permission_required("sql.menu_data_dictionary", raise_exception=True) +def procedure_info(request): + """数据字典获取存储过程详情(仅MySQL)""" + return _dict_detail( + request, + db_type_required="mysql", + engine_method="get_procedure_detail", + name_param="proc_name", + engine_kwarg="proc_name", + ) + + +@permission_required("sql.menu_data_dictionary", raise_exception=True) +def function_list(request): + """数据字典获取函数列表(仅MySQL)""" + return _dict_list( + request, db_type_required="mysql", engine_method="get_functions_list" + ) + + +@permission_required("sql.menu_data_dictionary", raise_exception=True) +def function_info(request): + """数据字典获取函数详情(仅MySQL)""" + return _dict_detail( + request, + db_type_required="mysql", + engine_method="get_function_detail", + name_param="func_name", + engine_kwarg="func_name", + ) + + +@permission_required("sql.menu_data_dictionary", raise_exception=True) +def event_list(request): + """数据字典获取定时任务列表(仅MySQL)""" + return _dict_list( + request, db_type_required="mysql", engine_method="get_events_list" + ) + + +@permission_required("sql.menu_data_dictionary", raise_exception=True) +def event_info(request): + """数据字典获取定时任务详情(仅MySQL)""" + return _dict_detail( + request, + db_type_required="mysql", + engine_method="get_event_detail", + name_param="event_name", + engine_kwarg="event_name", + ) + + +def _dict_list(request, db_type_required, engine_method): + """通用数据字典对象列表接口""" + instance_name = request.GET.get("instance_name", "") + db_name = request.GET.get("db_name", "") + db_type = request.GET.get("db_type", "") + + if db_type_required and db_type != db_type_required: + res = {"status": 1, "msg": "仅MySQL支持该功能"} + return HttpResponse( + json.dumps(res, cls=ExtendJSONEncoder, bigint_as_string=True), + content_type="application/json", + ) + + if instance_name and db_name: + try: + instance = Instance.objects.get( + instance_name=instance_name, db_type=db_type + ) + query_engine = get_engine(instance=instance) + db_name = query_engine.escape_string(db_name) + data = getattr(query_engine, engine_method)(db_name=db_name) + res = {"status": 0, "data": data} + except Instance.DoesNotExist: + res = {"status": 1, "msg": "Instance.DoesNotExist"} + except Exception as e: + res = {"status": 1, "msg": str(e)} + else: + res = {"status": 1, "msg": "非法调用!"} + return HttpResponse( + json.dumps(res, cls=ExtendJSONEncoder, bigint_as_string=True), + content_type="application/json", + ) + + +def _dict_detail(request, db_type_required, engine_method, name_param, engine_kwarg): + """通用数据字典对象详情接口""" + instance_name = request.GET.get("instance_name", "") + db_name = request.GET.get("db_name", "") + obj_name = request.GET.get(name_param, "") + db_type = request.GET.get("db_type", "") + + if db_type_required and db_type != db_type_required: + res = {"status": 1, "msg": "仅MySQL支持该功能"} + return HttpResponse( + json.dumps(res, cls=ExtendJSONEncoder, bigint_as_string=True), + content_type="application/json", + ) + + if instance_name and db_name and obj_name: + try: + instance = Instance.objects.get( + instance_name=instance_name, db_type=db_type + ) + query_engine = get_engine(instance=instance) + db_name = query_engine.escape_string(db_name) + obj_name = query_engine.escape_string(obj_name) + data = getattr(query_engine, engine_method)( + **{"db_name": db_name, engine_kwarg: obj_name} + ) + res = {"status": 0, "data": data} + except Instance.DoesNotExist: + res = {"status": 1, "msg": "Instance.DoesNotExist"} + except Exception as e: + res = {"status": 1, "msg": str(e)} + else: + res = {"status": 1, "msg": "非法调用!"} + return HttpResponse( + json.dumps(res, cls=ExtendJSONEncoder, bigint_as_string=True), + content_type="application/json", + ) + + def get_export_full_path(base_dir: str, instance_name: str, db_name: str) -> str: """validate if the instance_name and db_name provided is secure""" fullpath = os.path.normpath( diff --git a/sql/engines/__init__.py b/sql/engines/__init__.py index 27f31b3c14..d2083ac049 100644 --- a/sql/engines/__init__.py +++ b/sql/engines/__init__.py @@ -139,6 +139,46 @@ def get_tables_metas_data(self, db_name, **kwargs): """获取数据库所有表格信息,用作数据字典导出接口""" return list() + def get_views_list(self, db_name, **kwargs): + """获取视图列表, 返回 dict""" + return dict() + + def get_view_detail(self, db_name, view_name, **kwargs): + """获取视图详情, 返回 dict""" + return dict() + + def get_triggers_list(self, db_name, **kwargs): + """获取触发器列表, 返回 dict""" + return dict() + + def get_trigger_detail(self, db_name, trigger_name, **kwargs): + """获取触发器详情, 返回 dict""" + return dict() + + def get_procedures_list(self, db_name, **kwargs): + """获取存储过程列表, 返回 dict""" + return dict() + + def get_procedure_detail(self, db_name, proc_name, **kwargs): + """获取存储过程详情, 返回 dict""" + return dict() + + def get_functions_list(self, db_name, **kwargs): + """获取函数列表, 返回 dict""" + return dict() + + def get_function_detail(self, db_name, func_name, **kwargs): + """获取函数详情, 返回 dict""" + return dict() + + def get_events_list(self, db_name, **kwargs): + """获取定时任务列表, 返回 dict""" + return dict() + + def get_event_detail(self, db_name, event_name, **kwargs): + """获取定时任务详情, 返回 dict""" + return dict() + def get_all_databases_summary(self): """实例数据库管理功能,获取实例所有的数据库描述信息""" return ResultSet() diff --git a/sql/engines/mysql.py b/sql/engines/mysql.py index d94c711ddc..f8a8da1ede 100644 --- a/sql/engines/mysql.py +++ b/sql/engines/mysql.py @@ -299,6 +299,244 @@ def get_table_index_data(self, db_name, tb_name, **kwargs): ) return {"column_list": _index_data.column_list, "rows": _index_data.rows} + def get_views_list(self, db_name, **kwargs): + """获取视图列表,按首字符分组""" + data = {} + sql = """SELECT TABLE_NAME, VIEW_DEFINITION + FROM information_schema.VIEWS + WHERE TABLE_SCHEMA=%(db_name)s;""" + result = self.query(db_name=db_name, sql=sql, parameters={"db_name": db_name}) + for row in result.rows: + view_name = row[0] + view_comment = row[1][:80] if row[1] else "" + if view_name[0] not in data: + data[view_name[0]] = list() + data[view_name[0]].append([view_name, view_comment]) + return data + + def get_view_detail(self, db_name, view_name, **kwargs): + """获取视图详情""" + sql = """SELECT + TABLE_NAME as view_name, + VIEW_DEFINITION as view_definition, + CHECK_OPTION as check_option, + IS_UPDATABLE as is_updatable, + DEFINER as definer, + SECURITY_TYPE as security_type, + CHARACTER_SET_CLIENT as character_set_client, + COLLATION_CONNECTION as collation_connection + FROM information_schema.VIEWS + WHERE TABLE_SCHEMA=%(db_name)s AND TABLE_NAME=%(view_name)s;""" + _meta = self.query( + db_name, sql, parameters={"db_name": db_name, "view_name": view_name} + ) + meta_data = { + "column_list": _meta.column_list, + "rows": _meta.rows[0] if _meta.rows else [], + } + view_definition = "" + if _meta.rows: + # VIEW_DEFINITION 在第二列 + view_definition = _meta.rows[0][1] or "" + desc = self.get_table_desc_data(db_name=db_name, tb_name=view_name) + return { + "meta_data": meta_data, + "desc": desc, + "view_definition": view_definition, + } + + def get_triggers_list(self, db_name, **kwargs): + """获取触发器列表,按首字符分组""" + data = {} + sql = """SELECT + TRIGGER_NAME, + ACTION_TIMING, + EVENT_MANIPULATION, + EVENT_OBJECT_TABLE + FROM information_schema.TRIGGERS + WHERE TRIGGER_SCHEMA=%(db_name)s;""" + result = self.query(db_name=db_name, sql=sql, parameters={"db_name": db_name}) + for row in result.rows: + trigger_name = row[0] + desc = f"{row[1]} {row[2]} ON {row[3]}" + if trigger_name[0] not in data: + data[trigger_name[0]] = list() + data[trigger_name[0]].append([trigger_name, desc]) + return data + + def get_trigger_detail(self, db_name, trigger_name, **kwargs): + """获取触发器详情""" + sql = """SELECT + TRIGGER_NAME as trigger_name, + ACTION_TIMING as action_timing, + EVENT_MANIPULATION as event_manipulation, + EVENT_OBJECT_TABLE as event_object_table, + ACTION_ORIENTATION as action_orientation, + ACTION_STATEMENT as action_statement, + DEFINER as definer, + CREATED as created, + SQL_MODE as sql_mode, + CHARACTER_SET_CLIENT as character_set_client, + COLLATION_CONNECTION as collation_connection + FROM information_schema.TRIGGERS + WHERE TRIGGER_SCHEMA=%(db_name)s AND TRIGGER_NAME=%(trigger_name)s;""" + _data = self.query( + db_name, + sql, + parameters={"db_name": db_name, "trigger_name": trigger_name}, + ) + return { + "column_list": _data.column_list, + "rows": _data.rows[0] if _data.rows else [], + } + + def get_procedures_list(self, db_name, **kwargs): + """获取存储过程列表,按首字符分组""" + data = {} + sql = """SELECT ROUTINE_NAME, ROUTINE_COMMENT + FROM information_schema.ROUTINES + WHERE ROUTINE_SCHEMA=%(db_name)s AND ROUTINE_TYPE='PROCEDURE';""" + result = self.query(db_name=db_name, sql=sql, parameters={"db_name": db_name}) + for row in result.rows: + proc_name = row[0] + proc_cmt = row[1] + if proc_name[0] not in data: + data[proc_name[0]] = list() + data[proc_name[0]].append([proc_name, proc_cmt]) + return data + + def get_procedure_detail(self, db_name, proc_name, **kwargs): + """获取存储过程详情""" + sql_meta = """SELECT + ROUTINE_NAME as routine_name, + ROUTINE_SCHEMA as routine_schema, + DEFINER as definer, + CREATED as created, + LAST_ALTERED as last_altered, + SQL_MODE as sql_mode, + SECURITY_TYPE as security_type, + ROUTINE_COMMENT as routine_comment + FROM information_schema.ROUTINES + WHERE ROUTINE_SCHEMA=%(db_name)s + AND ROUTINE_NAME=%(proc_name)s + AND ROUTINE_TYPE='PROCEDURE';""" + _meta = self.query( + db_name, + sql_meta, + parameters={"db_name": db_name, "proc_name": proc_name}, + ) + meta_data = { + "column_list": _meta.column_list, + "rows": _meta.rows[0] if _meta.rows else [], + } + _create = self.query(db_name, f"SHOW CREATE PROCEDURE `{proc_name}`;") + create_sql = _create.rows + return {"meta_data": meta_data, "create_sql": create_sql} + + def get_functions_list(self, db_name, **kwargs): + """获取函数列表,按首字符分组""" + data = {} + sql = """SELECT ROUTINE_NAME, ROUTINE_COMMENT + FROM information_schema.ROUTINES + WHERE ROUTINE_SCHEMA=%(db_name)s AND ROUTINE_TYPE='FUNCTION';""" + result = self.query(db_name=db_name, sql=sql, parameters={"db_name": db_name}) + for row in result.rows: + func_name = row[0] + func_cmt = row[1] + if func_name[0] not in data: + data[func_name[0]] = list() + data[func_name[0]].append([func_name, func_cmt]) + return data + + def get_function_detail(self, db_name, func_name, **kwargs): + """获取函数详情""" + sql_meta = """SELECT + ROUTINE_NAME as routine_name, + ROUTINE_SCHEMA as routine_schema, + DTD_IDENTIFIER as return_type, + DEFINER as definer, + CREATED as created, + LAST_ALTERED as last_altered, + SQL_MODE as sql_mode, + SECURITY_TYPE as security_type, + ROUTINE_COMMENT as routine_comment + FROM information_schema.ROUTINES + WHERE ROUTINE_SCHEMA=%(db_name)s + AND ROUTINE_NAME=%(func_name)s + AND ROUTINE_TYPE='FUNCTION';""" + _meta = self.query( + db_name, + sql_meta, + parameters={"db_name": db_name, "func_name": func_name}, + ) + meta_data = { + "column_list": _meta.column_list, + "rows": _meta.rows[0] if _meta.rows else [], + } + _create = self.query(db_name, f"SHOW CREATE FUNCTION `{func_name}`;") + create_sql = _create.rows + return {"meta_data": meta_data, "create_sql": create_sql} + + def get_events_list(self, db_name, **kwargs): + """获取定时任务列表,按首字符分组""" + data = {} + sql = """SELECT + EVENT_NAME, + STATUS, + EVENT_TYPE, + INTERVAL_VALUE, + INTERVAL_FIELD + FROM information_schema.EVENTS + WHERE EVENT_SCHEMA=%(db_name)s;""" + result = self.query(db_name=db_name, sql=sql, parameters={"db_name": db_name}) + for row in result.rows: + event_name = row[0] + status = row[1] + event_type = row[2] + interval_value = row[3] + interval_field = row[4] + if event_type == "RECURRING": + desc = f"{status} EVERY {interval_value} {interval_field}" + else: + desc = f"{status} ONE TIME" + if event_name[0] not in data: + data[event_name[0]] = list() + data[event_name[0]].append([event_name, desc]) + return data + + def get_event_detail(self, db_name, event_name, **kwargs): + """获取定时任务详情""" + sql_meta = """SELECT + EVENT_NAME as event_name, + EVENT_SCHEMA as event_schema, + DEFINER as definer, + EVENT_TYPE as event_type, + INTERVAL_VALUE as interval_value, + INTERVAL_FIELD as interval_field, + STATUS as status, + EXECUTE_AT as execute_at, + STARTS as starts, + ENDS as ends, + LAST_EXECUTED as last_executed, + ON_COMPLETION as on_completion, + CREATED as created, + LAST_ALTERED as last_altered, + EVENT_COMMENT as event_comment + FROM information_schema.EVENTS + WHERE EVENT_SCHEMA=%(db_name)s AND EVENT_NAME=%(event_name)s;""" + _meta = self.query( + db_name, + sql_meta, + parameters={"db_name": db_name, "event_name": event_name}, + ) + meta_data = { + "column_list": _meta.column_list, + "rows": _meta.rows[0] if _meta.rows else [], + } + _create = self.query(db_name, f"SHOW CREATE EVENT `{event_name}`;") + create_sql = _create.rows + return {"meta_data": meta_data, "create_sql": create_sql} + def get_tables_metas_data(self, db_name, **kwargs): """获取数据库所有表格信息,用作数据字典导出接口""" sql_tbs = f"SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=%(db_name)s ORDER BY TABLE_SCHEMA,TABLE_NAME;" diff --git a/sql/engines/test_mysql.py b/sql/engines/test_mysql.py index 9aa34506e3..4fd0b0f7de 100644 --- a/sql/engines/test_mysql.py +++ b/sql/engines/test_mysql.py @@ -188,6 +188,230 @@ def testDescribe(self, mock_query): new_engine.describe_table("some_db", "some_db") mock_query.assert_called_once() + @patch.object(MysqlEngine, "query") + def test_get_views_list(self, mock_query): + r = ResultSet() + r.rows = [("v1", "select 1"), ("v2", "select 2"), ("a_view", None)] + mock_query.return_value = r + engine = MysqlEngine(instance=self.ins1) + data = engine.get_views_list(db_name="some_db") + self.assertIn("v", data) + self.assertIn("a", data) + self.assertEqual(data["v"][0][0], "v1") + + @patch.object(MysqlEngine, "get_table_desc_data") + @patch.object(MysqlEngine, "query") + def test_get_view_detail(self, mock_query, mock_desc): + r = ResultSet() + r.column_list = [ + "view_name", + "view_definition", + "check_option", + "is_updatable", + "definer", + "security_type", + "character_set_client", + "collation_connection", + ] + r.rows = [ + ( + "v1", + "select 1", + "NONE", + "YES", + "root@%", + "DEFINER", + "utf8", + "utf8_general_ci", + ) + ] + mock_query.return_value = r + mock_desc.return_value = {"column_list": [], "rows": []} + engine = MysqlEngine(instance=self.ins1) + data = engine.get_view_detail(db_name="some_db", view_name="v1") + self.assertEqual(data["view_definition"], "select 1") + self.assertIn("meta_data", data) + self.assertIn("desc", data) + + @patch.object(MysqlEngine, "query") + def test_get_triggers_list(self, mock_query): + r = ResultSet() + r.rows = [("tg1", "BEFORE", "INSERT", "t1")] + mock_query.return_value = r + engine = MysqlEngine(instance=self.ins1) + data = engine.get_triggers_list(db_name="some_db") + self.assertIn("t", data) + self.assertEqual(data["t"][0][0], "tg1") + self.assertIn("BEFORE", data["t"][0][1]) + + @patch.object(MysqlEngine, "query") + def test_get_trigger_detail(self, mock_query): + r = ResultSet() + r.column_list = [ + "trigger_name", + "action_timing", + "event_manipulation", + "event_object_table", + "action_orientation", + "action_statement", + "definer", + "created", + "sql_mode", + "character_set_client", + "collation_connection", + ] + r.rows = [ + ( + "tg1", + "BEFORE", + "INSERT", + "t1", + "ROW", + "BEGIN END", + "root@%", + None, + "", + "utf8", + "utf8", + ) + ] + mock_query.return_value = r + engine = MysqlEngine(instance=self.ins1) + data = engine.get_trigger_detail(db_name="some_db", trigger_name="tg1") + self.assertIn("column_list", data) + self.assertTrue(len(data["rows"]) > 0) + + @patch.object(MysqlEngine, "query") + def test_get_procedures_list(self, mock_query): + r = ResultSet() + r.rows = [("p1", "comment1"), ("p2", "comment2")] + mock_query.return_value = r + engine = MysqlEngine(instance=self.ins1) + data = engine.get_procedures_list(db_name="some_db") + self.assertIn("p", data) + self.assertEqual(len(data["p"]), 2) + + @patch.object(MysqlEngine, "query") + def test_get_procedure_detail(self, mock_query): + meta = ResultSet() + meta.column_list = [ + "routine_name", + "routine_schema", + "definer", + "created", + "last_altered", + "sql_mode", + "security_type", + "routine_comment", + ] + meta.rows = [("p1", "some_db", "root@%", None, None, "", "DEFINER", "")] + create = ResultSet() + create.rows = [("p1", "", "CREATE PROCEDURE p1() BEGIN END", "")] + mock_query.side_effect = [meta, create] + engine = MysqlEngine(instance=self.ins1) + data = engine.get_procedure_detail(db_name="some_db", proc_name="p1") + self.assertIn("meta_data", data) + self.assertEqual(data["create_sql"], create.rows) + + @patch.object(MysqlEngine, "query") + def test_get_functions_list(self, mock_query): + r = ResultSet() + r.rows = [("f1", "cmt"), ("f2", "cmt2")] + mock_query.return_value = r + engine = MysqlEngine(instance=self.ins1) + data = engine.get_functions_list(db_name="some_db") + self.assertIn("f", data) + self.assertEqual(len(data["f"]), 2) + + @patch.object(MysqlEngine, "query") + def test_get_function_detail(self, mock_query): + meta = ResultSet() + meta.column_list = [ + "routine_name", + "routine_schema", + "return_type", + "definer", + "created", + "last_altered", + "sql_mode", + "security_type", + "routine_comment", + ] + meta.rows = [("f1", "some_db", "int", "root@%", None, None, "", "DEFINER", "")] + create = ResultSet() + create.rows = [("f1", "", "CREATE FUNCTION f1() RETURNS INT RETURN 1", "", "")] + mock_query.side_effect = [meta, create] + engine = MysqlEngine(instance=self.ins1) + data = engine.get_function_detail(db_name="some_db", func_name="f1") + self.assertIn("meta_data", data) + self.assertEqual(data["create_sql"], create.rows) + + @patch.object(MysqlEngine, "query") + def test_get_events_list(self, mock_query): + r = ResultSet() + r.rows = [("e1", "ENABLED", "RECURRING", 1, "DAY")] + mock_query.return_value = r + engine = MysqlEngine(instance=self.ins1) + data = engine.get_events_list(db_name="some_db") + self.assertIn("e", data) + self.assertIn("EVERY", data["e"][0][1]) + + @patch.object(MysqlEngine, "query") + def test_get_event_detail(self, mock_query): + meta = ResultSet() + meta.column_list = [ + "event_name", + "event_schema", + "definer", + "event_type", + "interval_value", + "interval_field", + "status", + "execute_at", + "starts", + "ends", + "last_executed", + "on_completion", + "created", + "last_altered", + "event_comment", + ] + meta.rows = [ + ( + "e1", + "some_db", + "root@%", + "RECURRING", + 1, + "DAY", + "ENABLED", + None, + None, + None, + None, + "NOT PRESERVE", + None, + None, + "", + ) + ] + create = ResultSet() + create.rows = [ + ( + "e1", + "", + "", + "CREATE EVENT e1 ON SCHEDULE EVERY 1 DAY DO SELECT 1", + "", + "", + ) + ] + mock_query.side_effect = [meta, create] + engine = MysqlEngine(instance=self.ins1) + data = engine.get_event_detail(db_name="some_db", event_name="e1") + self.assertIn("meta_data", data) + self.assertEqual(data["create_sql"], create.rows) + def testQueryCheck(self): new_engine = MysqlEngine(instance=self.ins1) sql_without_limit = "-- 测试\n select user from usertable" diff --git a/sql/templates/data_dictionary.html b/sql/templates/data_dictionary.html index 238d9b4875..ea9f22ffff 100644 --- a/sql/templates/data_dictionary.html +++ b/sql/templates/data_dictionary.html @@ -23,6 +23,18 @@ data-live-search="true"> +
+ +
{% if perms.sql.data_dictionary_export %}
+ + + + + @@ -410,6 +662,24 @@ //实例变动获取库 $("#instance_name").change(function () { $('#db_name').empty(); + // 根据 db_type 调整 obj_type 可选项 + var _ins_name = $("#instance_name").val(); + var _ind = instance_result.findIndex((value)=>value.instance_name==_ins_name); + var _db_type = _ind >= 0 ? instance_result[_ind]['db_type'] : ''; + if (_db_type === 'mysql') { + $('#obj_type option').prop('disabled', false); + } else { + // 非MySQL仅显示“表” + $('#obj_type option').each(function () { + if ($(this).val() === 'table') { + $(this).prop('disabled', false); + } else { + $(this).prop('disabled', true); + } + }); + $('#obj_type').val('table'); + } + $('#obj_type').selectpicker('refresh'); $.ajax({ type: "get", url: "/instance/instance_resource/", @@ -446,9 +716,40 @@ //库变动获取表 $("#db_name").change(function () { - get_table_list() + getObjectList(); }); + //对象类型变动重新加载 + $("#obj_type").change(function () { + getObjectList(); + }); + + // 获取当前选择的实例 db_type + function getCurrentDbType() { + var instance_name = $("#instance_name").val(); + if (!instance_name) return ''; + var ind = instance_result.findIndex((value)=>value.instance_name==instance_name); + if (ind < 0) return ''; + return instance_result[ind]['db_type']; + } + + // 统一对象列表入口 + function getObjectList() { + var obj_type = $('#obj_type').val() || 'table'; + var db_type = getCurrentDbType(); + // 非MySQL实例强制重置为 table + if (db_type && db_type !== 'mysql' && obj_type !== 'table') { + $('#obj_type').val('table'); + $('#obj_type').selectpicker('refresh'); + obj_type = 'table'; + } + if (obj_type === 'table') { + get_table_list(); + } else { + get_dict_list(obj_type); + } + } + // 获取表 function get_table_list() { var instance_name = $("#instance_name").val(); @@ -645,5 +946,361 @@ } }); } + + // 对象类型到接口、详情函数的映射 + var OBJ_TYPE_CONFIG = { + view: { + listUrl: '/data_dictionary/view_list/', + showFn: 'showViewInfo' + }, + trigger: { + listUrl: '/data_dictionary/trigger_list/', + showFn: 'showTriggerInfo' + }, + procedure: { + listUrl: '/data_dictionary/procedure_list/', + showFn: 'showProcedureInfo' + }, + function: { + listUrl: '/data_dictionary/function_list/', + showFn: 'showFunctionInfo' + }, + event: { + listUrl: '/data_dictionary/event_list/', + showFn: 'showEventInfo' + } + }; + + // 通用列表获取(视图/触发器/存储过程/函数/事件) + function get_dict_list(obj_type) { + var conf = OBJ_TYPE_CONFIG[obj_type]; + if (!conf) return; + var instance_name = $("#instance_name").val(); + var db_name = $('#db_name').val(); + var db_type = getCurrentDbType(); + if (instance_name === "" || db_name === "") { + alert("请先选择实例和数据库!"); + return; + } + $.ajax({ + type: "get", + url: conf.listUrl, + dataType: "json", + data: { + instance_name: instance_name, + db_name: db_name, + db_type: db_type + }, + success: function (data) { + if (data.status === 0) { + $('#btn_export_dict').removeClass('disabled'); + $('#btn_export_dict').prop('disabled', false); + $('#jumpbox').empty(); + $('#indexTable').empty(); + var result = data.data; + for (var k in result) { + var jumpBoxStr = '' + k + ''; + $('#jumpbox').append(jumpBoxStr); + $('#indexTable').append('' + + '' + + '' + k + '' + + '' + + ''); + for (var i = 0; i < result[k].length; i++) { + var objName = result[k][i][0]; + var objDesc = result[k][i][1] == null ? '' : result[k][i][1]; + var indexTableStr = '' + + '' + + '' + + '' + objName + '' + + '' + + '' + objDesc + '' + + ''; + $('#indexTable').append(indexTableStr); + } + } + $('#indexTable').prepend(' '); + } else { + alert(data.msg); + } + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + alert(errorThrown); + } + }); + } + + // 填充 meta_data table,通用工具 + function fillMetaTable(tableSelector, idPrefix, meta_data) { + if (!meta_data || !meta_data.column_list) return; + var cols = meta_data.column_list; + var vals = meta_data.rows || []; + for (var i = 0; i < cols.length; i++) { + var v = vals[i]; + if (v === null || v === undefined) v = ''; + $(tableSelector + ' #' + idPrefix + cols[i]).text(v); + } + } + + // 格式化 SQL 到 pre 元素 + function renderSqlToPre(preSelector, sqlText) { + if (sqlText === null || sqlText === undefined) sqlText = ''; + try { + if (window.sqlFormatter && typeof window.sqlFormatter.format === 'function') { + sqlText = window.sqlFormatter.format(sqlText); + } + } catch (e) { /* ignore */ } + $(preSelector).text(sqlText); + } + + // 展示视图详情 + function showViewInfo(ins_name, db_name, view_name) { + $.ajax({ + type: "get", + url: "/data_dictionary/view_info/", + dataType: "json", + data: { + instance_name: ins_name, + db_name: db_name, + view_name: view_name, + db_type: 'mysql' + }, + success: function (data) { + if (data.status === 0) { + var result = data.data; + showModalBody('view'); + // 填充基本信息 + var meta = result['meta_data'] || {}; + var cols = meta.column_list || []; + var vals = meta.rows || []; + var mp = {}; + for (var i = 0; i < cols.length; i++) mp[cols[i]] = vals[i]; + $('#view_name').text(mp['view_name'] || ''); + $('#view_definer').text(mp['definer'] || ''); + $('#view_security_type').text(mp['security_type'] || ''); + $('#view_check_option').text(mp['check_option'] || ''); + $('#view_is_updatable').text(mp['is_updatable'] || ''); + $('#view_character_set_client').text(mp['character_set_client'] || ''); + // 列信息 + var desc = result['desc'] || {column_list: [], rows: []}; + var columns_field = []; + $.each(desc.column_list, function (i, column) { + columns_field.push({"field": i, "title": column, "sortable": true}); + }); + $("#field-list-view").bootstrapTable('destroy').bootstrapTable({ + escape: true, + data: desc.rows, + columns: columns_field, + striped: true, + pagination: true, + pageSize: 30, + pageList: [30, 50, 100, 500, 1000], + locale: 'zh-CN' + }); + renderSqlToPre('#view_definition_sql', result['view_definition']); + $('#showModal').modal('show'); + } else { + alert(data.msg); + } + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + alert(errorThrown); + } + }); + } + + // 展示触发器详情 + function showTriggerInfo(ins_name, db_name, trigger_name) { + $.ajax({ + type: "get", + url: "/data_dictionary/trigger_info/", + dataType: "json", + data: { + instance_name: ins_name, + db_name: db_name, + trigger_name: trigger_name, + db_type: 'mysql' + }, + success: function (data) { + if (data.status === 0) { + var result = data.data; + showModalBody('trigger'); + var cols = result.column_list || []; + var vals = result.rows || []; + var mp = {}; + for (var i = 0; i < cols.length; i++) mp[cols[i]] = vals[i]; + $('#trigger_name').text(mp['trigger_name'] || ''); + $('#trigger_action_timing').text(mp['action_timing'] || ''); + $('#trigger_event_manipulation').text(mp['event_manipulation'] || ''); + $('#trigger_event_object_table').text(mp['event_object_table'] || ''); + $('#trigger_action_orientation').text(mp['action_orientation'] || ''); + $('#trigger_definer').text(mp['definer'] || ''); + $('#trigger_created').text(mp['created'] || ''); + $('#trigger_sql_mode').text(mp['sql_mode'] || ''); + renderSqlToPre('#trigger_action_statement', mp['action_statement']); + $('#showModal').modal('show'); + } else { + alert(data.msg); + } + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + alert(errorThrown); + } + }); + } + + // 从 create_sql 结果中提取 SQL 文本 + function extractCreateSqlText(createSqlRows) { + if (!createSqlRows || !createSqlRows.length) return ''; + var row = createSqlRows[0]; + if (!row) return ''; + // SHOW CREATE PROCEDURE/FUNCTION 中第3列为语句 + // SHOW CREATE EVENT 第4列为语句 + // 遵循 bootstrapTable field 索引,尝试常见位置 + if (typeof row === 'object' && !Array.isArray(row)) { + // 将对象转为数组 + var arr = []; + for (var k in row) arr.push(row[k]); + row = arr; + } + // 选取最长的字符串字段作为 SQL + var best = ''; + for (var i = 0; i < row.length; i++) { + var v = row[i]; + if (typeof v === 'string' && v.length > best.length) best = v; + } + return best; + } + + // 展示存储过程详情 + function showProcedureInfo(ins_name, db_name, proc_name) { + $.ajax({ + type: "get", + url: "/data_dictionary/procedure_info/", + dataType: "json", + data: { + instance_name: ins_name, + db_name: db_name, + proc_name: proc_name, + db_type: 'mysql' + }, + success: function (data) { + if (data.status === 0) { + var result = data.data; + showModalBody('procedure'); + var meta = result['meta_data'] || {}; + var cols = meta.column_list || []; + var vals = meta.rows || []; + var mp = {}; + for (var i = 0; i < cols.length; i++) mp[cols[i]] = vals[i]; + $('#proc_routine_name').text(mp['routine_name'] || ''); + $('#proc_routine_schema').text(mp['routine_schema'] || ''); + $('#proc_definer').text(mp['definer'] || ''); + $('#proc_security_type').text(mp['security_type'] || ''); + $('#proc_created').text(mp['created'] || ''); + $('#proc_last_altered').text(mp['last_altered'] || ''); + $('#proc_sql_mode').text(mp['sql_mode'] || ''); + $('#proc_routine_comment').text(mp['routine_comment'] || ''); + renderSqlToPre('#proc_create_sql', extractCreateSqlText(result['create_sql'])); + $('#showModal').modal('show'); + } else { + alert(data.msg); + } + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + alert(errorThrown); + } + }); + } + + // 展示函数详情 + function showFunctionInfo(ins_name, db_name, func_name) { + $.ajax({ + type: "get", + url: "/data_dictionary/function_info/", + dataType: "json", + data: { + instance_name: ins_name, + db_name: db_name, + func_name: func_name, + db_type: 'mysql' + }, + success: function (data) { + if (data.status === 0) { + var result = data.data; + showModalBody('function'); + var meta = result['meta_data'] || {}; + var cols = meta.column_list || []; + var vals = meta.rows || []; + var mp = {}; + for (var i = 0; i < cols.length; i++) mp[cols[i]] = vals[i]; + $('#func_routine_name').text(mp['routine_name'] || ''); + $('#func_return_type').text(mp['return_type'] || ''); + $('#func_definer').text(mp['definer'] || ''); + $('#func_routine_schema').text(mp['routine_schema'] || ''); + $('#func_created').text(mp['created'] || ''); + $('#func_last_altered').text(mp['last_altered'] || ''); + $('#func_sql_mode').text(mp['sql_mode'] || ''); + $('#func_routine_comment').text(mp['routine_comment'] || ''); + renderSqlToPre('#func_create_sql', extractCreateSqlText(result['create_sql'])); + $('#showModal').modal('show'); + } else { + alert(data.msg); + } + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + alert(errorThrown); + } + }); + } + + // 展示事件详情 + function showEventInfo(ins_name, db_name, event_name) { + $.ajax({ + type: "get", + url: "/data_dictionary/event_info/", + dataType: "json", + data: { + instance_name: ins_name, + db_name: db_name, + event_name: event_name, + db_type: 'mysql' + }, + success: function (data) { + if (data.status === 0) { + var result = data.data; + showModalBody('event'); + var meta = result['meta_data'] || {}; + var cols = meta.column_list || []; + var vals = meta.rows || []; + var mp = {}; + for (var i = 0; i < cols.length; i++) mp[cols[i]] = vals[i]; + $('#event_name').text(mp['event_name'] || ''); + $('#event_status').text(mp['status'] || ''); + $('#event_event_type').text(mp['event_type'] || ''); + var interval_desc = ''; + if (mp['interval_value']) { + interval_desc = mp['interval_value'] + ' ' + (mp['interval_field'] || ''); + } else if (mp['execute_at']) { + interval_desc = 'AT ' + mp['execute_at']; + } + $('#event_interval').text(interval_desc); + $('#event_starts').text(mp['starts'] || ''); + $('#event_ends').text(mp['ends'] || ''); + $('#event_last_executed').text(mp['last_executed'] || ''); + $('#event_definer').text(mp['definer'] || ''); + $('#event_on_completion').text(mp['on_completion'] || ''); + $('#event_event_comment').text(mp['event_comment'] || ''); + renderSqlToPre('#event_create_sql', extractCreateSqlText(result['create_sql'])); + $('#showModal').modal('show'); + } else { + alert(data.msg); + } + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + alert(errorThrown); + } + }); + } {% endblock %} diff --git a/sql/tests.py b/sql/tests.py index 1bef131182..4e8d47d8b0 100644 --- a/sql/tests.py +++ b/sql/tests.py @@ -1964,6 +1964,176 @@ def test_table_info_exception(self, _get_engine): self.assertEqual(r.status_code, 200) self.assertDictEqual(json.loads(r.content), {"msg": "test error", "status": 1}) + # ===== 视图/触发器/存储过程/函数/事件 测试 ===== + @patch("sql.data_dictionary.get_engine") + def test_view_list(self, _get_engine): + _get_engine.return_value.get_views_list.return_value = { + "v": [["v1", "select 1"]] + } + data = { + "instance_name": self.ins.instance_name, + "db_name": self.db_name, + "db_type": "mysql", + } + r = self.client.get(path="/data_dictionary/view_list/", data=data) + self.assertEqual(r.status_code, 200) + self.assertDictEqual( + json.loads(r.content), + {"status": 0, "data": {"v": [["v1", "select 1"]]}}, + ) + + def test_view_list_non_mysql(self): + data = { + "instance_name": self.ins.instance_name, + "db_name": self.db_name, + "db_type": "oracle", + } + r = self.client.get(path="/data_dictionary/view_list/", data=data) + self.assertDictEqual( + json.loads(r.content), {"status": 1, "msg": "仅MySQL支持该功能"} + ) + + @patch("sql.data_dictionary.get_engine") + def test_view_info(self, _get_engine): + _get_engine.return_value.get_view_detail.return_value = { + "meta_data": {"column_list": [], "rows": []}, + "desc": {"column_list": [], "rows": []}, + "view_definition": "select 1", + } + data = { + "instance_name": self.ins.instance_name, + "db_name": self.db_name, + "view_name": "v1", + "db_type": "mysql", + } + r = self.client.get(path="/data_dictionary/view_info/", data=data) + self.assertEqual(r.status_code, 200) + self.assertEqual(json.loads(r.content)["status"], 0) + + @patch("sql.data_dictionary.get_engine") + def test_trigger_list(self, _get_engine): + _get_engine.return_value.get_triggers_list.return_value = { + "t": [["tg1", "BEFORE INSERT ON t1"]] + } + data = { + "instance_name": self.ins.instance_name, + "db_name": self.db_name, + "db_type": "mysql", + } + r = self.client.get(path="/data_dictionary/trigger_list/", data=data) + self.assertEqual(json.loads(r.content)["status"], 0) + + @patch("sql.data_dictionary.get_engine") + def test_trigger_info(self, _get_engine): + _get_engine.return_value.get_trigger_detail.return_value = { + "column_list": ["trigger_name"], + "rows": ["tg1"], + } + data = { + "instance_name": self.ins.instance_name, + "db_name": self.db_name, + "trigger_name": "tg1", + "db_type": "mysql", + } + r = self.client.get(path="/data_dictionary/trigger_info/", data=data) + self.assertEqual(json.loads(r.content)["status"], 0) + + @patch("sql.data_dictionary.get_engine") + def test_procedure_list(self, _get_engine): + _get_engine.return_value.get_procedures_list.return_value = { + "p": [["p1", "cmt"]] + } + data = { + "instance_name": self.ins.instance_name, + "db_name": self.db_name, + "db_type": "mysql", + } + r = self.client.get(path="/data_dictionary/procedure_list/", data=data) + self.assertEqual(json.loads(r.content)["status"], 0) + + @patch("sql.data_dictionary.get_engine") + def test_procedure_info(self, _get_engine): + _get_engine.return_value.get_procedure_detail.return_value = { + "meta_data": {"column_list": [], "rows": []}, + "create_sql": [], + } + data = { + "instance_name": self.ins.instance_name, + "db_name": self.db_name, + "proc_name": "p1", + "db_type": "mysql", + } + r = self.client.get(path="/data_dictionary/procedure_info/", data=data) + self.assertEqual(json.loads(r.content)["status"], 0) + + @patch("sql.data_dictionary.get_engine") + def test_function_list(self, _get_engine): + _get_engine.return_value.get_functions_list.return_value = { + "f": [["f1", "cmt"]] + } + data = { + "instance_name": self.ins.instance_name, + "db_name": self.db_name, + "db_type": "mysql", + } + r = self.client.get(path="/data_dictionary/function_list/", data=data) + self.assertEqual(json.loads(r.content)["status"], 0) + + @patch("sql.data_dictionary.get_engine") + def test_function_info(self, _get_engine): + _get_engine.return_value.get_function_detail.return_value = { + "meta_data": {"column_list": [], "rows": []}, + "create_sql": [], + } + data = { + "instance_name": self.ins.instance_name, + "db_name": self.db_name, + "func_name": "f1", + "db_type": "mysql", + } + r = self.client.get(path="/data_dictionary/function_info/", data=data) + self.assertEqual(json.loads(r.content)["status"], 0) + + @patch("sql.data_dictionary.get_engine") + def test_event_list(self, _get_engine): + _get_engine.return_value.get_events_list.return_value = { + "e": [["e1", "ENABLED EVERY 1 DAY"]] + } + data = { + "instance_name": self.ins.instance_name, + "db_name": self.db_name, + "db_type": "mysql", + } + r = self.client.get(path="/data_dictionary/event_list/", data=data) + self.assertEqual(json.loads(r.content)["status"], 0) + + @patch("sql.data_dictionary.get_engine") + def test_event_info(self, _get_engine): + _get_engine.return_value.get_event_detail.return_value = { + "meta_data": {"column_list": [], "rows": []}, + "create_sql": [], + } + data = { + "instance_name": self.ins.instance_name, + "db_name": self.db_name, + "event_name": "e1", + "db_type": "mysql", + } + r = self.client.get(path="/data_dictionary/event_info/", data=data) + self.assertEqual(json.loads(r.content)["status"], 0) + + def test_event_info_non_mysql(self): + data = { + "instance_name": self.ins.instance_name, + "db_name": self.db_name, + "event_name": "e1", + "db_type": "oracle", + } + r = self.client.get(path="/data_dictionary/event_info/", data=data) + self.assertDictEqual( + json.loads(r.content), {"status": 1, "msg": "仅MySQL支持该功能"} + ) + def test_export_instance_does_not_exist(self): """ 测试导出实例不存在 diff --git a/sql/urls.py b/sql/urls.py index 66142fd6e3..f0773f96d5 100644 --- a/sql/urls.py +++ b/sql/urls.py @@ -122,6 +122,16 @@ path("data_dictionary/", views.data_dictionary), path("data_dictionary/table_list/", data_dictionary.table_list), path("data_dictionary/table_info/", data_dictionary.table_info), + path("data_dictionary/view_list/", data_dictionary.view_list), + path("data_dictionary/view_info/", data_dictionary.view_info), + path("data_dictionary/trigger_list/", data_dictionary.trigger_list), + path("data_dictionary/trigger_info/", data_dictionary.trigger_info), + path("data_dictionary/procedure_list/", data_dictionary.procedure_list), + path("data_dictionary/procedure_info/", data_dictionary.procedure_info), + path("data_dictionary/function_list/", data_dictionary.function_list), + path("data_dictionary/function_info/", data_dictionary.function_info), + path("data_dictionary/event_list/", data_dictionary.event_list), + path("data_dictionary/event_info/", data_dictionary.event_info), path("data_dictionary/export/", data_dictionary.export), path("param/list/", instance.param_list), path("param/history/", instance.param_history),