если объект найден по реквизиту (колонка в FTS), в выдаче показывается только совпавший реквизит с типом значения — в том же формате дерева
This commit is contained in:
+205
-24
@@ -3,12 +3,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .parse_meta import FieldRec, ObjectRec, parse_metadata_xml
|
||||
|
||||
_QUERY_TOKEN_RE = re.compile(r"[\wА-Яа-яЁё]+", flags=re.UNICODE)
|
||||
|
||||
|
||||
FILL_LABEL = {
|
||||
"ShowError": "обяз.",
|
||||
@@ -43,6 +46,174 @@ def _field_matches(f: dict[str, Any], q: str, qn: str) -> bool:
|
||||
return name_cf == qn or syn_cf == qn or qn in syn_parts
|
||||
|
||||
|
||||
def _query_tokens(query: str) -> list[str]:
|
||||
return _QUERY_TOKEN_RE.findall(query or "")
|
||||
|
||||
|
||||
def _words_cf(text: str) -> list[str]:
|
||||
return [w.casefold() for w in _QUERY_TOKEN_RE.findall(text or "")]
|
||||
|
||||
|
||||
def _tokens_match_text(tokens: list[str], text: str) -> bool:
|
||||
"""Как FTS search: каждый токен — префикс какого-либо слова в тексте."""
|
||||
if not tokens:
|
||||
return False
|
||||
words = _words_cf(text)
|
||||
if not words:
|
||||
return False
|
||||
for tok in tokens:
|
||||
t = tok.casefold()
|
||||
if not any(w.startswith(t) for w in words):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def object_header_matches_search(row: dict[str, Any], query: str) -> bool:
|
||||
"""Совпадение по имени/синониму/комментарию объекта (не по реквизитам)."""
|
||||
tokens = _query_tokens(query)
|
||||
if not tokens:
|
||||
return False
|
||||
for part in (
|
||||
row.get("name") or "",
|
||||
row.get("full_name") or "",
|
||||
row.get("synonym") or "",
|
||||
row.get("comment") or "",
|
||||
):
|
||||
if _tokens_match_text(tokens, part):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def field_matches_search_query(f: dict[str, Any], query: str) -> bool:
|
||||
"""Совпадение реквизита с поисковым запросом (префикс по словам, как FTS)."""
|
||||
tokens = _query_tokens(query)
|
||||
if not tokens:
|
||||
return False
|
||||
text = " ".join(
|
||||
filter(
|
||||
None,
|
||||
[
|
||||
f.get("kind") or "",
|
||||
f.get("name") or "",
|
||||
f.get("synonym") or "",
|
||||
f.get("comment") or "",
|
||||
f.get("tabular_section") or "",
|
||||
" ".join(f.get("types") or []),
|
||||
],
|
||||
)
|
||||
)
|
||||
return _tokens_match_text(tokens, text)
|
||||
|
||||
|
||||
def _field_dict_from_db_row(row: sqlite3.Row) -> dict[str, Any]:
|
||||
return {
|
||||
"kind": row["kind"],
|
||||
"name": row["name"],
|
||||
"synonym": row["synonym"] or "",
|
||||
"comment": row["comment"] or "" if "comment" in row.keys() else "",
|
||||
"types": [t.strip() for t in (row["types"] or "").split(",") if t.strip()],
|
||||
"tabular_section": row["tabular_section"] or "",
|
||||
"refs": json.loads(row["refs_json"] or "[]"),
|
||||
"fill_checking": "",
|
||||
}
|
||||
|
||||
|
||||
def find_fields_for_search_query(
|
||||
conn: sqlite3.Connection,
|
||||
object_id: int,
|
||||
query: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Реквизиты объекта, совпавшие с FTS-запросом search."""
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT kind, name, synonym, comment, types, tabular_section, refs_json
|
||||
FROM fields
|
||||
WHERE object_id = ?
|
||||
ORDER BY tabular_section, kind, name
|
||||
""",
|
||||
(object_id,),
|
||||
)
|
||||
matched: list[dict[str, Any]] = []
|
||||
seen: set[tuple[str, str, str]] = set()
|
||||
for row in rows:
|
||||
fdict = _field_dict_from_db_row(row)
|
||||
if not field_matches_search_query(fdict, query):
|
||||
continue
|
||||
sig = (fdict["tabular_section"], fdict["name"], fdict["kind"])
|
||||
if sig in seen:
|
||||
continue
|
||||
seen.add(sig)
|
||||
matched.append(fdict)
|
||||
return matched
|
||||
|
||||
|
||||
def enrich_matched_fields(obj: dict[str, Any], matched: list[dict[str, Any]]) -> None:
|
||||
"""Подтянуть FillChecking и типы из актуальной структуры объекта."""
|
||||
if not matched or not obj.get("fields"):
|
||||
return
|
||||
by_f = {
|
||||
(f.get("tabular_section") or "", f.get("name") or "", f.get("kind") or ""): f
|
||||
for f in obj["fields"]
|
||||
}
|
||||
for mf in matched:
|
||||
key = (mf.get("tabular_section") or "", mf.get("name") or "", mf.get("kind") or "")
|
||||
src = by_f.get(key)
|
||||
if not src:
|
||||
continue
|
||||
mf["fill_checking"] = src.get("fill_checking") or ""
|
||||
mf["types"] = src.get("types") or mf.get("types")
|
||||
mf["synonym"] = src.get("synonym") or mf.get("synonym")
|
||||
mf["refs"] = src.get("refs") or mf.get("refs")
|
||||
|
||||
|
||||
def load_object_structure_for_row(
|
||||
conn: sqlite3.Connection,
|
||||
row: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
match = {
|
||||
"config": row["config"],
|
||||
"full_name": row["full_name"],
|
||||
"path": row.get("path"),
|
||||
"object_type": row.get("object_type"),
|
||||
}
|
||||
if row.get("json"):
|
||||
match["json"] = row["json"]
|
||||
elif row.get("object_id"):
|
||||
jrow = conn.execute(
|
||||
"SELECT json FROM objects WHERE id=?", (row["object_id"],)
|
||||
).fetchone()
|
||||
match["json"] = jrow["json"] if jrow else "{}"
|
||||
else:
|
||||
jrow = conn.execute(
|
||||
"SELECT json FROM objects WHERE config=? AND full_name=?",
|
||||
(row["config"], row["full_name"]),
|
||||
).fetchone()
|
||||
match["json"] = jrow["json"] if jrow else "{}"
|
||||
return load_object_structure(match)
|
||||
|
||||
|
||||
def annotate_search_hits(
|
||||
conn: sqlite3.Connection,
|
||||
rows: list[dict[str, Any]],
|
||||
query: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Уточнить search: object — по шапке; field — по реквизитам в fields_text."""
|
||||
for row in rows:
|
||||
if object_header_matches_search(row, query):
|
||||
row["match_kind"] = "object"
|
||||
row["matched_fields"] = []
|
||||
continue
|
||||
oid = row.get("object_id")
|
||||
matched = find_fields_for_search_query(conn, int(oid), query) if oid else []
|
||||
if matched:
|
||||
row["match_kind"] = "field"
|
||||
row["matched_fields"] = matched
|
||||
else:
|
||||
row["match_kind"] = "object"
|
||||
row["matched_fields"] = []
|
||||
return rows
|
||||
|
||||
|
||||
def find_structure_matches(
|
||||
conn: sqlite3.Connection,
|
||||
query: str,
|
||||
@@ -239,12 +410,43 @@ def format_field_line(
|
||||
return f"{indent}{branch}`{kind}.{name}`{syn_s} [{fill}] : {types_s}"
|
||||
|
||||
|
||||
def _select_matched_field_nodes(
|
||||
fields: list[dict[str, Any]],
|
||||
matched_fields: list[dict[str, Any]],
|
||||
) -> tuple[list[dict[str, Any]], set[tuple[str, str, str]], set[str]]:
|
||||
want: set[tuple[str, str, str]] = set()
|
||||
need_ts: set[str] = set()
|
||||
for mf in matched_fields:
|
||||
want.add(
|
||||
(
|
||||
mf.get("kind") or "",
|
||||
mf.get("tabular_section") or "",
|
||||
mf.get("name") or "",
|
||||
)
|
||||
)
|
||||
if mf.get("tabular_section") and mf.get("kind") == "Column":
|
||||
need_ts.add(mf["tabular_section"])
|
||||
selected: list[dict[str, Any]] = []
|
||||
for f in fields:
|
||||
key = (
|
||||
f.get("kind") or "",
|
||||
f.get("tabular_section") or "",
|
||||
f.get("name") or "",
|
||||
)
|
||||
if key in want:
|
||||
selected.append(f)
|
||||
elif f.get("kind") == "TabularSection" and f.get("name") in need_ts:
|
||||
selected.append(f)
|
||||
return selected, want, need_ts
|
||||
|
||||
|
||||
def format_object_tree(
|
||||
obj: dict[str, Any],
|
||||
*,
|
||||
clear: bool = False,
|
||||
matched_fields: list[dict[str, Any]] | None = None,
|
||||
match_kind: str = "object",
|
||||
omit_header: bool = False,
|
||||
) -> str:
|
||||
"""Дерево реквизитов объекта. clear — только совпадения."""
|
||||
lines: list[str] = []
|
||||
@@ -254,35 +456,14 @@ def format_object_tree(
|
||||
header = f"`{obj.get('full_name') or obj.get('object_type') + '.' + obj.get('name')}`{syn_s}"
|
||||
if cfg:
|
||||
header = f"[{cfg}] {header}"
|
||||
lines.append(header)
|
||||
if not omit_header:
|
||||
lines.append(header)
|
||||
|
||||
fields: list[dict[str, Any]] = list(obj.get("fields") or [])
|
||||
|
||||
if clear:
|
||||
if match_kind == "field" and matched_fields:
|
||||
want: set[tuple[str, str, str]] = set()
|
||||
need_ts: set[str] = set()
|
||||
for mf in matched_fields:
|
||||
want.add(
|
||||
(
|
||||
mf.get("kind") or "",
|
||||
mf.get("tabular_section") or "",
|
||||
mf.get("name") or "",
|
||||
)
|
||||
)
|
||||
if mf.get("tabular_section") and mf.get("kind") == "Column":
|
||||
need_ts.add(mf["tabular_section"])
|
||||
selected: list[dict[str, Any]] = []
|
||||
for f in fields:
|
||||
key = (
|
||||
f.get("kind") or "",
|
||||
f.get("tabular_section") or "",
|
||||
f.get("name") or "",
|
||||
)
|
||||
if key in want:
|
||||
selected.append(f)
|
||||
elif f.get("kind") == "TabularSection" and f.get("name") in need_ts:
|
||||
selected.append(f)
|
||||
selected, want, need_ts = _select_matched_field_nodes(fields, matched_fields)
|
||||
if not selected:
|
||||
lines.append(" (совпавшие реквизиты не найдены в структуре)")
|
||||
return "\n".join(lines)
|
||||
|
||||
Reference in New Issue
Block a user