From 6fcf8c9b43974537827db0c17459a07845609698 Mon Sep 17 00:00:00 2001 From: yym Date: Sun, 26 Apr 2026 23:13:33 +0800 Subject: [PATCH 1/5] feat: Add SQLite and Hive database support with sample data for AI - Add SQLite support to data sources (file-based connection) - Add Hive support to data sources - Add SSL toggle option for MySQL and Doris databases - Add sample data (3 rows JSON format) to help AI better understand table schema - Fix SQLite schema table name issue (empty schema prefix) - Add SQLite.yaml template for SQL generation - Add Hive to haveSchema list in frontend Co-Authored-By: Claude Opus 4.6 --- backend/apps/chat/models/chat_model.py | 3 +- backend/apps/chat/task/llm.py | 16 ++- backend/apps/datasource/crud/datasource.py | 90 ++++++++++++++-- backend/apps/db/constant.py | 2 + backend/apps/db/db.py | 120 +++++++++++++++++++-- backend/apps/db/db_sql.py | 19 ++++ backend/templates/sql_examples/SQLite.yaml | 81 ++++++++++++++ backend/templates/template.yaml | 3 + frontend/src/views/ds/DatasourceForm.vue | 20 +++- frontend/src/views/ds/js/ds-type.ts | 8 +- 10 files changed, 343 insertions(+), 19 deletions(-) create mode 100644 backend/templates/sql_examples/SQLite.yaml diff --git a/backend/apps/chat/models/chat_model.py b/backend/apps/chat/models/chat_model.py index 66ef2060d..b0a6c2c73 100644 --- a/backend/apps/chat/models/chat_model.py +++ b/backend/apps/chat/models/chat_model.py @@ -229,6 +229,7 @@ class AiModelQuestion(BaseModel): custom_prompt: str = "" error_msg: str = "" regenerate_record_id: Optional[int] = None + sample_data: str = "" def sql_sys_question(self, db_type: Union[str, DB], enable_query_limit: bool = True): templates: dict[str, str] = {} @@ -256,7 +257,7 @@ def sql_sys_question(self, db_type: Union[str, DB], enable_query_limit: bool = T example_answer_1=_example_answer_1, example_answer_2=_example_answer_2, example_answer_3=_example_answer_3) - templates['schema'] = _base_template['generate_basic_info'].format(engine=self.engine, schema=self.db_schema) + templates['schema'] = _base_template['generate_basic_info'].format(engine=self.engine, schema=self.db_schema, sample_data=self.sample_data) if self.terminologies: templates['terminologies'] = _base_template['generate_terminologies_info'].format( diff --git a/backend/apps/chat/task/llm.py b/backend/apps/chat/task/llm.py index 006b8bb9a..7953b5e3a 100644 --- a/backend/apps/chat/task/llm.py +++ b/backend/apps/chat/task/llm.py @@ -36,7 +36,7 @@ from apps.chat.models.chat_model import ChatQuestion, ChatRecord, Chat, RenameChat, ChatLog, OperationEnum, \ ChatFinishStep, AxisObj, SystemPromptMessage, HumanPromptMessage, AIPromptMessage from apps.data_training.curd.data_training import get_training_template -from apps.datasource.crud.datasource import get_table_schema +from apps.datasource.crud.datasource import get_table_schema, get_tables_sample_data from apps.datasource.crud.permission import get_row_permission_filters, is_normal_user from apps.datasource.embedding.ds_embedding import get_ds_embedding from apps.datasource.models.datasource import CoreDatasource @@ -384,6 +384,13 @@ def choose_table_schema(self, _session: Session): ds=self.ds, question=self.chat_question.question) + # Get sample data for all tables + if not self.out_ds_instance: + self.chat_question.sample_data = get_tables_sample_data( + session=_session, + current_user=self.current_user, + ds=self.ds) + self.current_logs[OperationEnum.CHOOSE_TABLE] = end_log(session=_session, log=self.current_logs[OperationEnum.CHOOSE_TABLE], full_message=self.chat_question.db_schema) @@ -505,6 +512,13 @@ def generate_recommend_questions_task(self, _session: Session): question=self.chat_question.question, embedding=False) + # Get sample data for all tables + if not self.out_ds_instance: + self.chat_question.sample_data = get_tables_sample_data( + session=_session, + current_user=self.current_user, + ds=self.ds) + guess_msg: List[Union[BaseMessage, dict[str, Any]]] = [] guess_msg.append(SystemPromptMessage(content=self.chat_question.guess_sys_question(self.articles_number))) diff --git a/backend/apps/datasource/crud/datasource.py b/backend/apps/datasource/crud/datasource.py index 5a4cc6223..fc7afb7cf 100644 --- a/backend/apps/datasource/crud/datasource.py +++ b/backend/apps/datasource/crud/datasource.py @@ -17,7 +17,7 @@ from common.core.config import settings from common.core.deps import SessionDep, CurrentUser, Trans from common.utils.embedding_threads import run_save_table_embeddings, run_save_ds_embeddings -from common.utils.utils import SQLBotLogUtil, deepcopy_ignore_extra +from common.utils.utils import SQLBotLogUtil, deepcopy_ignore_extra, equals_ignore_case from common.core.sqlbot_cache import cache, clear_cache from .table import get_tables_by_ds_id from ..crud.field import delete_field_by_ds_id, update_field @@ -357,12 +357,16 @@ def preview(session: SessionDep, current_user: CurrentUser, id: int, data: Table {where} LIMIT 100""" elif ds.type == "dm": - sql = f"""SELECT "{'", "'.join(fields)}" FROM "{conf.dbSchema}"."{data.table.table_name}" - {where} + sql = f"""SELECT "{'", "'.join(fields)}" FROM "{conf.dbSchema}"."{data.table.table_name}" + {where} LIMIT 100""" elif ds.type == "es": - sql = f"""SELECT "{'", "'.join(fields)}" FROM "{data.table.table_name}" - {where} + sql = f"""SELECT "{'", "'.join(fields)}" FROM "{data.table.table_name}" + {where} + LIMIT 100""" + elif ds.type == "sqlite": + sql = f"""SELECT "{'", "'.join(fields)}" FROM "{data.table.table_name}" + {where} LIMIT 100""" return exec_sql(ds, sql, True) @@ -430,6 +434,79 @@ def get_table_obj_by_ds(session: SessionDep, current_user: CurrentUser, ds: Core return _list +def get_table_sample_data(ds: CoreDatasource, table_name: str, fields: list) -> str: + """Get 3 sample rows from a table in JSON format to help AI understand the data""" + if not fields: + return "" + + db = DB.get_db(ds.type) + # Get prefix/suffix for identifier quoting + prefix = db.prefix if hasattr(db, 'prefix') else '"' + suffix = db.suffix if hasattr(db, 'suffix') else '"' + + # Build field list with proper quoting + field_names = [] + for field in fields[:10]: # Limit to first 10 fields to avoid too wide results + field_name = f"{prefix}{field.field_name}{suffix}" + field_names.append(field_name) + + # Build LIMIT query based on database type + if equals_ignore_case(ds.type, "sqlServer"): + query = f"SELECT TOP 3 {','.join(field_names)} FROM {prefix}{table_name}{suffix}" + elif equals_ignore_case(ds.type, "ck"): + query = f"SELECT {','.join(field_names)} FROM {table_name} LIMIT 3" + elif equals_ignore_case(ds.type, "hive"): + query = f"SELECT {','.join(field_names)} FROM {table_name} LIMIT 3" + elif equals_ignore_case(ds.type, "oracle"): + query = f"SELECT {','.join(field_names)} FROM \"{table_name}\" WHERE ROWNUM <= 3" + elif equals_ignore_case(ds.type, "dm"): + query = f"SELECT {','.join(field_names)} FROM \"{table_name}\" WHERE ROWNUM <= 3" + else: + query = f"SELECT {','.join(field_names)} FROM {prefix}{table_name}{suffix} LIMIT 3" + + try: + result = exec_sql(ds=ds, sql=query, origin_column=True) + if result and result.get('data') and len(result['data']) > 0: + import json + # Truncate long string values for readability + json_rows = [] + for row in result['data'][:3]: + truncated_row = {} + for key, value in row.items(): + if value is None: + truncated_row[key] = None + elif isinstance(value, str): + # Truncate long strings + if len(value) > 100: + value = value[:100] + '...' + truncated_row[key] = value.replace('\n', ' ').replace('\r', ' ') + else: + truncated_row[key] = value + json_rows.append(truncated_row) + return json.dumps(json_rows, ensure_ascii=False, indent=2) + except Exception: + pass + return "" + + +def get_tables_sample_data(session: SessionDep, current_user: CurrentUser, ds: CoreDatasource, + table_list: list[str] = None) -> str: + """Get sample data (3 rows) for all tables to help AI understand the data""" + table_objs = get_table_obj_by_ds(session=session, current_user=current_user, ds=ds) + if len(table_objs) == 0: + return "" + + sample_data_parts = [] + for obj in table_objs: + if table_list is not None and obj.table.table_name not in table_list: + continue + if obj.fields: + sample = get_table_sample_data(ds, obj.table.table_name, obj.fields) + if sample: + sample_data_parts.append(f"# Table: {obj.table.table_name}\n{sample}") + return "\n".join(sample_data_parts) + + def get_table_schema(session: SessionDep, current_user: CurrentUser, ds: CoreDatasource, question: str, embedding: bool = True, table_list: list[str] = None) -> str: schema_str = "" @@ -446,7 +523,8 @@ def get_table_schema(session: SessionDep, current_user: CurrentUser, ds: CoreDat continue schema_table = '' - schema_table += f"# Table: {db_name}.{obj.table.table_name}" if ds.type != "mysql" and ds.type != "es" else f"# Table: {obj.table.table_name}" + no_schema_types = ["mysql", "es", "sqlite", "hive", "doris", "starrocks"] + schema_table += f"# Table: {db_name}.{obj.table.table_name}" if ds.type not in no_schema_types and db_name else f"# Table: {obj.table.table_name}" table_comment = '' if obj.table.custom_comment: table_comment = obj.table.custom_comment.strip() diff --git a/backend/apps/db/constant.py b/backend/apps/db/constant.py index 1509fcf47..46c8a1df5 100644 --- a/backend/apps/db/constant.py +++ b/backend/apps/db/constant.py @@ -28,6 +28,8 @@ class DB(Enum): oracle = ('oracle', 'Oracle', '"', '"', ConnectType.sqlalchemy, 'Oracle', []) pg = ('pg', 'PostgreSQL', '"', '"', ConnectType.sqlalchemy, 'PostgreSQL', []) starrocks = ('starrocks', 'StarRocks', '`', '`', ConnectType.py_driver, 'StarRocks', []) + sqlite = ('sqlite', 'SQLite', '"', '"', ConnectType.sqlalchemy, 'SQLite', []) + hive = ('hive', 'Apache Hive', '"', '"', ConnectType.py_driver, 'Hive', []) def __init__(self, type, db_name, prefix, suffix, connect_type: ConnectType, template_name: str, illegalParams: List[str]): diff --git a/backend/apps/db/db.py b/backend/apps/db/db.py index bbf9e97e1..d92360724 100644 --- a/backend/apps/db/db.py +++ b/backend/apps/db/db.py @@ -36,6 +36,12 @@ from sqlglot import expressions as exp from sqlalchemy.pool import NullPool +try: + from pyhive import hive + PYHIVE_AVAILABLE = True +except ImportError: + PYHIVE_AVAILABLE = False + try: if os.path.exists(settings.ORACLE_CLIENT_PATH): oracledb.init_oracle_client( @@ -88,6 +94,8 @@ def get_uri_from_config(type: str, conf: DatasourceConf) -> str: db_url = f"clickhouse+http://{urllib.parse.quote(conf.username)}:{urllib.parse.quote(conf.password)}@{conf.host}:{conf.port}/{conf.database}?{conf.extraJdbc}" else: db_url = f"clickhouse+http://{urllib.parse.quote(conf.username)}:{urllib.parse.quote(conf.password)}@{conf.host}:{conf.port}/{conf.database}" + elif equals_ignore_case(type, "sqlite"): + db_url = f"sqlite:///{conf.filename}" else: raise 'The datasource type not support.' return db_url @@ -157,6 +165,8 @@ def get_engine(ds: CoreDatasource, timeout: int = 0) -> Engine: elif equals_ignore_case(ds.type, 'mysql'): # mysql ssl_mode = {"require": True} if conf.ssl else None engine = create_engine(get_uri(ds), connect_args={"connect_timeout": conf.timeout, "ssl": ssl_mode}, poolclass=NullPool) + elif equals_ignore_case(ds.type, 'sqlite'): + engine = create_engine(get_uri(ds), connect_args={"check_same_thread": False}, poolclass=NullPool) else: # ck engine = create_engine(get_uri(ds), connect_args={"connect_timeout": conf.timeout}, poolclass=NullPool) return engine @@ -207,9 +217,10 @@ def check_connection(trans: Optional[Trans], ds: CoreDatasource | AssistantOutDs raise HTTPException(status_code=500, detail=trans('i18n_ds_invalid') + f': {e.args}') return False elif equals_ignore_case(ds.type, 'doris', 'starrocks'): + ssl_args = {'ssl': {'ssl_mode': 'REQUIRE'}} if conf.ssl else {} with pymysql.connect(user=conf.username, passwd=conf.password, host=conf.host, port=conf.port, db=conf.database, connect_timeout=10, - read_timeout=10, **extra_config_dict) as conn, conn.cursor() as cursor: + read_timeout=10, **extra_config_dict, **ssl_args) as conn, conn.cursor() as cursor: try: cursor.execute('select 1') SQLBotLogUtil.info("success") @@ -247,6 +258,26 @@ def check_connection(trans: Optional[Trans], ds: CoreDatasource | AssistantOutDs if is_raise: raise HTTPException(status_code=500, detail=trans('i18n_ds_invalid') + f': {e.args}') return False + elif equals_ignore_case(ds.type, 'hive'): + if PYHIVE_AVAILABLE: + try: + conn = hive.connect(host=conf.host, port=conf.port, username=conf.username, + database=conf.database, **extra_config_dict) + cursor = conn.cursor() + cursor.execute('select 1') + cursor.fetchall() + cursor.close() + conn.close() + SQLBotLogUtil.info("success") + return True + except Exception as e: + SQLBotLogUtil.error(f"Datasource {ds.id} connection failed: {e}") + if is_raise: + raise HTTPException(status_code=500, detail=trans('i18n_ds_invalid') + f': {e.args}') + return False + else: + SQLBotLogUtil.error("pyhive not installed") + return False elif equals_ignore_case(ds.type, 'es'): es_conn = get_es_connect(conf) if es_conn.ping(): @@ -289,6 +320,8 @@ def get_version(ds: CoreDatasource | AssistantOutDsSchema): # conf.timeout = 10 db = DB.get_db(ds.type) sql = get_version_sql(ds, conf) + if equals_ignore_case(ds.type, 'sqlite'): + return '' try: if db.connect_type == ConnectType.sqlalchemy: with get_session(ds) as session: @@ -304,13 +337,14 @@ def get_version(ds: CoreDatasource | AssistantOutDsSchema): res = cursor.fetchall() version = res[0][0] elif equals_ignore_case(ds.type, 'doris', 'starrocks'): + ssl_args = {'ssl': {'ssl_mode': 'REQUIRE'}} if conf.ssl else {} with pymysql.connect(user=conf.username, passwd=conf.password, host=conf.host, port=conf.port, db=conf.database, connect_timeout=10, - read_timeout=10, **extra_config_dict) as conn, conn.cursor() as cursor: + read_timeout=10, **extra_config_dict, **ssl_args) as conn, conn.cursor() as cursor: cursor.execute(sql) res = cursor.fetchall() version = res[0][0] - elif equals_ignore_case(ds.type, 'redshift', 'es'): + elif equals_ignore_case(ds.type, 'redshift', 'es', 'hive'): version = '' except Exception as e: print(e) @@ -333,6 +367,8 @@ def get_schema(ds: CoreDatasource): elif equals_ignore_case(ds.type, "oracle"): sql = """select * from all_users""" + elif equals_ignore_case(ds.type, "sqlite"): + return ['main'] with session.execute(text(sql)) as result: res = result.fetchall() res_list = [item[0] for item in res] @@ -374,7 +410,16 @@ def get_tables(ds: CoreDatasource): "excel") else get_engine_config() db = DB.get_db(ds.type) sql, sql_param = get_table_sql(ds, conf, get_version(ds)) - if db.connect_type == ConnectType.sqlalchemy: + if equals_ignore_case(ds.type, "sqlite"): + engine = get_engine(ds) + with engine.raw_connection() as conn: + cursor = conn.cursor() + cursor.execute(sql) + res = cursor.fetchall() + cursor.close() + res_list = [TableSchema(*item) for item in res] + return res_list + elif db.connect_type == ConnectType.sqlalchemy: with get_session(ds) as session: with session.execute(text(sql), {"param": sql_param}) as result: res = result.fetchall() @@ -390,9 +435,10 @@ def get_tables(ds: CoreDatasource): res_list = [TableSchema(*item) for item in res] return res_list elif equals_ignore_case(ds.type, 'doris', 'starrocks'): + ssl_args = {'ssl': {'ssl_mode': 'REQUIRE'}} if conf.ssl else {} with pymysql.connect(user=conf.username, passwd=conf.password, host=conf.host, port=conf.port, db=conf.database, connect_timeout=conf.timeout, - read_timeout=conf.timeout, **extra_config_dict) as conn, conn.cursor() as cursor: + read_timeout=conf.timeout, **extra_config_dict, **ssl_args) as conn, conn.cursor() as cursor: cursor.execute(sql, (sql_param,)) res = cursor.fetchall() res_list = [TableSchema(*item) for item in res] @@ -418,6 +464,18 @@ def get_tables(ds: CoreDatasource): res = get_es_index(conf) res_list = [TableSchema(*item) for item in res] return res_list + elif equals_ignore_case(ds.type, 'hive'): + if PYHIVE_AVAILABLE: + conn = hive.connect(host=conf.host, port=conf.port, username=conf.username, + database=conf.database, **extra_config_dict) + cursor = conn.cursor() + cursor.execute(sql) + res = cursor.fetchall() + res_list = [TableSchema(*item) for item in res] + cursor.close() + conn.close() + return res_list + return [] def get_fields(ds: CoreDatasource, table_name: str = None): @@ -425,7 +483,16 @@ def get_fields(ds: CoreDatasource, table_name: str = None): "excel") else get_engine_config() db = DB.get_db(ds.type) sql, p1, p2 = get_field_sql(ds, conf, table_name) - if db.connect_type == ConnectType.sqlalchemy: + if equals_ignore_case(ds.type, "sqlite"): + engine = get_engine(ds) + with engine.raw_connection() as conn: + cursor = conn.cursor() + cursor.execute(sql) + res = cursor.fetchall() + cursor.close() + res_list = [ColumnSchema(item[1], item[2], '') for item in res] + return res_list + elif db.connect_type == ConnectType.sqlalchemy: with get_session(ds) as session: with session.execute(text(sql), {"param1": p1, "param2": p2}) as result: res = result.fetchall() @@ -441,9 +508,10 @@ def get_fields(ds: CoreDatasource, table_name: str = None): res_list = [ColumnSchema(*item) for item in res] return res_list elif equals_ignore_case(ds.type, 'doris', 'starrocks'): + ssl_args = {'ssl': {'ssl_mode': 'REQUIRE'}} if conf.ssl else {} with pymysql.connect(user=conf.username, passwd=conf.password, host=conf.host, port=conf.port, db=conf.database, connect_timeout=conf.timeout, - read_timeout=conf.timeout, **extra_config_dict) as conn, conn.cursor() as cursor: + read_timeout=conf.timeout, **extra_config_dict, **ssl_args) as conn, conn.cursor() as cursor: cursor.execute(sql, (p1, p2)) res = cursor.fetchall() res_list = [ColumnSchema(*item) for item in res] @@ -469,6 +537,18 @@ def get_fields(ds: CoreDatasource, table_name: str = None): res = get_es_fields(conf, table_name) res_list = [ColumnSchema(*item) for item in res] return res_list + elif equals_ignore_case(ds.type, 'hive'): + if PYHIVE_AVAILABLE: + conn = hive.connect(host=conf.host, port=conf.port, username=conf.username, + database=conf.database, **extra_config_dict) + cursor = conn.cursor() + cursor.execute(sql) + res = cursor.fetchall() + res_list = [ColumnSchema(*item) for item in res] + cursor.close() + conn.close() + return res_list + return [] def convert_value(value, datetime_format='space'): @@ -587,9 +667,10 @@ def exec_sql(ds: CoreDatasource | AssistantOutDsSchema, sql: str, origin_column= except Exception as ex: raise ParseSQLResultError(str(ex)) elif equals_ignore_case(ds.type, 'doris', 'starrocks'): + ssl_args = {'ssl': {'ssl_mode': 'REQUIRE'}} if conf.ssl else {} with pymysql.connect(user=conf.username, passwd=conf.password, host=conf.host, port=conf.port, db=conf.database, connect_timeout=conf.timeout, - read_timeout=conf.timeout, **extra_config_dict) as conn, conn.cursor() as cursor: + read_timeout=conf.timeout, **extra_config_dict, **ssl_args) as conn, conn.cursor() as cursor: try: cursor.execute(sql) res = cursor.fetchall() @@ -655,6 +736,29 @@ def exec_sql(ds: CoreDatasource | AssistantOutDsSchema, sql: str, origin_column= "sql": bytes.decode(base64.b64encode(bytes(sql, 'utf-8')))} except Exception as ex: raise Exception(str(ex)) + elif equals_ignore_case(ds.type, 'hive'): + if PYHIVE_AVAILABLE: + conn = hive.connect(host=conf.host, port=conf.port, username=conf.username, + database=conf.database, **extra_config_dict) + cursor = conn.cursor() + try: + cursor.execute(sql) + res = cursor.fetchall() + columns = [field[0] for field in cursor.description] if origin_column else [field[0].lower() for + field in + cursor.description] + result_list = [ + {str(columns[i]): convert_value(value) for i, value in enumerate(tuple_item)} for tuple_item in + res + ] + return {"fields": columns, "data": result_list, + "sql": bytes.decode(base64.b64encode(bytes(sql, 'utf-8')))} + except Exception as ex: + raise ParseSQLResultError(str(ex)) + finally: + cursor.close() + conn.close() + raise Exception("pyhive not installed") def check_sql_read(sql: str, ds: CoreDatasource | AssistantOutDsSchema): diff --git a/backend/apps/db/db_sql.py b/backend/apps/db/db_sql.py index 566fbeb03..d4c3c74f8 100644 --- a/backend/apps/db/db_sql.py +++ b/backend/apps/db/db_sql.py @@ -31,6 +31,8 @@ def get_version_sql(ds: CoreDatasource, conf: DatasourceConf): """ elif equals_ignore_case(ds.type, "redshift"): return '' + elif equals_ignore_case(ds.type, "sqlite"): + return '' def get_table_sql(ds: CoreDatasource, conf: DatasourceConf, db_version: str = ''): @@ -162,6 +164,17 @@ def get_table_sql(ds: CoreDatasource, conf: DatasourceConf, db_version: str = '' """, conf.dbSchema elif equals_ignore_case(ds.type, "es"): return "", None + elif equals_ignore_case(ds.type, "sqlite"): + return """ + SELECT name AS TABLE_NAME, '' + FROM sqlite_master + WHERE type='table' + ORDER BY name + """, None + elif equals_ignore_case(ds.type, "hive"): + return """ + SHOW TABLES + """, None def get_field_sql(ds: CoreDatasource, conf: DatasourceConf, table_name: str = None): @@ -312,3 +325,9 @@ def get_field_sql(ds: CoreDatasource, conf: DatasourceConf, table_name: str = No return sql1 + sql2, conf.dbSchema, table_name elif equals_ignore_case(ds.type, "es"): return "", None, None + elif equals_ignore_case(ds.type, "sqlite"): + sql1 = f"PRAGMA table_info({table_name})" + return sql1, None, None + elif equals_ignore_case(ds.type, "hive"): + sql1 = f"DESCRIBE {table_name}" + return sql1, None, None diff --git a/backend/templates/sql_examples/SQLite.yaml b/backend/templates/sql_examples/SQLite.yaml new file mode 100644 index 000000000..bfcaacc94 --- /dev/null +++ b/backend/templates/sql_examples/SQLite.yaml @@ -0,0 +1,81 @@ +template: + quot_rule: | + + 必须对数据库名、表名、字段名、别名外层加双引号(")。 + + 1. 点号(.)不能包含在引号内,必须写成 "table" + 2. 即使标识符不含特殊字符或非关键字,也需强制加双引号 + + + + limit_rule: | + + 当需要限制行数时,必须使用标准的LIMIT语法 + + + other_rule: | + 必须为每个表生成别名(不加AS) + {multi_table_condition} + 禁止使用星号(*),必须明确字段名 + 中文/特殊字符字段需保留原名并添加英文别名 + 函数字段必须加别名 + 百分比字段保留两位小数并以%结尾 + 避免与数据库关键字冲突 + + basic_example: | + + + 📌 以下示例严格遵循中的 SQLite 规范,展示符合要求的 SQL 写法与典型错误案例。 + ⚠️ 注意:示例中的表名、字段名均为演示虚构,实际使用时需替换为用户提供的真实标识符。 + 🔍 重点观察: + 1. 双引号包裹所有数据库对象的规范用法 + 2. 中英别名/百分比/函数等特殊字段的处理 + 3. 关键字冲突的规避方式 + + + 查询 ORDERS 表的前100条订单(含中文字段和百分比) + + SELECT * FROM ORDERS LIMIT 100 -- 错误:未加引号、使用星号 + SELECT "订单ID", "金额" FROM "ORDERS" "t1" LIMIT 100 -- 错误:缺少英文别名 + SELECT COUNT("订单ID") FROM "ORDERS" "t1" -- 错误:函数未加别名 + + + SELECT + "t1"."订单ID" AS "order_id", + "t1"."金额" AS "amount", + COUNT("t1"."订单ID") AS "total_orders", + ROUND("t1"."折扣率" * 100, 2) || '%' AS "discount_percent" + FROM "ORDERS" "t1" + LIMIT 100 + + + + + 统计用户表 USERS(含关键字字段user)的活跃占比 + + SELECT user, status FROM USERS -- 错误:未处理关键字和引号 + SELECT "user", ROUND(active_ratio) FROM "USERS" -- 错误:百分比格式错误 + + + SELECT + "u"."user" AS "username", + ROUND("u"."active_ratio" * 100, 2) || '%' AS "active_percent" + FROM "USERS" "u" + WHERE "u"."status" = 1 + + + + + example_engine: SQLite 3.x + example_answer_1: | + {"success":true,"sql":"SELECT \"country_name\", \"continent_name\", \"year\", \"gdp\" FROM \"sample_country_gdp\" ORDER BY \"country_name\", \"year\"","tables":["sample_country_gdp"],"chart-type":"line"} + example_answer_1_with_limit: | + {"success":true,"sql":"SELECT \"country_name\", \"continent_name\", \"year\", \"gdp\" FROM \"sample_country_gdp\" ORDER BY \"country_name\", \"year\" LIMIT 1000","tables":["sample_country_gdp"],"chart-type":"line"} + example_answer_2: | + {"success":true,"sql":"SELECT \"country_name\", \"gdp\" FROM \"sample_country_gdp\" WHERE \"year\" = '2024' ORDER BY \"gdp\" DESC","tables":["sample_country_gdp"],"chart-type":"pie"} + example_answer_2_with_limit: | + {"success":true,"sql":"SELECT \"country_name\", \"gdp\" FROM \"sample_country_gdp\" WHERE \"year\" = '2024' ORDER BY \"gdp\" DESC LIMIT 1000","tables":["sample_country_gdp"],"chart-type":"pie"} + example_answer_3: | + {"success":true,"sql":"SELECT \"country_name\", \"gdp\" FROM \"sample_country_gdp\" WHERE \"year\" = '2025' AND \"country_name\" = '中国'","tables":["sample_country_gdp"],"chart-type":"table"} + example_answer_3_with_limit: | + {"success":true,"sql":"SELECT \"country_name\", \"gdp\" FROM \"sample_country_gdp\" WHERE \"year\" = '2025' AND \"country_name\" = '中国' LIMIT 1000","tables":["sample_country_gdp"],"chart-type":"table"} diff --git a/backend/templates/template.yaml b/backend/templates/template.yaml index a447c8287..0179c4605 100644 --- a/backend/templates/template.yaml +++ b/backend/templates/template.yaml @@ -348,6 +348,9 @@ template: {schema} + + {sample_data} + user: | diff --git a/frontend/src/views/ds/DatasourceForm.vue b/frontend/src/views/ds/DatasourceForm.vue index 240692d13..892824006 100644 --- a/frontend/src/views/ds/DatasourceForm.vue +++ b/frontend/src/views/ds/DatasourceForm.vue @@ -99,6 +99,13 @@ const rules = reactive({ trigger: 'blur', }, ], + filename: [ + { + required: true, + message: t('datasource.please_enter') + t('common.empty') + t('ds.form.file_path'), + trigger: 'blur', + }, + ], }) const dialogVisible = ref(false) @@ -647,7 +654,16 @@ defineExpose({ {{ $t('common.not_exceed_50mb') }} -
+
+ + + +
+
- + diff --git a/frontend/src/views/ds/js/ds-type.ts b/frontend/src/views/ds/js/ds-type.ts index 800fdefd4..4de66d5cb 100644 --- a/frontend/src/views/ds/js/ds-type.ts +++ b/frontend/src/views/ds/js/ds-type.ts @@ -10,6 +10,8 @@ import redshift from '@/assets/datasource/icon_redshift.png' import es from '@/assets/datasource/icon_es.png' import kingbase from '@/assets/datasource/icon_kingbase.png' import starrocks from '@/assets/datasource/icon_starrocks.png' +import sqlite_icon from '@/assets/datasource/icon_starrocks.png' +import hive_icon from '@/assets/datasource/icon_starrocks.png' import { i18n } from '@/i18n' const t = i18n.global.t @@ -26,6 +28,8 @@ export const dsType = [ { label: 'Elasticsearch', value: 'es' }, { label: 'Kingbase', value: 'kingbase' }, { label: 'StarRocks', value: 'starrocks' }, + { label: 'SQLite', value: 'sqlite' }, + { label: 'Apache Hive', value: 'hive' }, ] export const dsTypeWithImg = [ @@ -41,6 +45,8 @@ export const dsTypeWithImg = [ { name: 'Elasticsearch', type: 'es', img: es }, { name: 'Kingbase', type: 'kingbase', img: kingbase }, { name: 'StarRocks', type: 'starrocks', img: starrocks }, + { name: 'SQLite', type: 'sqlite', img: sqlite_icon }, + { name: 'Apache Hive', type: 'hive', img: hive_icon }, ] -export const haveSchema = ['sqlServer', 'pg', 'oracle', 'dm', 'redshift', 'kingbase'] +export const haveSchema = ['sqlServer', 'pg', 'oracle', 'dm', 'redshift', 'kingbase', 'hive'] From 4aca03a1f4b5fecdd417357e05803c69320e245f Mon Sep 17 00:00:00 2001 From: yym Date: Tue, 28 Apr 2026 21:27:01 +0800 Subject: [PATCH 2/5] feat: Add SQLite and Hive icons for datasource type selection Co-Authored-By: Claude Opus 4.6 --- frontend/src/assets/datasource/icon_hive.png | Bin 0 -> 1604 bytes frontend/src/assets/datasource/icon_sqlite.png | Bin 0 -> 10360 bytes frontend/src/views/ds/js/ds-type.ts | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 frontend/src/assets/datasource/icon_hive.png create mode 100644 frontend/src/assets/datasource/icon_sqlite.png diff --git a/frontend/src/assets/datasource/icon_hive.png b/frontend/src/assets/datasource/icon_hive.png new file mode 100644 index 0000000000000000000000000000000000000000..0fdebd0da7d858efb95a162ae76a9de7f41b0db3 GIT binary patch literal 1604 zcmaKs`#02i7{@=~nQ;vxgvPDNZHZ~EOYFGL6g3=!F0}n(%Jt@T~%3oyy^W)!!Z1VBGi?z)ML&odyON3R0aTO{{Mph&XWZx!_AqbgPGxJ zahX~4j98GBmF1N9dom-Mek9f@EhD~C{xc2$mF^&-Uj(yCCfR%5lmvRe^ zvup&7LHKKV4MY+JD{|xRP8qw7~~@);!lVl$S-pC)(>DCP~-2i(XD%llKj}OJ3}Am29#6KDCwcR1rpC z?D3GaIX}Q`96N_vA8=q<-%%+~5{6zLJWht9zyaD93|?EPb_lyPp}8k>HP?iV#m|2^ zCq?zZI}mxsto0^=S~`oyKONG9hsV@7& zqG+Z0qw7tbmLQ+%!Et%_&Zg;Cs1`4v&{m*Q(32?h+L6l9N8viNr(ELAWOsD9;$!Lu zN0t^3|B`e1q^H?IjN_9_T#1ShC@yV|p;E&L!VBBM)0}tRdy>*`m?Qgi*ii=euN5ts3ol>+3ePm3(Hr zd)EeGVWi2lR+Nc$=SIOqE6e&NG`^SEK(qJSIhiLgWA1EV)wF1HL~0&DafPqYYZbo{ zfoefTjwMGM>>jvzYWs7O5Z>&Go*UUw> zhUK*K-GIZU6F|=?8&aVeD)UyaEDc|3JaCHsHez~C8Ccd4;9EMe*SbW8O(B}T)B-r% zv`>&J#cW*y0&`Svda~W%3rpk>Cxe78s`f!L&Zk$DzTLJ2wv{*+zbf6K9)UK8WocL4 z83Jm}%!7jV^}=CJp8j-sq4`8Zi$og^hARY5mu{igG;jM5?h%ksQ3BkgT)Z0IX_5_I zNkHaHne9Img? z(+^@UK5cuxxTD`QvI1IvLYClqUguBO7|yg8O(p%y?;UCqv-($>dKYA{3A}c% zwqSbJlA>2;=d5R4Z4Z78fj(qt`j$1)f2rsQIJitzH*KQVhqdqoKZc3+{Zv1a_PDBl z7bDi8;_hz)!8fZ_7>OksWcT_^jyO=_rZW(XvEAXmsdVs|v$Rvx_0`A(Z**W>>vdqv znDEL~L*P#Dm~F@vr>Z!MFS&h`j$B_3RBawDI{eU8ahU8tX}^3q%FN|ig3(K(3Ehp~ cr5OH(9hYnG7Sa4}DnC95B83od`_oSU2di1khyVZp literal 0 HcmV?d00001 diff --git a/frontend/src/assets/datasource/icon_sqlite.png b/frontend/src/assets/datasource/icon_sqlite.png new file mode 100644 index 0000000000000000000000000000000000000000..e41576dd7e2663e940788e443b9035469d7c0b92 GIT binary patch literal 10360 zcmZWvbyQSew7xUc&c0-bMnAiytOn5oriYx0 zE(F~IA-0j|Z`_y4MjilwQT1O3ormqKpeJcOpBsAWxFbD%tX|szK0ZFYj&9B#HdZg~ zc->#yryoer0su3hq9Cj5o3Wqi6R7w6--Gn%P=*JYGB`Y80R=!~mC3%ol|hZA=j8Bm za=ynJY%cc-`PNuo{w)~fW%T1y(cXk^nlip#^Lvu_UvT@|M8eJG+zup9Z|{}Azl*`d z2gx7YoUdYK6pv*2h&tf;&l%Oa7pz|;) zIy$;%sl}&*JwK#@gX3x~|6ue7iq{J#n7H&;W0ic0ZU~=xUWa2!aJQgv+Phz!e=0?o zS%Zpi3{rLXo^-~ssTr3W);V!vl}*$B9iwC%pD*#*UGG0Ib~bTyb1PI^>Kjlr+a)r^ zYrnE~^O!6!pP&t@{gEyI>YKB1-MlXU=>6S|^tACZuH^>X?&k9^#;{Htrob2CtgI)Z zibugS*70^E0laBJo+8LwCI+yDXlRXDL|0u zN-OM0-OD~HI7|Dk=?A>$)6btjmsC_#o_rXzwXj{t4q`l3t+o7TLZ?H2;n$bV|A|Ih>;bg@5VekU?sB5mus_+w2w$+c` zb0{=cTwSNGZ7VC_1XzjRnXG_NaV`mdw6U_Pf8*%r*rJv^Iy4lG7ziAJPeUf*c(pSI z^ZOS%R2Fdq8`XLMpsb~#S4hzMm~F{dY=o_>c|856-~)rh^kz5$DKLp_ccCp_Jz9xhu~9q>BAQ(h|Qpa;*;FGJd4IDelrJNXDkk+_K6TIu18n7 ziHDkkErG(Nq}!SHuV+L}jrlWCGdTUosNS%0_Av6RO1H2^ROMy5NQX5QU~6le;@uMT z1>vMyW?biO?&Qb&cv=nwSlYdIE@Yb)M6r9DwI56-E{PGa9wig_z9rEH)0aCdGLm`U zsFct&Y1q02Jt;NmxQ(p+-Bdp}mxJIOLDno$QBVk_L|!2VJ-!qPqYWB=a()VrxX4Iu zC0=vKdHKG;B%&g+vVJX2$i{&EiBd_WrDUjs0=Nu!TN!Zy7)Je^@6$8A#ZB+_YVAyy z{M8}g@v7^1rC#%!L3aBT|DAqGA{gkLZ8M&VCYUjliJp~t#`E|t@9z7wLGnnS4NJS( z(#|NRKNy9zLPtUX#4fDvft_oha_L##v}}&k9;_L&BJ>8BWBx3;tI%#BqGtaHafQ@ zP}NxE&l%(fbTE%TA)TJ02$ActVu;A{$BwL#e=+^l7QpRD|43@4Ug35XF_&YTx8Fn&|v1(mYg3+(@SEll~$JNw27QAVI^fy!?@wQ z=62;c_a~P8&~ug-gK|Ltq&6Z_7%Y*X+=_qnaja4F_ye0oV!3_a3L^|9v@!5BnLS~8 zr70(+Xeq=hL}I+SGUQknmyynfyY_DZmCnka*_nS2A)c^lnlua8%)xcA zh!%^4B!`C1g|5hk1?&74?3~s9VYTmP-ew>l*P2y2*&f%qPl(kc)k1osfvA<0bv8O5 zy`;Y^SRB?1IpqPhAJd*_nD~0!VKOReV@6(v<% z(LkQrmf?6CRau$Y{bWF(SnG*Z??x(%r+OzT83tS1P29T#Gb7tcN*N<+y;U@L_J$2p zaDf0fEkc3QDYCR}6=XQy6^X!Z^=5-mk$6pAiea?#?e!|tptHNYu`y4vW=MsQU(+EQe5cK(HmjurY=V;3wJk)VbvEnEr>5V zwVbGR!~zcH{Nyi-Edx6{aTah2H-+wA@0_jvYj5GLt0yv?>^!sm(8ML2G@E@krE3=^ z9s8xCK823d1LMOuO6W=Gc&AKPCScx1QXS!f#*bmmi!hl(=pUTWl&PNR)Y_+`bQG1T z0;Kl5JMDY_YWJ{?JAgEo(};&*(E#t??N*Y@(<`->FKX3p1H0bDo$u2o=`Z7LUk^1) z)%>0EoLOa}&WXM^lWKrS*Jae){t<8`R^!)V`Zh^BwjY+%PMPy9^_mlT0k?_AcDMfE z<0LC>^ro}jIQWla%PX4|xqe186sEyw9%9Y?78zr>eV>PgY0eChlJQRNF{mQH6wIo3 z5CHzt%-ngy@9-asiDc9t-SF?x&xjk7Us1g=%Bs$Gq1%k|RSCZHymAW2twW8ISQuvqMvi@4jswXS z?oS5H*7&)S!N;`X`WT%fEX4+7Q9=rV_IWfrI$R|ITQQoi?sKX#>h6dHwTQc#ZxDE%7}D1{MSy?HKrPw%mFH-=A}vvh z%z_*AVtdejOi<>tyM&EnQ&@+q^5^DxJc0`xzJydV>c=#_-{nKFW2|FTiY%@`&g{-KPlf)j^BY%&7wcD*pX*w%y zt|!uX#hmj_#!z0A+6BjwEoCb=w2EPSCFtN@{vS8$Gm=Wh4;HvMR& z(6S{^fl>1cLu7yBZ*((J@WpxvkBY~c) zqUU{j#1j%3*W2CT!+fA_R!;g!r#L9n>gXf5dBclCF2qV{+J2tws>^Sn$1O6)@d6h6$z z(e^UtPg%Ux9gK8%3cyc8w6Q31<~2aynthzOa}45FzVxu-hBHFFJ}o0!OVOW zv?{3mU^*8$9D0JVNDQLAFrnE9 z^6L3gv)3MJtkkPjg;Fc(kR-&XSOgL}{*qi1WI!^Gk!(C-Ay=0QrY0G@v(lmd_n4XS z$&n^}Dg3I4YGY^el2yvQB!N-UFk+pIQ{q;DWh3BxciIFWkh0L^glvVo)!of%kPF{6hX4c%g+WFeBE;1KTZm&22m zcLvxp^+%7y%6zEH_m9Y?i;l75k0k~4D=9B24V?;kq8r?o+V9$t8tLsurcwd|li0|H zI9xy#f+Ej{2IG-R0$qO)c5(7%j z+mLuEzg;H_Ht>MCR=-mR2R^%-i=i-$baOsJfFMs%0D1q#yv^TUBEZkjk9V-T*QpE# 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 literal 0 HcmV?d00001 diff --git a/frontend/src/views/ds/js/ds-type.ts b/frontend/src/views/ds/js/ds-type.ts index 4de66d5cb..cca0982e4 100644 --- a/frontend/src/views/ds/js/ds-type.ts +++ b/frontend/src/views/ds/js/ds-type.ts @@ -10,8 +10,8 @@ import redshift from '@/assets/datasource/icon_redshift.png' import es from '@/assets/datasource/icon_es.png' import kingbase from '@/assets/datasource/icon_kingbase.png' import starrocks from '@/assets/datasource/icon_starrocks.png' -import sqlite_icon from '@/assets/datasource/icon_starrocks.png' -import hive_icon from '@/assets/datasource/icon_starrocks.png' +import sqlite_icon from '@/assets/datasource/icon_sqlite.png' +import hive_icon from '@/assets/datasource/icon_hive.png' import { i18n } from '@/i18n' const t = i18n.global.t From 018daff48f794e466f2a5ac35155774d7b038fee Mon Sep 17 00:00:00 2001 From: yym Date: Wed, 29 Apr 2026 01:24:13 +0800 Subject: [PATCH 3/5] fix(Hive): normalize identifier quoting and stabilize metadata/query paths Use Hive-compatible identifier handling and template defaults so generated SQL returns real column values, while also fixing table schema parsing and adding required Hive runtime dependencies. Made-with: Cursor --- backend/apps/datasource/models/datasource.py | 2 +- backend/apps/db/constant.py | 2 +- backend/apps/db/db.py | 168 +++++++++++-------- backend/pyproject.toml | 4 +- backend/templates/sql_examples/Hive.yaml | 86 ++++++++++ 5 files changed, 190 insertions(+), 72 deletions(-) create mode 100644 backend/templates/sql_examples/Hive.yaml diff --git a/backend/apps/datasource/models/datasource.py b/backend/apps/datasource/models/datasource.py index 6a23e0b7f..3971318cf 100644 --- a/backend/apps/datasource/models/datasource.py +++ b/backend/apps/datasource/models/datasource.py @@ -143,7 +143,7 @@ def to_dict(self): class TableSchema: - def __init__(self, attr1, attr2): + def __init__(self, attr1, attr2=None): self.tableName = attr1 self.tableComment = attr2 if attr2 is None or isinstance(attr2, str) else attr2.decode("utf-8") diff --git a/backend/apps/db/constant.py b/backend/apps/db/constant.py index 46c8a1df5..6ee33f02f 100644 --- a/backend/apps/db/constant.py +++ b/backend/apps/db/constant.py @@ -29,7 +29,7 @@ class DB(Enum): pg = ('pg', 'PostgreSQL', '"', '"', ConnectType.sqlalchemy, 'PostgreSQL', []) starrocks = ('starrocks', 'StarRocks', '`', '`', ConnectType.py_driver, 'StarRocks', []) sqlite = ('sqlite', 'SQLite', '"', '"', ConnectType.sqlalchemy, 'SQLite', []) - hive = ('hive', 'Apache Hive', '"', '"', ConnectType.py_driver, 'Hive', []) + hive = ('hive', 'Apache Hive', '`', '`', ConnectType.py_driver, 'Hive', []) def __init__(self, type, db_name, prefix, suffix, connect_type: ConnectType, template_name: str, illegalParams: List[str]): diff --git a/backend/apps/db/db.py b/backend/apps/db/db.py index d92360724..99e5911b7 100644 --- a/backend/apps/db/db.py +++ b/backend/apps/db/db.py @@ -2,6 +2,7 @@ import json import os import platform +import re import urllib.parse from datetime import datetime, date, time, timedelta from decimal import Decimal @@ -35,12 +36,8 @@ import sqlglot from sqlglot import expressions as exp from sqlalchemy.pool import NullPool +from pyhive import hive -try: - from pyhive import hive - PYHIVE_AVAILABLE = True -except ImportError: - PYHIVE_AVAILABLE = False try: if os.path.exists(settings.ORACLE_CLIENT_PATH): @@ -259,25 +256,22 @@ def check_connection(trans: Optional[Trans], ds: CoreDatasource | AssistantOutDs raise HTTPException(status_code=500, detail=trans('i18n_ds_invalid') + f': {e.args}') return False elif equals_ignore_case(ds.type, 'hive'): - if PYHIVE_AVAILABLE: - try: - conn = hive.connect(host=conf.host, port=conf.port, username=conf.username, - database=conf.database, **extra_config_dict) - cursor = conn.cursor() - cursor.execute('select 1') - cursor.fetchall() - cursor.close() - conn.close() - SQLBotLogUtil.info("success") - return True - except Exception as e: - SQLBotLogUtil.error(f"Datasource {ds.id} connection failed: {e}") - if is_raise: - raise HTTPException(status_code=500, detail=trans('i18n_ds_invalid') + f': {e.args}') - return False - else: - SQLBotLogUtil.error("pyhive not installed") + try: + conn = hive.connect(host=conf.host, port=conf.port, username=conf.username, + database=conf.database, **extra_config_dict) + cursor = conn.cursor() + cursor.execute('select 1') + cursor.fetchall() + cursor.close() + conn.close() + SQLBotLogUtil.info("success") + return True + except Exception as e: + SQLBotLogUtil.error(f"Datasource {ds.id} connection failed: {e}") + if is_raise: + raise HTTPException(status_code=500, detail=trans('i18n_ds_invalid') + f': {e.args}') return False + elif equals_ignore_case(ds.type, 'es'): es_conn = get_es_connect(conf) if es_conn.ping(): @@ -403,6 +397,30 @@ def get_schema(ds: CoreDatasource): res = cursor.fetchall() res_list = [item[0] for item in res] return res_list + elif equals_ignore_case(ds.type, 'hive'): + conn = hive.connect(host=conf.host, port=conf.port, username=conf.username, + database=conf.database, **extra_config_dict) + cursor = conn.cursor() + cursor.execute('SHOW DATABASES') + res = cursor.fetchall() + res_list = [item[0] for item in res] + cursor.close() + conn.close() + return res_list + elif equals_ignore_case(ds.type, 'doris', 'starrocks'): + with pymysql.connect(user=conf.username, passwd=conf.password, host=conf.host, + port=conf.port, db=conf.database, connect_timeout=10, + read_timeout=10, **extra_config_dict) as conn, conn.cursor() as cursor: + cursor.execute('SHOW DATABASES') + res = cursor.fetchall() + res_list = [item[0] for item in res] + return res_list + elif equals_ignore_case(ds.type, 'ck'): + with get_session(ds) as session: + with session.execute(text('SHOW DATABASES')) as result: + res = result.fetchall() + res_list = [item[0] for item in res] + return res_list def get_tables(ds: CoreDatasource): @@ -465,17 +483,15 @@ def get_tables(ds: CoreDatasource): res_list = [TableSchema(*item) for item in res] return res_list elif equals_ignore_case(ds.type, 'hive'): - if PYHIVE_AVAILABLE: - conn = hive.connect(host=conf.host, port=conf.port, username=conf.username, - database=conf.database, **extra_config_dict) - cursor = conn.cursor() - cursor.execute(sql) - res = cursor.fetchall() - res_list = [TableSchema(*item) for item in res] - cursor.close() - conn.close() - return res_list - return [] + conn = hive.connect(host=conf.host, port=conf.port, username=conf.username, + database=conf.database, **extra_config_dict) + cursor = conn.cursor() + cursor.execute(sql) + res = cursor.fetchall() + res_list = [TableSchema(*item) for item in res] + cursor.close() + conn.close() + return res_list def get_fields(ds: CoreDatasource, table_name: str = None): @@ -538,17 +554,15 @@ def get_fields(ds: CoreDatasource, table_name: str = None): res_list = [ColumnSchema(*item) for item in res] return res_list elif equals_ignore_case(ds.type, 'hive'): - if PYHIVE_AVAILABLE: - conn = hive.connect(host=conf.host, port=conf.port, username=conf.username, - database=conf.database, **extra_config_dict) - cursor = conn.cursor() - cursor.execute(sql) - res = cursor.fetchall() - res_list = [ColumnSchema(*item) for item in res] - cursor.close() - conn.close() - return res_list - return [] + conn = hive.connect(host=conf.host, port=conf.port, username=conf.username, + database=conf.database, **extra_config_dict) + cursor = conn.cursor() + cursor.execute(sql) + res = cursor.fetchall() + res_list = [ColumnSchema(*item) for item in res] + cursor.close() + conn.close() + return res_list def convert_value(value, datetime_format='space'): @@ -737,37 +751,53 @@ def exec_sql(ds: CoreDatasource | AssistantOutDsSchema, sql: str, origin_column= except Exception as ex: raise Exception(str(ex)) elif equals_ignore_case(ds.type, 'hive'): - if PYHIVE_AVAILABLE: - conn = hive.connect(host=conf.host, port=conf.port, username=conf.username, - database=conf.database, **extra_config_dict) - cursor = conn.cursor() - try: - cursor.execute(sql) - res = cursor.fetchall() - columns = [field[0] for field in cursor.description] if origin_column else [field[0].lower() for - field in - cursor.description] - result_list = [ - {str(columns[i]): convert_value(value) for i, value in enumerate(tuple_item)} for tuple_item in - res - ] - return {"fields": columns, "data": result_list, - "sql": bytes.decode(base64.b64encode(bytes(sql, 'utf-8')))} - except Exception as ex: - raise ParseSQLResultError(str(ex)) - finally: - cursor.close() - conn.close() - raise Exception("pyhive not installed") + conn = hive.connect(host=conf.host, port=conf.port, username=conf.username, + database=conf.database, **extra_config_dict) + cursor = conn.cursor() + try: + # Hive uses backticks for identifiers; normalize quoted identifiers as a compatibility fallback. + hive_sql = re.sub(r'"([A-Za-z_][A-Za-z0-9_]*)"', r'`\1`', sql) + cursor.execute(hive_sql) + res = cursor.fetchall() + columns = [field[0] for field in cursor.description] if origin_column else [field[0].lower() for + field in + cursor.description] + result_list = [ + {str(columns[i]): convert_value(value) for i, value in enumerate(tuple_item)} for tuple_item in + res + ] + return {"fields": columns, "data": result_list, + "sql": bytes.decode(base64.b64encode(bytes(hive_sql, 'utf-8')))} + except Exception as ex: + raise ParseSQLResultError(str(ex)) + finally: + cursor.close() + conn.close() def check_sql_read(sql: str, ds: CoreDatasource | AssistantOutDsSchema): try: + normalized_sql = sql.strip().lstrip("(").strip() + first_keyword = normalized_sql.split(None, 1)[0].upper() if normalized_sql else "" + allowed_read_commands = {"SELECT", "WITH", "SHOW", "DESCRIBE", "DESC", "EXPLAIN"} + denied_write_commands = { + "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER", + "TRUNCATE", "MERGE", "COPY", "REPLACE", "GRANT", "REVOKE", + "USE", "SET", "CALL" + } + + if not first_keyword: + raise ValueError("Parse SQL Error") + if first_keyword in denied_write_commands: + return False + dialect = None if equals_ignore_case(ds.type, 'mysql', 'doris', 'starrocks'): dialect = 'mysql' elif equals_ignore_case(ds.type, 'sqlServer'): dialect = 'tsql' + elif equals_ignore_case(ds.type, 'hive'): + dialect = 'hive' statements = sqlglot.parse(sql, dialect=dialect) @@ -777,7 +807,7 @@ def check_sql_read(sql: str, ds: CoreDatasource | AssistantOutDsSchema): write_types = ( exp.Insert, exp.Update, exp.Delete, exp.Create, exp.Drop, exp.Alter, - exp.Merge, exp.Command, exp.Copy + exp.Merge, exp.Copy ) for stmt in statements: @@ -786,7 +816,7 @@ def check_sql_read(sql: str, ds: CoreDatasource | AssistantOutDsSchema): if isinstance(stmt, write_types): return False - return True + return first_keyword in allowed_read_commands except Exception as e: raise ValueError(f"Parse SQL Error: {e}") diff --git a/backend/pyproject.toml b/backend/pyproject.toml index f112f7eca..727efde3b 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -53,7 +53,9 @@ dependencies = [ "elasticsearch[requests] (>=7.10,<8.0)", "ldap3>=2.9.1", "sqlglot>=28.6.0", - "numpy==2.3.5" + "numpy==2.3.5", + "pyhive[hive]>=0.7.0", + "thrift-sasl" ] [project.optional-dependencies] diff --git a/backend/templates/sql_examples/Hive.yaml b/backend/templates/sql_examples/Hive.yaml new file mode 100644 index 000000000..d4ef6e0e2 --- /dev/null +++ b/backend/templates/sql_examples/Hive.yaml @@ -0,0 +1,86 @@ +template: + quot_rule: | + + 必须对数据库名、表名、字段名、别名外层加反引号(`)。 + + 1. 点号(.)不能包含在引号内,必须写成 `database`.`table` + 2. 即使标识符不含特殊字符或非关键字,也需强制加反引号 + + + + limit_rule: | + + 当需要限制行数时,必须使用标准的LIMIT语法 + + + other_rule: | + 必须为每个表生成别名(不加AS) + {multi_table_condition} + 禁止使用星号(*),必须明确字段名 + 中文/特殊字符字段需保留原名并添加英文别名 + 不能用 + 拼接字符串,字符串必须使用单引号 + 分组非常严格:SELECT 里的字段必须出现在 GROUP BY 里,或者是聚合函数 + 函数字段必须加别名 + 百分比字段保留两位小数并以%结尾 + WHERE 条件中不能使用 >、<、>=、<= 等比较运算符,必须使用 = + HIVE 中没有 NOT IN 操作符,必须使用 LEFT JOIN 或 EXISTS 替代 + 判空使用 NVL()函数 + 避免与数据库关键字冲突 + + basic_example: | + + + 📌 以下示例严格遵循中的 Hive 规范,展示符合要求的 SQL 写法与典型错误案例。 + ⚠️ 注意:示例中的表名、字段名均为演示虚构,实际使用时需替换为用户提供的真实标识符。 + 🔍 重点观察: + 1. 反引号包裹所有数据库对象的规范用法 + 2. 中英别名/百分比/函数等特殊字段的处理 + 3. 关键字冲突的规避方式 + + + 查询 ods.orders 表的前100条订单(含中文字段和百分比) + + SELECT * FROM ods.orders LIMIT 100 -- 错误:未加引号、使用星号 + SELECT `订单ID`, `金额` FROM `ods`.`orders` `t1` LIMIT 100 -- 错误:缺少英文别名 + SELECT COUNT(`订单ID`) FROM `ods`.`orders` `t1` -- 错误:函数未加别名 + + + SELECT + `t1`.`订单ID` AS `order_id`, + `t1`.`金额` AS `amount`, + COUNT(`t1`.`订单ID`) AS `total_orders`, + CONCAT(CAST(ROUND(`t1`.`折扣率` * 100, 2) AS STRING), '%') AS `discount_percent` + FROM `ods`.`orders` `t1` + LIMIT 100 + + + + + 统计 dim.users(含关键字字段user)的活跃占比 + + SELECT user, status FROM dim.users -- 错误:未处理关键字和引号 + SELECT `user`, ROUND(active_ratio) FROM `dim`.`users` -- 错误:百分比格式错误 + + + SELECT + `u`.`user` AS `username`, + CONCAT(CAST(ROUND(`u`.`active_ratio` * 100, 2) AS STRING), '%') AS `active_percent` + FROM `dim`.`users` `u` + WHERE `u`.`status` = 1 + + + + + example_engine: Apache Hive 2.X + example_answer_1: | + {"success":true,"sql":"SELECT `country` AS `country_name`, `continent` AS `continent_name`, `year` AS `year`, `gdp` AS `gdp` FROM `Sample_Database`.`sample_country_gdp` ORDER BY `country`, `year`","tables":["sample_country_gdp"],"chart-type":"line"} + example_answer_1_with_limit: | + {"success":true,"sql":"SELECT `country` AS `country_name`, `continent` AS `continent_name`, `year` AS `year`, `gdp` AS `gdp` FROM `Sample_Database`.`sample_country_gdp` ORDER BY `country`, `year` LIMIT 1000","tables":["sample_country_gdp"],"chart-type":"line"} + example_answer_2: | + {"success":true,"sql":"SELECT `country` AS `country_name`, `gdp` AS `gdp` FROM `Sample_Database`.`sample_country_gdp` WHERE `year` = '2024' ORDER BY `gdp` DESC","tables":["sample_country_gdp"],"chart-type":"pie"} + example_answer_2_with_limit: | + {"success":true,"sql":"SELECT `country` AS `country_name`, `gdp` AS `gdp` FROM `Sample_Database`.`sample_country_gdp` WHERE `year` = '2024' ORDER BY `gdp` DESC LIMIT 1000","tables":["sample_country_gdp"],"chart-type":"pie"} + example_answer_3: | + {"success":true,"sql":"SELECT `country` AS `country_name`, `gdp` AS `gdp` FROM `Sample_Database`.`sample_country_gdp` WHERE `year` = '2025' AND `country` = '中国'","tables":["sample_country_gdp"],"chart-type":"table"} + example_answer_3_with_limit: | + {"success":true,"sql":"SELECT `country` AS `country_name`, `gdp` AS `gdp` FROM `Sample_Database`.`sample_country_gdp` WHERE `year` = '2025' AND `country` = '中国' LIMIT 1000","tables":["sample_country_gdp"],"chart-type":"table"} From 4b816e91732375fcff644c7fa76b0b3706c532a7 Mon Sep 17 00:00:00 2001 From: yym Date: Sun, 3 May 2026 09:01:00 +0800 Subject: [PATCH 4/5] feat/fix(charts): Normalize qualified SQL column names for display - Add DataFormat.normalize_qualified_sql_column_keys* so driver keys like "_u2.table_name" also expose "table_name", matching chart columns[].value. - Apply after SQL execution (LLM save path), format_json_list_data (API/history), and get_chart_data_ds (live refresh). Hive prompts: Document explicit table aliases for duplicate column names in JOINs. Frontend: Add datasource form i18n key "file_path" (en/zh-CN/zh-TW/ko-KR). Update Hive/SQLite datasource icons. Remove hive from haveSchema list. --- backend/apps/chat/curd/chat.py | 3 ++- backend/apps/chat/task/llm.py | 1 + backend/common/utils/data_format.py | 28 ++++++++++++++++++++++++ backend/templates/sql_examples/Hive.yaml | 1 + 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/backend/apps/chat/curd/chat.py b/backend/apps/chat/curd/chat.py index 1f8befdf5..eaddc671e 100644 --- a/backend/apps/chat/curd/chat.py +++ b/backend/apps/chat/curd/chat.py @@ -211,7 +211,7 @@ def format_json_list_data(origin_data: list[dict]): if len(decimal_str) > 15: value = str(value) _row[key] = value - data.append(_row) + data.append(DataFormat.normalize_qualified_sql_column_keys(_row)) return data @@ -253,6 +253,7 @@ def get_chart_data_ds(session: SessionDep,ds_id,sql): else: result = exec_sql(ds=datasource,sql=sql, origin_column=False) _data = DataFormat.convert_large_numbers_in_object_array(result.get('data')) + _data = DataFormat.normalize_qualified_sql_column_keys_in_object_array(_data) json_result['data'] = _data return json_result except Exception as e: diff --git a/backend/apps/chat/task/llm.py b/backend/apps/chat/task/llm.py index 7953b5e3a..cf469b371 100644 --- a/backend/apps/chat/task/llm.py +++ b/backend/apps/chat/task/llm.py @@ -1318,6 +1318,7 @@ def run_task(self, in_chat: bool = True, stream: bool = True, 'count': len(result.get('data'))}) _data = DataFormat.convert_large_numbers_in_object_array(result.get('data')) + _data = DataFormat.normalize_qualified_sql_column_keys_in_object_array(_data) result["data"] = _data self.save_sql_data(session=_session, data_obj=result) diff --git a/backend/common/utils/data_format.py b/backend/common/utils/data_format.py index 1991fb3e4..bfa9e88b5 100644 --- a/backend/common/utils/data_format.py +++ b/backend/common/utils/data_format.py @@ -17,6 +17,34 @@ def safe_convert_to_string(df): return df_copy + @staticmethod + def normalize_qualified_sql_column_keys(row: dict) -> dict: + """Add unqualified keys for names like ``alias.column`` (Hive/MySQL return shape). + + Chart bindings use the bare column name (``table_name``) while drivers may return + ``_u2.table_name``. Only adds ``short`` when absent to avoid clobbering real duplicates. + """ + if not row: + return row + out = dict(row) + for k, v in row.items(): + ks = str(k) + if "." not in ks: + continue + short = ks.rsplit(".", 1)[-1] + if short not in out: + out[short] = v + return out + + @staticmethod + def normalize_qualified_sql_column_keys_in_object_array(obj_array: list) -> list: + if not obj_array: + return obj_array + return [ + DataFormat.normalize_qualified_sql_column_keys(obj) if isinstance(obj, dict) else obj + for obj in obj_array + ] + @staticmethod def convert_large_numbers_in_object_array(obj_array, int_threshold=1e15, float_threshold=1e10): """处理对象数组,将每个对象中的大数字转换为字符串""" diff --git a/backend/templates/sql_examples/Hive.yaml b/backend/templates/sql_examples/Hive.yaml index d4ef6e0e2..813f6ab50 100644 --- a/backend/templates/sql_examples/Hive.yaml +++ b/backend/templates/sql_examples/Hive.yaml @@ -5,6 +5,7 @@ template: 1. 点号(.)不能包含在引号内,必须写成 `database`.`table` 2. 即使标识符不含特殊字符或非关键字,也需强制加反引号 + 3. 在多表关联(JOIN)/ 多子查询的 SQL 中,只要多个表 / 子查询存在 同名字段,所有引用该字段的位置,必须显式指定 表别名 From 99482af465b22a64629341fcfa785a9e96821574 Mon Sep 17 00:00:00 2001 From: yym Date: Sun, 3 May 2026 09:05:58 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix(chat):=20Normalize=20driver=20column=20?= =?UTF-8?q?keys=20(e.g.=20=5Fu2.col=20=E2=86=92=20col)=20for=20chart/table?= =?UTF-8?q?=20binding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - common/utils/data_format.py: normalize_qualified_sql_column_keys helpers - apps/chat/curd/chat.py: format_json_list_data + get_chart_data_ds - apps/chat/task/llm.py: persist normalized rows after SQL exec - templates/sql_examples/Hive.yaml: JOIN / duplicate column alias note Frontend — assets: - src/assets/datasource/icon_hive.png - src/assets/datasource/icon_sqlite.png Frontend — i18n (datasource.form.file_path): - src/i18n/en.json - src/i18n/zh-CN.json - src/i18n/zh-TW.json - src/i18n/ko-KR.json Frontend — datasource types: - src/views/ds/js/ds-type.ts: haveSchema list (remove hive) --- frontend/src/assets/datasource/icon_hive.png | Bin 1604 -> 7646 bytes .../src/assets/datasource/icon_sqlite.png | Bin 10360 -> 1676 bytes frontend/src/i18n/en.json | 3 ++- frontend/src/i18n/ko-KR.json | 3 ++- frontend/src/i18n/zh-CN.json | 3 ++- frontend/src/i18n/zh-TW.json | 3 ++- frontend/src/views/ds/js/ds-type.ts | 2 +- 7 files changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/src/assets/datasource/icon_hive.png b/frontend/src/assets/datasource/icon_hive.png index 0fdebd0da7d858efb95a162ae76a9de7f41b0db3..51b996f5e52d6123a6ea1407e605efedeb4dd0a0 100644 GIT binary patch literal 7646 zcmY*;Wl$VU)9nI5m*7rtC%8Mof_t!F!GpUyL4&(4E=h39;tqku7lKQ0Tij*g^3?nN zxm`Wer@BvdPt|nI{OA}BHTky~WEcPd;H{#9tk$b-{coWmz3wvZPEG&-5W065dY5t z-n{wGf%ISE|4;tsSzY-4Kk)KQ!^eY$hWdXTrH`-pzZ9?0w{KBiv%TtT6@O1G69dff zK%D>aCi=;UiTvmEdP}^_=K^$?)&dAae6#O zIfy_(j-siG+R2u-r9nqU0Zl@bP*3YE8|xcIIn4LW$X2EpBLhK@zE4)BK*)eaXRG%j z)InGPh4nqAmopY0H_DHq4|P>8W%)wkq1xZWa7~RxC@E1unF6n&=uiT22|Qm9`s`$^ zryJ_m(oZKePd5cmC)AI}uS)ZDO#OUL13#sCIe*1e=;#P06chybZQ;{( zKK!=e`MwAT2l3^hXmgH&kPwNV2j%vF`ecKOg$ZSEiE?(BoSf_p{FDiPlk#*zzdHTy z>4fcKhl-x|ji}%o_?0^~<(mLcZ1{!l)2SdQJ4)}M*z+k>UkmB=KJ`Q&IsDQtCj}pV z6$n4$Sr~gKBZ>NaL36Z5c>&AxbjEJ3B-T_#AMPXz55$GvWT(a9*_geBUx$l}Visi) z#D?SQX<<-P5x~zR+Z#w9Zwi$^qCZ{59`C2Z&*huILX{;XBcs9>dknQ@L@(DgGvna| z_?YD&Lio9wz17>}EhbqR*4=+eFZUoHPo+QofpaswovofmpM~HT#zuO;&Ti4<1fz&3 zCQom=yCd4FYW}+mkgyQ_)CAwtg{7<ZfTCw#SRPH;Iy;$J|s|s zhnprjjqTquhq?xFE{My?K~6=Pi-m=}xQxfh_?@+d8|roH;0Wam7@clr6rk` zmXe+U`N{U@yezuP%CgjW%E!m2l#~<*q|e*i`+A?F1c@T!^%iYCwA8c#_nb)1;V5T@ z&;0NQGMUx8?K9?q8|uI4AI3E~kNEAL65f#)9VTA|b{m^&!EwxX~pnouX4nA+o zn8Qgrj;?`|glcj;nNFQiZQe*loUQE--t}C#F30A&CN{|TY4_cL^UqIeq&(|70RS?3 zMOmp&K1;CP(AN_HLJHwOiGRNKdig|LP|m`I(DN5_HyLz<4>(TBG@8XG=Tdk+*W=wg z-sSycO;$@-GZ?yc>~g?CG@|cC)C34Da~WCu)jp%Y5jyBmphncn)VatF1A$B1635x3 zZ0yqU6f6G(&tpOg?#FKfCU5iZNj+Poijrl*nHr$gM@RgN=G=S!lCGsIM;yjD^_u&-vL8t5?UWrv{(tJKbltJU@mxObwsCm3m@5 zK*yZ}23|$4#XK9vH_ndgHkbS!&UAp-zRR)d%wMxRZ+T@clCkxxS1%keurqsya zNM*@)9Yh}_LvenVFoV>h@)}3K-Doeg4&YgHR*Q!kd+LlxJlq|^mqU|TcuD4O2(1J! zg1Ll%L+6h_&=v8{C_0@mgNto+iIH!Lw}iD#bQYch3TFxji>m|0wU-aa#Vr)C6R%nT zYcrEc`wBTSJ})saa$0{Ws|T%4uXc|^r_2l~RRTAmshNuIU`Dns&(4NJdB-n1naY5i zKfFB*F=q<`MrQ-9f-;(c=eRzdn^x_*0geRE?2{d;TOP=&l}8WIBb(M>jTT0hM7Z|f2hi^y|oAN}dxCS>lp{G%nv zTDx0AuP6i>))3Z5iwvlnd?Z1C;`2G}cs~$0cw%p4vG|>}=sV$jj_K;3fAS`|x9tNz z!43oQWnhjxTpu0W(xYB`tvV)YgMYY@VD~?Z#JV?9n$RRuJ?s480DFQ9949TlIOk0E zU!o;S(Y?6^P0ixIQ}9^II?AAcWwn_sxadYJQIHu$BXL>|>Qp_r{HVF|luY*O43FT) z1U=GwuA@h@O3#+g%f+M6{A{Lfz+^Q z*+H(60^|6lL!zYHbfUiCom%Lo^jFH^I{w{?$Eb3fw&n8b#YJsR;RAIWQ{xq0L{WY_ z1*-OsdswyE^0sbsl-tBea_J43dyW|t_gBpow$()zPv^1Ik74WG=+3#S=66?Fi>O?; zO%*RZJ9#U75>7S>f&w}vU9_gp!xd+yk2w|S(;kmkV^Q2Zh7+Y)iEb0V(ks&|e^LEH z_9pk6yvuWM*?WabTz$d@4xQFOKpx|X9Qo$#fAX-nIT4WyJ2WdX1A)8OCrsgtJRTuL z-g$)110q1OczRN(*rB8fw1tBF<=r;-BAFBG0mn1@gr-G_0SJ1kOdcxc3dB&#%EUD*RG!W`{h! zyxEehV)1w@Y?HY4Qj0E$*gL4a1Cu9q?#m5IGhR50^attU2{N6+s)!FfW(60-gJM>< zm*&5xWevOo1bp2EKINb z+sOa^NZ1qWuc8g%d|s8mC84oF3zC~o67~I7+?x1tU3X?fQVG`jZM0DUV@A88K{Pc8 zJc%N;=EWrAKtV8cC;wXHN9q}On>-ID6?#uUjfmVyb1m9? z8R)S4#Dn216}=xg%HC3?)Ma6Awd& z_3Ar$2N;W604BAb&V{Jfy6zTSp{LI!l)Ur_2+d-PUOMh~?xO3l8# zs7&IsL#I>_xgErv@w6)wztu9wKz%rhb8g=I)(f$E*SfK_`B7?l>;qGO(gOA+lywIS zno5ki1!>Z)N(e8*{qEL@!TjcuB;JjX0mShhDP+bnnnqg}yY^tF zSg+*%wA*E!8u@}yR^-pALmGhm;_Z7R2O6UEJhnKBEiT~il)edaAehhI9AP48_quQZlD zF*a>t1rotdsHgFWPGafB!QX$L(AEXi$HD#zy(6PpxR-5WyWNtrVhKtu4d@o?pftCz z_jYz2}kt8pY&lbc;xhKrz5Z{%#g1VJb9D zBDuhgI2PS(Di~FQi+u|c*wP#XeYL1nnAj+88(v|u*e8R8%7B0<-(-`xn*Z^Cmx)BH zmEFkXZbL_(9yV*Qil+jXPK?VJ^Y6Hk`H4Rk1NnEsRDk_5= z{zuL6p*eZ$5=$WViPIGpRYT`-efeTYj%m-!;TWfn%nQPz5p7LF*jxOaVPWhbM-Knv=6$TF0q&MNiu;3ttTgWmfK`HpdXB`Q1r z#gX>M9K2>+2hUuZ1+aF+Cio1P^Gn_7T$$1x5Q!=Xe?YdhlG zi*MAHdD%9|OhV$BI{NLegs~z?O>`NmmzV3QXCG||1+YB*E;9LyL_IW7Ea2=X^p11F zE{!fKaFr-Bfym8jLQTXV3tN(eDx76$f?-J~?a!24n*IK#D@2(C`Q?P6Fpi{J#W(eh zu*~*F#r1ODo9M@EbSD2PNhQ5}(03Z=EzWFVli`gCa;lCsE`=1IphZ@3Ps<$zYHj;V z@pX&6k4q`Q@qHA+R5og`?}R7SQt$@(tW%;{ zS*y!0&E`UHA=X){`b&GF-47K$boIO6LK8wU8Zkep=CTNJSb2fs^$$-~EQUB1@nq*2 zatA~-X6#17g{lS9z8ak#Xl$J>utPiEiSZKEfap@1e+T!Nzsa$-O;^Wf(XH&8fs|CJ z&j-J>kEK;)_JkLAqu`y^*m@jTPC;qP#xQO6dOoh-N zen-wJP3>&6Y1RmoYH&m&wBJKk?0cDyku*3S1G?U~FR)B5(0MCJAF^rYfe2YGy}$w0 zcui^d+Ji>6&6Dx$Xz!TlBQ9-F)e|Qs;h{z30gW~9b=nk$rv8Z@Mn9IeT?WOeG|X(VjZc#yuI<$T9^ewfnf%b9+O>CN2Jv@G`D316%1ex))fzfaJXXR!dOb( zr)xXm4<0@a7Z;0bZszE8%(a16y-@(J=S=ZIVUZAspZ2L`Vz!WcmYd!R$Vu8`ubL(9 zBFR>cj`L6UZhxxHc|wzdgG1>XlYk?_o`oaUy4KRN8`#3tW9n8?>bO^6P2zl4P)x-i zMw7z&E1lXxkf^_3Lnc$s)>$+sr{1sJLvbFyITRHvLxQ9HV>iwYtX!|g)d&pg$eRf= z{%?^DLy?21pPQO@E)1k**e{Cx3yVAJyCw;4eg!_{3(Buik%E_xC^pB?2pU(jycim! zEm#RPs!^MgqN1s&i#m_@7uhEU6RBb3*-qx0&(T#kIiP%it=jE2!n^gNuV)$>9jT>XOpJ!VK)heKs@F4v` zHEiAD5E-U)R2ETouG z2Pzfz-(gCia@0+254fl&G=XsgB97W%cZ{f;nV224b_iQ1VidI+W9NTHQB!=2u!Qie za}PNRnSqL8;=eOwo6Ku2mk@^NYuE<-N7B^UStWfAvm6mqX88xl1Ug7u5Rf}Zi$BQA z^k{+_RJ3Rcc>9d2Cf&S)&H>5KpI9)|G%D*G%8cS{VWq@Bm%{MiRWm#iJQQwcl|Uwg z6Mu63dD(_2O6vR;8zo;$at)RknLF8WPN(myIV!ixj}}LD*YWk)jbS~;@3(kaP`K?(ZsPn@dGj{bPB2T5|pd_tl6^fv86q zCSko2_Z0Jt*{LaJd$Y-$f;Vl-M?W>ptfFOlw?5Xsb4CIuRQ+%0Tal6>8=DqUG4jzr za2?r4`x6|h$qtT7QK@5O{YDIKS)n7i%eRr31LWg*retR-=TGM)0StX;d(Wazuk_-|E@rmT1^5fj*y2L3?}kUpC!MTknZ3V1nl$A$_+03g(W3fmN3TK zB@WQZ`)_i}asWr0f$wlBDN(16l?{PioDr|H3DKQ&j0pue&R zi%Pi7GNkJyM+{rE)h;Pp{d$&3&dc?T5Wy!!a29N@djJFK$upEOgsi+e> zTL%#Gl$Zm<>F2Hd)wn@gY&@lsTz59FljLk#dH-^|sd#*>jZ<5X5YFNI*7ew`a5*F2 zXL}D?^4zdMl`iKZiF_07T(0+ztA2E4O{#!102#P0gKjf=Bg?`)$C>S~o^C?~el zxT)7j3YIEs{M|4CB)L{K4Zrio+?whC2JFO;$MLgKdknbT;;~#E+8rq9b4=@jmSp>< zkh59Z+mT*an>(H(2%)!$S=!g@=`{Oy(DmcG&l!ul*WKJCYpySBkByCOQ_qP%v;hkX zExng=l3hhtL$LW-WZUP63*W#gd$w(uI(Q7$1^M~qKlb2japEfR z4l8j&&RTJtZ3$dn8@il!&a z_SIiqRkvTnrfBMK2^!<_1YKSUlYZTX)R5UU(;wg;q82hckNV{uo^NzflnHpxtY#)_FNs z-osvTa-q%)>8R20m*Um=px`FkpuX$R$#|K>q!FZT&D@R@qmFt9)yHj@8Di{r60W;% ziP>bTVNO`tJ~oW6{m=+-B9G}itK3n+w6ex(bcTg+2aJtjVtgwFj!Z#z1dObm60=_g z#d-g0)~W89`TGh!dcv+E;;l>&(<$d?k!-|GwH!-`?%j3dJ{>QJWG5SuzvR;I8YC-JvV^%N`#O>f??abYn5_wmLY@i;W()TR2G(?;D{^x+ezMQ!2YzxDu#v z68;3|BlWmHEMJ?p9;}$qKIi}NuM|rI^`g66NKKo9g+&w8(s0OKpu+8C$M zCz2lN84~%@#iPFaQ9MO2@;c_W_Fm7QroSzcIEErq0^@Q-dk31or>sLcM!~hG#5|SQ zn%TZT9~{j0sf4Bbkj8b&#y&^1Y3#frNo~;1PA*S4Wa5yIsl${0v+_mjQhpwo7H)1@ zY<9wW%SE;;1Z@J-Qey}8$c~*$r5LcK1!=+`e)1K``Rq^gS5}rDm6OYnktx6r--~4{ z*l54-g4@%i9kGO7m@?FL90IxB6U?$^2@4DSZuoY-^@Fg*Z-%r*Zyhlsq{(lqg>PnK z+x_S8MuiQ8fkYKW!o{}vqpYfKqMR>L3~3Gt`xXgfx$4+G+yO0ymeuK2LRK6(*&UV} zE4bNPPtM)53Ft!#?pYwQWJOL#(IU^(S`?N!+sA>mf!b~^enh4xl zJK`JDBA!3t(Qi~Dv*|4PEgbK_4>8OI`<{Egcy6bQ>NQ<@(H(;CcV#+~<- z9}iS~joI@l<%CT319qP)4f%d+%*ys}%gbVyT`;W3DB!aB^yJ`pSwoqqDA{(6h*0BE3rY{0ih2KgD~|lf(#jX8 zQ~M62EI9ZjV7>gU<$z$-k^f~uT`7f2?r6KBE35n14$leU`R>nF9xN`*!%fjAViUaw zZ(_5_er(fAv~k>Hqh*Y`@F-sIGwyRD?7~*Geux-iWQu`r=*t|iw(am&?WtiaPqXYS zr9=TfS8=R91*emj;?!N{_aVB{}@T0qq>B5hZtVzn4D z?SwMpk5}n(%Jt@T~%3oyy^W)!!Z1VBGi?z)ML&odyON3R0aTO{{Mph&XWZx!_AqbgPGxJ zahX~4j98GBmF1N9dom-Mek9f@EhD~C{xc2$mF^&-Uj(yCCfR%5lmvRe^ zvup&7LHKKV4MY+JD{|xRP8qw7~~@);!lVl$S-pC)(>DCP~-2i(XD%llKj}OJ3}Am29#6KDCwcR1rpC z?D3GaIX}Q`96N_vA8=q<-%%+~5{6zLJWht9zyaD93|?EPb_lyPp}8k>HP?iV#m|2^ zCq?zZI}mxsto0^=S~`oyKONG9hsV@7& zqG+Z0qw7tbmLQ+%!Et%_&Zg;Cs1`4v&{m*Q(32?h+L6l9N8viNr(ELAWOsD9;$!Lu zN0t^3|B`e1q^H?IjN_9_T#1ShC@yV|p;E&L!VBBM)0}tRdy>*`m?Qgi*ii=euN5ts3ol>+3ePm3(Hr zd)EeGVWi2lR+Nc$=SIOqE6e&NG`^SEK(qJSIhiLgWA1EV)wF1HL~0&DafPqYYZbo{ zfoefTjwMGM>>jvzYWs7O5Z>&Go*UUw> zhUK*K-GIZU6F|=?8&aVeD)UyaEDc|3JaCHsHez~C8Ccd4;9EMe*SbW8O(B}T)B-r% zv`>&J#cW*y0&`Svda~W%3rpk>Cxe78s`f!L&Zk$DzTLJ2wv{*+zbf6K9)UK8WocL4 z83Jm}%!7jV^}=CJp8j-sq4`8Zi$og^hARY5mu{igG;jM5?h%ksQ3BkgT)Z0IX_5_I zNkHaHne9Img? z(+^@UK5cuxxTD`QvI1IvLYClqUguBO7|yg8O(p%y?;UCqv-($>dKYA{3A}c% zwqSbJlA>2;=d5R4Z4Z78fj(qt`j$1)f2rsQIJitzH*KQVhqdqoKZc3+{Zv1a_PDBl z7bDi8;_hz)!8fZ_7>OksWcT_^jyO=_rZW(XvEAXmsdVs|v$Rvx_0`A(Z**W>>vdqv znDEL~L*P#Dm~F@vr>Z!MFS&h`j$B_3RBawDI{eU8ahU8tX}^3q%FN|ig3(K(3Ehp~ cr5OH(9hYnG7Sa4}DnC95B83od`_oSU2di1khyVZp diff --git a/frontend/src/assets/datasource/icon_sqlite.png b/frontend/src/assets/datasource/icon_sqlite.png index e41576dd7e2663e940788e443b9035469d7c0b92..1a198d0b4e8209497164216a42530236bd741b66 100644 GIT binary patch literal 1676 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%0D1q#yv^TUBEZkjk9V-T*QpE# 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 diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index df8d29c06..4670a92eb 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -395,7 +395,8 @@ "timeout": "Connection Timeout(second)", "address": "Address", "low_version": "Compatible with lower versions", - "ssl": "Enable SSL" + "ssl": "Enable SSL", + "file_path": "File Path" }, "sync_fields": "Sync Fields" }, diff --git a/frontend/src/i18n/ko-KR.json b/frontend/src/i18n/ko-KR.json index bb269ca1f..133c2813e 100644 --- a/frontend/src/i18n/ko-KR.json +++ b/frontend/src/i18n/ko-KR.json @@ -395,7 +395,8 @@ "timeout": "연결 시간 초과(초)", "address": "주소", "low_version": "낮은 버전 호환", - "ssl": "SSL 활성화" + "ssl": "SSL 활성화", + "file_path": "파일 경로" }, "sync_fields": "동기화된 테이블 구조" }, diff --git a/frontend/src/i18n/zh-CN.json b/frontend/src/i18n/zh-CN.json index 902b06421..8040db821 100644 --- a/frontend/src/i18n/zh-CN.json +++ b/frontend/src/i18n/zh-CN.json @@ -395,7 +395,8 @@ "timeout": "连接超时(秒)", "address": "地址", "low_version": "兼容低版本", - "ssl": "启用 SSL" + "ssl": "启用 SSL", + "file_path": "文件路径" }, "sync_fields": "同步表结构" }, diff --git a/frontend/src/i18n/zh-TW.json b/frontend/src/i18n/zh-TW.json index bd2aa57cb..d8973c4b8 100644 --- a/frontend/src/i18n/zh-TW.json +++ b/frontend/src/i18n/zh-TW.json @@ -395,7 +395,8 @@ "timeout": "連線逾時(秒)", "address": "位址", "low_version": "相容低版本", - "ssl": "啟用 SSL" + "ssl": "啟用 SSL", + "file_path": "文件路徑" }, "sync_fields": "同步表結構" }, diff --git a/frontend/src/views/ds/js/ds-type.ts b/frontend/src/views/ds/js/ds-type.ts index cca0982e4..b2d578b74 100644 --- a/frontend/src/views/ds/js/ds-type.ts +++ b/frontend/src/views/ds/js/ds-type.ts @@ -49,4 +49,4 @@ export const dsTypeWithImg = [ { name: 'Apache Hive', type: 'hive', img: hive_icon }, ] -export const haveSchema = ['sqlServer', 'pg', 'oracle', 'dm', 'redshift', 'kingbase', 'hive'] +export const haveSchema = ['sqlServer', 'pg', 'oracle', 'dm', 'redshift', 'kingbase']