Files
ragflow/rag/utils/gaussdb_text_to_sql.py
2026-08-18 09:53:11 +08:00

220 lines
8.0 KiB
Python

#
# Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import logging
from sqlglot import exp, parse_one
from common.doc_store.gaussdb_conn_base import (
ExposedGaussDBTable,
GaussDBSQLValidator,
gaussdb_field_json_path_parts,
is_gaussdb_aggregate_sql,
jsonb_path_literal,
)
logger = logging.getLogger(__name__)
_SQL_RULES = """RULES:
1. Use only the table and fields listed below.
2. Select doc_id and docnm_kwd for non-aggregate data queries.
3. Do not add kb_id to SELECT or WHERE; the system injects and validates the KB scope.
4. Use only the static JSONB path literals listed under Fields.
5. Do not construct dynamic JSONB paths.
6. Do not use json_extract, json_extract_string, or json_extract_isnull; use #>> / #> instead.
7. A/ORA treats '' as NULL; never compare with = '' or <> ''; use IS NULL or IS NOT NULL.
8. Return exactly one read-only SELECT statement and no other text.
9. Do not use SELECT *, window functions, or system functions.
10. Do not use LATERAL, JOIN, UNION, INTERSECT, EXCEPT, or OR.
11. Do not expand JSON with jsonb_each, jsonb_each_text, jsonb_to_record, jsonb_to_recordset, jsonb_array_elements, or jsonb_array_elements_text.
12. Do not use DML or DDL, including INSERT, UPDATE, DELETE, MERGE, CREATE, ALTER, DROP, TRUNCATE, GRANT, REVOKE, CALL, COPY, or DO."""
def build_validator(table_name, kb_ids, field_map):
table = ExposedGaussDBTable.from_field_map(table_name, kb_ids, field_map)
return GaussDBSQLValidator({table.logical_name: table}, default_limit=128)
def is_aggregate_sql(sql):
return is_gaussdb_aggregate_sql(sql)
def _json_path_literal(field, field_type=None):
return jsonb_path_literal(gaussdb_field_json_path_parts(str(field), field_type))
def _field_lines(field_map):
return "\n".join(f" - {field} ({descriptor}): chunk_data #>> {_json_path_literal(field, descriptor)}" for field, descriptor in (field_map or {}).items())
def build_sql_prompt(table_name, field_map, question):
return """You are a Database Administrator. Write SQL for GaussDB A/ORA mode for a table with JSONB 'chunk_data' column.
Table: {table_name}
JSONB text extraction: chunk_data #>> '{{FieldName}}'
JSONB value extraction: chunk_data #> '{{FieldName}}'
Numeric cast: CAST(chunk_data #>> '{{FieldName}}' AS DOUBLE PRECISION)
Date cast: to_date(chunk_data #>> '{{FieldName}}', 'YYYY-MM-DD')
NULL check: (chunk_data #>> '{{FieldName}}') IS NOT NULL
{rules}
Fields:
{fields}
Question: {question}""".format(
table_name=table_name,
rules=_SQL_RULES,
fields=_field_lines(field_map),
question=question,
)
def build_user_prompt(table_name, field_map, question):
return """Table: {table_name}
Fields:
{fields}
Question: {question}
Write SQL using GaussDB JSONB #>> / #> syntax. Include doc_id and docnm_kwd for data queries. Only SQL.""".format(
table_name=table_name,
fields=_field_lines(field_map),
question=question,
)
def build_repair_prompt(table_name, field_map, question, previous_sql):
return """Table name: {table_name};
GaussDB JSONB fields available in chunk_data:
{fields}
Question: {question}
Previous SQL:
{previous_sql}
The previous SQL result is missing required source columns for citations.
Rewrite SQL to keep the same query intent and include doc_id and docnm_kwd in the SELECT list.
Use chunk_data #>> '{{field}}' or chunk_data #> '{{field}}' for JSONB fields.
For date fields, use to_date(chunk_data #>> '{{field}}', 'YYYY-MM-DD').
{rules}""".format(
table_name=table_name,
fields=_field_lines(field_map),
question=question,
previous_sql=previous_sql,
rules=_SQL_RULES,
)
def build_retry_prompt(table_name, field_map, question, error):
return """
Table name: {table_name};
GaussDB JSONB fields available in chunk_data:
{fields}
Question: {question}
Please write SQL using chunk_data #>> '{{field}}' / chunk_data #> '{{field}}' syntax. Include doc_id and docnm_kwd for data queries. Only SQL.
For date fields, use to_date(chunk_data #>> '{{field}}', 'YYYY-MM-DD').
The previous SQL error is:
{error}
Correct the SQL for GaussDB A/ORA mode.
{rules}
""".format(
table_name=table_name,
fields=_field_lines(field_map),
question=question,
error=error,
rules=_SQL_RULES,
)
def build_aggregate_source_sql(sql, doc_name_column, include_kb_id):
statement = parse_one(sql, read=GaussDBSQLValidator.PARSE_DIALECT)
if statement.args.get("having"):
raise ValueError("GaussDB aggregate source lookup cannot preserve HAVING safely")
columns = [exp.column("doc_id"), exp.column(doc_name_column)]
if include_kb_id:
columns.append(exp.column("kb_id"))
statement.set("expressions", columns)
statement.set("distinct", None)
statement.set("group", None)
statement.set("order", None)
statement.set("limit", None)
statement.set("offset", None)
return statement.sql(dialect=GaussDBSQLValidator.PARSE_DIALECT)
def build_source_reference(table, kb_ids):
rows = table.get("rows", [])
if not rows:
return None
columns = table.get("columns", [])
doc_idx = next((i for i, column in enumerate(columns) if column["name"].lower() == "doc_id"), None)
name_idx = next((i for i, column in enumerate(columns) if column["name"].lower() in ["docnm_kwd", "docnm"]), None)
kb_idx = next((i for i, column in enumerate(columns) if column["name"].lower() in ["kb_id", "kb_id_kwd"]), None)
if doc_idx is None or name_idx is None:
return None
chunks = []
doc_aggs = {}
for row in rows:
chunk = {"doc_id": row[doc_idx], "docnm_kwd": row[name_idx]}
if len(kb_ids or []) == 1:
chunk["kb_id"] = kb_ids[0]
elif kb_idx is not None:
chunk["kb_id"] = row[kb_idx]
chunks.append(chunk)
doc_id = row[doc_idx]
if doc_id not in doc_aggs:
doc_aggs[doc_id] = {"doc_name": row[name_idx], "count": 0}
doc_aggs[doc_id]["count"] += 1
return {
"chunks": chunks,
"doc_aggs": [{"doc_id": doc_id, "doc_name": values["doc_name"], "count": values["count"]} for doc_id, values in doc_aggs.items()],
}
def complete_reference_kb_ids(result, table_name, kb_ids, validator, sql_retrieval):
if len(kb_ids or []) <= 1:
return
chunks = result["reference"]["chunks"]
doc_ids = list(dict.fromkeys(str(chunk["doc_id"]) for chunk in chunks if chunk.get("doc_id") and not chunk.get("kb_id")))
if not doc_ids:
return
quoted_doc_ids = ", ".join("'" + doc_id.replace("'", "''") + "'" for doc_id in doc_ids)
lookup_sql = f"SELECT doc_id, kb_id FROM {table_name} WHERE doc_id IN ({quoted_doc_ids})"
try:
lookup_sql = validator.validate_and_patch(lookup_sql).sql
lookup_tbl = sql_retrieval(lookup_sql, format="json")
doc_idx = next((i for i, column in enumerate(lookup_tbl.get("columns", [])) if column["name"].lower() == "doc_id"), None)
kb_idx = next((i for i, column in enumerate(lookup_tbl.get("columns", [])) if column["name"].lower() in ["kb_id", "kb_id_kwd"]), None)
if doc_idx is None or kb_idx is None:
return
mapping = {row[doc_idx]: row[kb_idx] for row in lookup_tbl.get("rows", []) if len(row) > max(doc_idx, kb_idx)}
for chunk in chunks:
if not chunk.get("kb_id") and chunk.get("doc_id") in mapping:
chunk["kb_id"] = mapping[chunk["doc_id"]]
except Exception:
logger.warning("use_sql: Failed to complete GaussDB reference kb_id values", exc_info=True)