2026-09-13 16:19:24 +08:00
|
|
|
|
"""构造并 upsert NL2SQL 元数据行。"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import time
|
2026-09-13 20:48:42 +08:00
|
|
|
|
from collections.abc import Awaitable, Callable, Iterable
|
2026-09-13 16:19:24 +08:00
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
from nl2sql.embedding import embed_texts
|
|
|
|
|
|
from nl2sql.metadata import build_metadata_chunks
|
|
|
|
|
|
from nl2sql.milvus_collections import NL2SQL_COLLECTION
|
|
|
|
|
|
|
2026-09-13 20:48:42 +08:00
|
|
|
|
# ORM 已定义但数据库尚未建表的表,在表说明后追加此标注,
|
|
|
|
|
|
# 让 NL2SQL 生成侧知道该表暂不可查询(权威 Schema 校验也会兜底拒绝)。
|
|
|
|
|
|
MISSING_TABLE_MARKER = "(注意:当前数据库中尚未建表)"
|
|
|
|
|
|
|
2026-09-13 16:19:24 +08:00
|
|
|
|
|
|
|
|
|
|
def _escape_filter_value(value: str) -> str:
|
|
|
|
|
|
return value.replace("\\", "\\\\").replace('"', '\\"')
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-13 20:48:42 +08:00
|
|
|
|
def _row_table_name(row: dict[str, Any]) -> str:
|
|
|
|
|
|
"""兼容 information_schema 大小写键名,取行内表名。"""
|
|
|
|
|
|
value = (
|
|
|
|
|
|
row.get("table_name")
|
|
|
|
|
|
or row.get("TABLE_NAME")
|
|
|
|
|
|
or row.get("Table_name")
|
|
|
|
|
|
or ""
|
|
|
|
|
|
)
|
|
|
|
|
|
return str(value).strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _filter_rows_by_tables(
|
|
|
|
|
|
rows: list[dict[str, Any]], allowed_tables: set[str]
|
|
|
|
|
|
) -> list[dict[str, Any]]:
|
|
|
|
|
|
return [row for row in rows if _row_table_name(row) in allowed_tables]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _delete_tables_outside_allowlist(
|
|
|
|
|
|
milvus_client, allowed_tables: set[str]
|
|
|
|
|
|
) -> list[str]:
|
|
|
|
|
|
"""删除集合中不在允许名单内的表元数据 chunk,返回被清理的表名。"""
|
|
|
|
|
|
existing_rows = await milvus_client.query(
|
|
|
|
|
|
collection_name=NL2SQL_COLLECTION,
|
|
|
|
|
|
filter="is_valid == true",
|
|
|
|
|
|
output_fields=["table_name"],
|
|
|
|
|
|
limit=16384,
|
|
|
|
|
|
)
|
|
|
|
|
|
existing_tables = {
|
|
|
|
|
|
str(row.get("table_name") or "").strip()
|
|
|
|
|
|
for row in existing_rows or []
|
|
|
|
|
|
if str(row.get("table_name") or "").strip()
|
|
|
|
|
|
}
|
|
|
|
|
|
removed: list[str] = []
|
|
|
|
|
|
for table_name in sorted(existing_tables - allowed_tables):
|
|
|
|
|
|
await milvus_client.delete(
|
|
|
|
|
|
collection_name=NL2SQL_COLLECTION,
|
|
|
|
|
|
filter=f'table_name == "{_escape_filter_value(table_name)}"',
|
|
|
|
|
|
)
|
|
|
|
|
|
removed.append(table_name)
|
|
|
|
|
|
return removed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_orm_metadata_rows(
|
|
|
|
|
|
tables: Iterable[Any],
|
|
|
|
|
|
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
|
|
|
|
"""把 SQLAlchemy Table 定义合成 information_schema 风格的元数据行。
|
|
|
|
|
|
|
|
|
|
|
|
用于 model/ 已定义、但数据库尚未建表的表:
|
|
|
|
|
|
- 表注释取 ORM ``__table_args__`` 的 comment(ORM 无则空串);
|
|
|
|
|
|
- 字段注释取列 comment(ORM 通常未标注,则为空串);
|
|
|
|
|
|
- 字段类型/可空性从 ORM 列定义推导。
|
|
|
|
|
|
"""
|
|
|
|
|
|
table_rows: list[dict[str, Any]] = []
|
|
|
|
|
|
column_rows: list[dict[str, Any]] = []
|
|
|
|
|
|
for table in tables:
|
|
|
|
|
|
table_rows.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"TABLE_NAME": str(table.name).strip(),
|
|
|
|
|
|
"TABLE_COMMENT": str(table.comment or "").strip(),
|
|
|
|
|
|
"TABLE_TYPE": "BASE TABLE",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
for position, column in enumerate(table.columns, start=1):
|
|
|
|
|
|
column_rows.append(
|
|
|
|
|
|
{
|
|
|
|
|
|
"TABLE_NAME": str(table.name).strip(),
|
|
|
|
|
|
"COLUMN_NAME": str(column.name).strip(),
|
|
|
|
|
|
"COLUMN_COMMENT": str(column.comment or "").strip(),
|
|
|
|
|
|
"DATA_TYPE": str(column.type).strip().lower(),
|
|
|
|
|
|
"IS_NULLABLE": "YES" if column.nullable else "NO",
|
|
|
|
|
|
"ORDINAL_POSITION": position,
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
return table_rows, column_rows
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def merge_orm_metadata_rows(
|
|
|
|
|
|
table_rows: list[dict[str, Any]],
|
|
|
|
|
|
column_rows: list[dict[str, Any]],
|
|
|
|
|
|
*,
|
|
|
|
|
|
allowed_tables: set[str],
|
|
|
|
|
|
missing_table_objects: Iterable[Any] = (),
|
|
|
|
|
|
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
|
|
|
|
"""以 model/ ORM 全量为名单主体合并元数据行。
|
|
|
|
|
|
|
|
|
|
|
|
- 库里真实存在的 ORM 表:沿用 information_schema 行(列注释更全);
|
|
|
|
|
|
- 库里没有的 ORM 表(missing_table_objects):用 ORM 定义合成行,
|
|
|
|
|
|
并在表说明追加 MISSING_TABLE_MARKER 标注"尚未建表";
|
|
|
|
|
|
- 名单之外(DB-only)的行在此丢弃,交由 sync_metadata 从 Milvus 清理。
|
|
|
|
|
|
"""
|
|
|
|
|
|
allowed = {str(name).strip() for name in allowed_tables if str(name).strip()}
|
|
|
|
|
|
merged_tables = [row for row in table_rows if _row_table_name(row) in allowed]
|
|
|
|
|
|
merged_columns = [row for row in column_rows if _row_table_name(row) in allowed]
|
|
|
|
|
|
|
|
|
|
|
|
missing_table_rows, missing_column_rows = build_orm_metadata_rows(
|
|
|
|
|
|
missing_table_objects
|
|
|
|
|
|
)
|
|
|
|
|
|
for row in missing_table_rows:
|
|
|
|
|
|
comment = str(row.get("TABLE_COMMENT") or "").strip()
|
|
|
|
|
|
row["TABLE_COMMENT"] = f"{comment}{MISSING_TABLE_MARKER}".strip()
|
|
|
|
|
|
merged_tables.extend(missing_table_rows)
|
|
|
|
|
|
merged_columns.extend(missing_column_rows)
|
|
|
|
|
|
return merged_tables, merged_columns
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-13 16:19:24 +08:00
|
|
|
|
async def prepare_metadata_rows(
|
|
|
|
|
|
chunks: list[dict[str, Any]],
|
|
|
|
|
|
*,
|
|
|
|
|
|
embedder: Callable[[list[str]], Awaitable[list[list[float]]]] = embed_texts,
|
|
|
|
|
|
timestamp: int | None = None,
|
|
|
|
|
|
) -> list[dict[str, Any]]:
|
|
|
|
|
|
if not chunks:
|
|
|
|
|
|
return []
|
|
|
|
|
|
vectors = await embedder([chunk["text"] for chunk in chunks])
|
|
|
|
|
|
if len(vectors) != len(chunks):
|
|
|
|
|
|
raise ValueError("Embedding result count must match metadata chunk count")
|
|
|
|
|
|
created_at = int(time.time()) if timestamp is None else timestamp
|
|
|
|
|
|
return [
|
|
|
|
|
|
{**chunk, "vector": vector, "created_at": created_at}
|
|
|
|
|
|
for chunk, vector in zip(chunks, vectors)
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def sync_metadata(
|
|
|
|
|
|
milvus_client,
|
|
|
|
|
|
table_rows: list[dict[str, Any]],
|
|
|
|
|
|
column_rows: list[dict[str, Any]],
|
|
|
|
|
|
*,
|
|
|
|
|
|
embedder: Callable[[list[str]], Awaitable[list[list[float]]]] = embed_texts,
|
|
|
|
|
|
timestamp: int | None = None,
|
2026-09-13 20:48:42 +08:00
|
|
|
|
allowed_tables: set[str] | None = None,
|
2026-09-13 16:19:24 +08:00
|
|
|
|
) -> int:
|
2026-09-13 20:48:42 +08:00
|
|
|
|
"""构造并 upsert 元数据 chunk。
|
|
|
|
|
|
|
|
|
|
|
|
allowed_tables 提供时:仅同步名单内的表,并删除集合中名单之外的
|
|
|
|
|
|
表元数据( Milvus 里只保留主数据源认可的表,如 model/ ORM 覆盖的表)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if allowed_tables is not None:
|
|
|
|
|
|
allowed = {str(name).strip() for name in allowed_tables if str(name).strip()}
|
|
|
|
|
|
table_rows = _filter_rows_by_tables(table_rows, allowed)
|
|
|
|
|
|
column_rows = _filter_rows_by_tables(column_rows, allowed)
|
|
|
|
|
|
|
2026-09-13 16:19:24 +08:00
|
|
|
|
chunks = build_metadata_chunks(table_rows, column_rows)
|
|
|
|
|
|
chunks_by_table: dict[str, list[dict[str, Any]]] = {}
|
|
|
|
|
|
for chunk in chunks:
|
|
|
|
|
|
chunks_by_table.setdefault(chunk["table_name"], []).append(chunk)
|
|
|
|
|
|
|
|
|
|
|
|
updated_count = 0
|
|
|
|
|
|
for table_name, table_chunks in chunks_by_table.items():
|
|
|
|
|
|
escaped_name = _escape_filter_value(table_name)
|
|
|
|
|
|
existing_rows = await milvus_client.query(
|
|
|
|
|
|
collection_name=NL2SQL_COLLECTION,
|
|
|
|
|
|
filter=f'table_name == "{escaped_name}"',
|
|
|
|
|
|
output_fields=["id", "content_hash", "is_valid", "is_deprecated"],
|
|
|
|
|
|
)
|
|
|
|
|
|
desired_signature = {
|
|
|
|
|
|
(chunk["id"], chunk["content_hash"], chunk["is_valid"], chunk["is_deprecated"])
|
|
|
|
|
|
for chunk in table_chunks
|
|
|
|
|
|
}
|
|
|
|
|
|
existing_signature = {
|
|
|
|
|
|
(
|
|
|
|
|
|
row.get("id"),
|
|
|
|
|
|
row.get("content_hash"),
|
|
|
|
|
|
row.get("is_valid", True),
|
|
|
|
|
|
row.get("is_deprecated", False),
|
|
|
|
|
|
)
|
|
|
|
|
|
for row in existing_rows or []
|
|
|
|
|
|
}
|
|
|
|
|
|
if desired_signature == existing_signature:
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
if existing_rows:
|
|
|
|
|
|
await milvus_client.delete(
|
|
|
|
|
|
collection_name=NL2SQL_COLLECTION,
|
|
|
|
|
|
filter=f'table_name == "{escaped_name}"',
|
|
|
|
|
|
)
|
|
|
|
|
|
rows = await prepare_metadata_rows(
|
|
|
|
|
|
table_chunks, embedder=embedder, timestamp=timestamp
|
|
|
|
|
|
)
|
|
|
|
|
|
if rows:
|
|
|
|
|
|
await milvus_client.upsert(collection_name=NL2SQL_COLLECTION, data=rows)
|
|
|
|
|
|
updated_count += len(rows)
|
2026-09-13 20:48:42 +08:00
|
|
|
|
|
|
|
|
|
|
if allowed_tables is not None:
|
|
|
|
|
|
await _delete_tables_outside_allowlist(milvus_client, allowed)
|
2026-09-13 16:19:24 +08:00
|
|
|
|
return updated_count
|