"""Mining dei join reali dall'SQL approvato: coppie equi-join -> FK logiche candidate. La fonte di verita' sono le query gia' validate da un umano (sql_final.sql, ctes/*.sql delle sessioni approvate): un equi-join ricorrente tra due tabelle del catalogo, con una delle due colonne PK della propria tabella, e' una FK logica ad alta confidenza. """ from collections import Counter import sqlglot from sqlglot import exp from tht.mschema.models import PhysicalSchema JoinPair = tuple[str, str, str, str] # (src_table, src_col, ref_table, ref_col) def mine_join_pairs(sql_text: str, physical: PhysicalSchema) -> Counter: """Estrae le coppie equi-join tra tabelle del catalogo da un testo SQL. Ritorna un Counter {(src_table, src_col, ref_table, ref_col): occorrenze}. Il lato ref e' quello la cui colonna e' PK della propria tabella; coppie in cui nessuno o entrambi i lati sono PK vengono scartate (non FK-like). Alias e CTE vengono risolti; i riferimenti a CTE (non nel catalogo) sono ignorati. """ pairs: Counter = Counter() try: statements = sqlglot.parse(sql_text, read="postgres") except sqlglot.errors.ParseError: return pairs for stmt in statements: if stmt is None: continue alias_map: dict[str, str] = {} for t in stmt.find_all(exp.Table): alias_map[t.alias_or_name] = t.name for eq in stmt.find_all(exp.EQ): left, right = eq.left, eq.right if not (isinstance(left, exp.Column) and isinstance(right, exp.Column)): continue if not (left.table and right.table): continue lt = alias_map.get(left.table, left.table) rt = alias_map.get(right.table, right.table) if lt == rt or lt not in physical.tables or rt not in physical.tables: continue lc, rc = left.name, right.name if lc not in physical.tables[lt].columns or rc not in physical.tables[rt].columns: continue l_pk = physical.tables[lt].columns[lc].pk r_pk = physical.tables[rt].columns[rc].pk if l_pk == r_pk: # nessuna o entrambe PK: non FK-like continue if r_pk: pairs[(lt, lc, rt, rc)] += 1 else: pairs[(rt, rc, lt, lc)] += 1 return pairs