pretensor 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pretensor/__init__.py +50 -0
- pretensor/benchmark/__init__.py +54 -0
- pretensor/benchmark/cli.py +294 -0
- pretensor/benchmark/fixtures.py +84 -0
- pretensor/benchmark/l1/__init__.py +23 -0
- pretensor/benchmark/l1/metrics.py +141 -0
- pretensor/benchmark/l1/pipeline.py +188 -0
- pretensor/benchmark/l1/runner.py +245 -0
- pretensor/benchmark/l2/__init__.py +27 -0
- pretensor/benchmark/l2/gold.py +236 -0
- pretensor/benchmark/l2/metrics.py +146 -0
- pretensor/benchmark/l2/pipeline.py +124 -0
- pretensor/benchmark/l2/runner.py +530 -0
- pretensor/benchmark/l3/__init__.py +73 -0
- pretensor/benchmark/l3/agent.py +316 -0
- pretensor/benchmark/l3/db.py +188 -0
- pretensor/benchmark/l3/gold.py +85 -0
- pretensor/benchmark/l3/llm_client.py +395 -0
- pretensor/benchmark/l3/mcp_client.py +357 -0
- pretensor/benchmark/l3/pretensor_runner.py +456 -0
- pretensor/benchmark/l3/prompt.py +132 -0
- pretensor/benchmark/l3/runner.py +358 -0
- pretensor/benchmark/l3/sql_equivalence.py +176 -0
- pretensor/benchmark/release_gate.py +448 -0
- pretensor/benchmark/results.py +298 -0
- pretensor/benchmark/runner.py +109 -0
- pretensor/cli/__init__.py +1 -0
- pretensor/cli/commands/_source_runner.py +147 -0
- pretensor/cli/commands/analyze.py +201 -0
- pretensor/cli/commands/connections/__init__.py +7 -0
- pretensor/cli/commands/connections/add_remove.py +126 -0
- pretensor/cli/commands/connections/register.py +12 -0
- pretensor/cli/commands/export.py +131 -0
- pretensor/cli/commands/index.py +559 -0
- pretensor/cli/commands/list.py +76 -0
- pretensor/cli/commands/quickstart.py +207 -0
- pretensor/cli/commands/reindex.py +646 -0
- pretensor/cli/commands/semantic.py +190 -0
- pretensor/cli/commands/serve.py +144 -0
- pretensor/cli/commands/sync_grants.py +149 -0
- pretensor/cli/commands/validate.py +176 -0
- pretensor/cli/config_file.py +442 -0
- pretensor/cli/constants.py +10 -0
- pretensor/cli/dbt_enrichment.py +96 -0
- pretensor/cli/main.py +109 -0
- pretensor/cli/paths.py +43 -0
- pretensor/cli/plugin.py +52 -0
- pretensor/config.py +226 -0
- pretensor/connectors/__init__.py +29 -0
- pretensor/connectors/base.py +165 -0
- pretensor/connectors/bigquery.py +468 -0
- pretensor/connectors/inspect.py +321 -0
- pretensor/connectors/lineage_sqlglot.py +97 -0
- pretensor/connectors/models.py +130 -0
- pretensor/connectors/mysql.py +402 -0
- pretensor/connectors/pg_array_parse.py +53 -0
- pretensor/connectors/postgres.py +938 -0
- pretensor/connectors/registry.py +93 -0
- pretensor/connectors/snapshot.py +244 -0
- pretensor/connectors/snowflake.py +908 -0
- pretensor/core/__init__.py +1 -0
- pretensor/core/builder.py +307 -0
- pretensor/core/dsn_crypto.py +51 -0
- pretensor/core/graph_schema_manager.py +246 -0
- pretensor/core/graph_store.py +1226 -0
- pretensor/core/ids.py +101 -0
- pretensor/core/portable_export.py +276 -0
- pretensor/core/query_runner.py +67 -0
- pretensor/core/registry.py +209 -0
- pretensor/core/schema.py +473 -0
- pretensor/core/secure_io.py +93 -0
- pretensor/core/store.py +469 -0
- pretensor/enrichment/__init__.py +1 -0
- pretensor/enrichment/analyze/__init__.py +0 -0
- pretensor/enrichment/analyze/classify.py +49 -0
- pretensor/enrichment/analyze/extract_python.py +196 -0
- pretensor/enrichment/analyze/parse.py +141 -0
- pretensor/enrichment/analyze/pipeline.py +195 -0
- pretensor/enrichment/analyze/summary.py +38 -0
- pretensor/enrichment/analyze/walker.py +98 -0
- pretensor/enrichment/analyze/writers.py +214 -0
- pretensor/enrichment/dbt/__init__.py +30 -0
- pretensor/enrichment/dbt/lineage.py +100 -0
- pretensor/enrichment/dbt/manifest.py +300 -0
- pretensor/enrichment/dbt/metadata.py +263 -0
- pretensor/enrichment/dbt/pipeline.py +77 -0
- pretensor/enrichment/dbt/resolution.py +101 -0
- pretensor/enrichment/dbt/signals.py +305 -0
- pretensor/entities/__init__.py +27 -0
- pretensor/entities/builder.py +63 -0
- pretensor/entities/classifier.py +383 -0
- pretensor/entities/llm_extract.py +66 -0
- pretensor/errors.py +35 -0
- pretensor/graph_models/__init__.py +17 -0
- pretensor/graph_models/base.py +11 -0
- pretensor/graph_models/consumer.py +71 -0
- pretensor/graph_models/edge.py +35 -0
- pretensor/graph_models/entity.py +21 -0
- pretensor/graph_models/node.py +79 -0
- pretensor/graph_models/relationship.py +33 -0
- pretensor/integrations/__init__.py +42 -0
- pretensor/integrations/_base.py +138 -0
- pretensor/integrations/google_adk.py +49 -0
- pretensor/integrations/langchain.py +55 -0
- pretensor/integrations/llamaindex.py +53 -0
- pretensor/intelligence/__init__.py +33 -0
- pretensor/intelligence/cluster_labeler.py +425 -0
- pretensor/intelligence/clustering.py +168 -0
- pretensor/intelligence/combining.py +32 -0
- pretensor/intelligence/discovery.py +114 -0
- pretensor/intelligence/embeddings.py +317 -0
- pretensor/intelligence/graph_export.py +200 -0
- pretensor/intelligence/heuristic.py +544 -0
- pretensor/intelligence/join_paths/__init__.py +130 -0
- pretensor/intelligence/join_paths/on_demand.py +516 -0
- pretensor/intelligence/join_paths/storage.py +70 -0
- pretensor/intelligence/llm_infer.py +78 -0
- pretensor/intelligence/llm_runtime.py +62 -0
- pretensor/intelligence/metric_templates.py +193 -0
- pretensor/intelligence/pipeline.py +364 -0
- pretensor/intelligence/role_exemplars.py +263 -0
- pretensor/intelligence/schema_classification.py +360 -0
- pretensor/intelligence/scoring.py +76 -0
- pretensor/intelligence/semantic.py +240 -0
- pretensor/intelligence/shadow_alias.py +101 -0
- pretensor/intelligence/statistical.py +50 -0
- pretensor/intelligence/steps.py +191 -0
- pretensor/intelligence/steps_embedding.py +168 -0
- pretensor/introspection/__init__.py +6 -0
- pretensor/introspection/inspector.py +5 -0
- pretensor/introspection/models/__init__.py +0 -0
- pretensor/introspection/models/base.py +5 -0
- pretensor/introspection/models/config.py +237 -0
- pretensor/introspection/models/dsn.py +550 -0
- pretensor/introspection/models/plan.py +116 -0
- pretensor/introspection/models/schema.py +10 -0
- pretensor/introspection/models/semantic.py +121 -0
- pretensor/introspection/models/validation.py +116 -0
- pretensor/introspection/snapshot.py +46 -0
- pretensor/mcp/__init__.py +16 -0
- pretensor/mcp/config_json.py +24 -0
- pretensor/mcp/payload_types.py +274 -0
- pretensor/mcp/resources/__init__.py +17 -0
- pretensor/mcp/resources/markdown.py +314 -0
- pretensor/mcp/server.py +285 -0
- pretensor/mcp/service.py +49 -0
- pretensor/mcp/service_context.py +142 -0
- pretensor/mcp/service_registry.py +294 -0
- pretensor/mcp/store_cache.py +43 -0
- pretensor/mcp/tool_registry.py +136 -0
- pretensor/mcp/tools/__init__.py +1 -0
- pretensor/mcp/tools/_rank.py +244 -0
- pretensor/mcp/tools/_timed.py +26 -0
- pretensor/mcp/tools/compile_metric.py +144 -0
- pretensor/mcp/tools/consumers.py +161 -0
- pretensor/mcp/tools/context.py +1121 -0
- pretensor/mcp/tools/cypher.py +509 -0
- pretensor/mcp/tools/detect_changes.py +254 -0
- pretensor/mcp/tools/impact.py +271 -0
- pretensor/mcp/tools/list.py +131 -0
- pretensor/mcp/tools/schema.py +170 -0
- pretensor/mcp/tools/search.py +316 -0
- pretensor/mcp/tools/semantic_search.py +282 -0
- pretensor/mcp/tools/traverse.py +1027 -0
- pretensor/mcp/tools/validate_sql.py +150 -0
- pretensor/observability.py +203 -0
- pretensor/py.typed +0 -0
- pretensor/quickstart/README.md +29 -0
- pretensor/quickstart/__init__.py +6 -0
- pretensor/quickstart/docker-compose.yml +18 -0
- pretensor/quickstart/pagila_data.sql +63 -0
- pretensor/quickstart/pagila_ddl.sql +92 -0
- pretensor/search/__init__.py +6 -0
- pretensor/search/base.py +80 -0
- pretensor/search/index.py +435 -0
- pretensor/semantic/__init__.py +24 -0
- pretensor/semantic/base.py +123 -0
- pretensor/semantic/compiler.py +487 -0
- pretensor/semantic/yaml_layer.py +180 -0
- pretensor/skills/__init__.py +5 -0
- pretensor/skills/generator.py +235 -0
- pretensor/staleness/__init__.py +15 -0
- pretensor/staleness/graph_patcher.py +355 -0
- pretensor/staleness/impact_analyzer.py +162 -0
- pretensor/staleness/snapshot_store.py +38 -0
- pretensor/validation/__init__.py +9 -0
- pretensor/validation/query_validator.py +436 -0
- pretensor/visibility/__init__.py +23 -0
- pretensor/visibility/config.py +126 -0
- pretensor/visibility/filter.py +143 -0
- pretensor/visibility/kuzu_helpers.py +32 -0
- pretensor/visibility/runtime.py +36 -0
- pretensor/visibility/sync_grants.py +188 -0
- pretensor-0.1.0.dist-info/METADATA +251 -0
- pretensor-0.1.0.dist-info/RECORD +198 -0
- pretensor-0.1.0.dist-info/WHEEL +4 -0
- pretensor-0.1.0.dist-info/entry_points.txt +2 -0
- pretensor-0.1.0.dist-info/licenses/LICENSE +21 -0
pretensor/core/store.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"""Kuzu-backed persistence for the schema graph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import kuzu
|
|
11
|
+
|
|
12
|
+
from pretensor.core.graph_schema_manager import (
|
|
13
|
+
_SCHEMA_TABLE_EMBEDDING_DIM, # noqa: F401 – re-exported for existing callers
|
|
14
|
+
GraphSchemaManager,
|
|
15
|
+
)
|
|
16
|
+
from pretensor.core.graph_store import GraphStore, TableEmbeddingRow
|
|
17
|
+
from pretensor.core.query_runner import QueryRunner
|
|
18
|
+
from pretensor.graph_models.consumer import ConsumesEdge, ExternalConsumerNode
|
|
19
|
+
from pretensor.graph_models.edge import GraphEdge, LineageEdge
|
|
20
|
+
from pretensor.graph_models.entity import EntityNode
|
|
21
|
+
from pretensor.graph_models.node import GraphNode
|
|
22
|
+
from pretensor.graph_models.relationship import RelationshipCandidate
|
|
23
|
+
|
|
24
|
+
__all__ = ["KuzuStore", "TableEmbeddingRow"]
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class KuzuStore:
|
|
30
|
+
"""Open or create a Kuzu database file and upsert schema graph nodes/edges.
|
|
31
|
+
|
|
32
|
+
Facade that composes :class:`~pretensor.core.query_runner.QueryRunner`,
|
|
33
|
+
:class:`~pretensor.core.graph_store.GraphStore`, and
|
|
34
|
+
:class:`~pretensor.core.graph_schema_manager.GraphSchemaManager`.
|
|
35
|
+
All public methods preserve their original signatures and behavior.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, database_path: Path, *, read_only: bool = False) -> None:
|
|
39
|
+
self._path = database_path
|
|
40
|
+
# ``read_only`` is enforced by the Kuzu engine itself (write statements
|
|
41
|
+
# raise); it is not re-stored as an attribute because nothing downstream
|
|
42
|
+
# needs to branch on it.
|
|
43
|
+
self._db = kuzu.Database(str(database_path), read_only=read_only)
|
|
44
|
+
self._conn = kuzu.Connection(self._db)
|
|
45
|
+
self._runner = QueryRunner(self._conn, self._path)
|
|
46
|
+
self._schema = GraphSchemaManager(self._conn)
|
|
47
|
+
self._graph = GraphStore(self._runner)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def path(self) -> Path:
|
|
51
|
+
return self._path
|
|
52
|
+
|
|
53
|
+
def close(self) -> None:
|
|
54
|
+
"""Close the underlying database handle."""
|
|
55
|
+
self._db.close()
|
|
56
|
+
|
|
57
|
+
def ensure_schema(self) -> None:
|
|
58
|
+
"""Create node and relationship tables if they do not exist."""
|
|
59
|
+
self._schema.ensure_schema()
|
|
60
|
+
|
|
61
|
+
# ── Execution methods (delegated to QueryRunner) ──────────────────────────
|
|
62
|
+
|
|
63
|
+
def execute_write(self, query: str, params: dict[str, Any] | None = None) -> None:
|
|
64
|
+
"""Execute a write (mutation) Cypher query. Use this for DELETE / SET operations."""
|
|
65
|
+
return self._runner.execute_write(query, params)
|
|
66
|
+
|
|
67
|
+
def execute(
|
|
68
|
+
self, cypher: str, parameters: dict[str, Any] | None = None
|
|
69
|
+
) -> kuzu.QueryResult | list[kuzu.QueryResult]:
|
|
70
|
+
"""Run a read query (or arbitrary Cypher) with optional parameters."""
|
|
71
|
+
return self._runner.execute(cypher, parameters)
|
|
72
|
+
|
|
73
|
+
def query_all_rows(
|
|
74
|
+
self, cypher: str, parameters: dict[str, Any] | None = None
|
|
75
|
+
) -> list[tuple[Any, ...]]:
|
|
76
|
+
"""Execute Cypher and materialize all result rows as tuples.
|
|
77
|
+
|
|
78
|
+
Keeps callers (MCP, search index) off raw ``kuzu.QueryResult`` handles
|
|
79
|
+
while still routing reads through :class:`KuzuStore`.
|
|
80
|
+
"""
|
|
81
|
+
return self._runner.query_all_rows(cypher, parameters)
|
|
82
|
+
|
|
83
|
+
# ── Graph methods (delegated to GraphStore) ───────────────────────────────
|
|
84
|
+
|
|
85
|
+
def clear_graph(self) -> None:
|
|
86
|
+
"""Remove all FK edges and table nodes (full re-index)."""
|
|
87
|
+
return self._graph.clear_graph()
|
|
88
|
+
|
|
89
|
+
def clear_connection_subgraph(self, connection_name: str) -> None:
|
|
90
|
+
"""Remove all nodes and edges scoped to one indexed connection (unified graph merge)."""
|
|
91
|
+
return self._graph.clear_connection_subgraph(connection_name)
|
|
92
|
+
|
|
93
|
+
def upsert_table(self, node: GraphNode) -> None:
|
|
94
|
+
"""Insert or update a single ``SchemaTable`` node."""
|
|
95
|
+
return self._graph.upsert_table(node)
|
|
96
|
+
|
|
97
|
+
def upsert_entity(self, node: EntityNode) -> None:
|
|
98
|
+
"""Insert or update a single ``Entity`` node."""
|
|
99
|
+
return self._graph.upsert_entity(node)
|
|
100
|
+
|
|
101
|
+
def upsert_represents(self, entity_node_id: str, table_node_id: str) -> None:
|
|
102
|
+
"""Link an entity to a table (idempotent; one edge per pair)."""
|
|
103
|
+
return self._graph.upsert_represents(entity_node_id, table_node_id)
|
|
104
|
+
|
|
105
|
+
def set_table_entity_type(self, table_node_id: str, entity_type: str) -> None:
|
|
106
|
+
"""Set the business entity label on a ``SchemaTable`` node."""
|
|
107
|
+
return self._graph.set_table_entity_type(table_node_id, entity_type)
|
|
108
|
+
|
|
109
|
+
def set_table_classification(
|
|
110
|
+
self,
|
|
111
|
+
table_node_id: str,
|
|
112
|
+
*,
|
|
113
|
+
role: str,
|
|
114
|
+
role_confidence: float,
|
|
115
|
+
classification_signals_json: str,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Persist classifier output on a ``SchemaTable`` node."""
|
|
118
|
+
return self._graph.set_table_classification(
|
|
119
|
+
table_node_id,
|
|
120
|
+
role=role,
|
|
121
|
+
role_confidence=role_confidence,
|
|
122
|
+
classification_signals_json=classification_signals_json,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def set_table_embedding(
|
|
126
|
+
self, table_node_id: str, vector: list[float] | None
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Set (or clear with ``None``) the embedding on a ``SchemaTable`` node."""
|
|
129
|
+
return self._graph.set_table_embedding(table_node_id, vector)
|
|
130
|
+
|
|
131
|
+
def has_any_table_embeddings(self) -> bool:
|
|
132
|
+
"""Return True iff any ``SchemaTable`` row carries a non-null embedding.
|
|
133
|
+
|
|
134
|
+
Cheap LIMIT-1 probe used by the builder to warn before a full
|
|
135
|
+
rebuild silently drops previously computed vectors.
|
|
136
|
+
"""
|
|
137
|
+
return self._graph.has_any_table_embeddings()
|
|
138
|
+
|
|
139
|
+
def iter_table_embeddings(
|
|
140
|
+
self,
|
|
141
|
+
*,
|
|
142
|
+
database: str | None = None,
|
|
143
|
+
cluster: str | None = None,
|
|
144
|
+
) -> Iterator[TableEmbeddingRow]:
|
|
145
|
+
"""Yield ``SchemaTable`` rows for cosine retrieval, honoring optional filters.
|
|
146
|
+
|
|
147
|
+
``database`` matches either ``SchemaTable.connection_name`` or
|
|
148
|
+
``SchemaTable.database`` (so callers may pass a registry connection
|
|
149
|
+
name or a logical database name, mirroring ``query.db``). ``cluster``
|
|
150
|
+
requires an ``IN_CLUSTER`` edge to a :class:`Cluster` with the given
|
|
151
|
+
``cluster_id``; when unset, the cluster membership is looked up via
|
|
152
|
+
``OPTIONAL MATCH`` and populated on the row when present.
|
|
153
|
+
|
|
154
|
+
Rows with ``embedding is None`` are still yielded so the caller can
|
|
155
|
+
detect the "no tables carry vectors" case and return the documented
|
|
156
|
+
``fallback_bm25`` envelope without a second store round-trip.
|
|
157
|
+
"""
|
|
158
|
+
return self._graph.iter_table_embeddings(database=database, cluster=cluster)
|
|
159
|
+
|
|
160
|
+
def upsert_fk_edge(self, edge: GraphEdge) -> None:
|
|
161
|
+
"""Insert a foreign-key relationship edge (assumes endpoints exist)."""
|
|
162
|
+
return self._graph.upsert_fk_edge(edge)
|
|
163
|
+
|
|
164
|
+
def merge_fk_edge(self, edge: GraphEdge) -> None:
|
|
165
|
+
"""Idempotent FK edge upsert by ``edge_id`` (avoids duplicates on reindex)."""
|
|
166
|
+
return self._graph.merge_fk_edge(edge)
|
|
167
|
+
|
|
168
|
+
def upsert_lineage_edge(self, edge: LineageEdge) -> None:
|
|
169
|
+
"""Insert or update a ``LINEAGE`` edge (idempotent by ``edge_id``)."""
|
|
170
|
+
return self._graph.upsert_lineage_edge(edge)
|
|
171
|
+
|
|
172
|
+
def clear_lineage_edges(self, connection_name: str) -> None:
|
|
173
|
+
"""Remove all ``LINEAGE`` edges touching tables on one connection."""
|
|
174
|
+
return self._graph.clear_lineage_edges(connection_name)
|
|
175
|
+
|
|
176
|
+
def clear_dbt_model_dependency_lineage(self, connection_name: str) -> None:
|
|
177
|
+
"""Remove dbt ``parent_map`` lineage edges for one connection (see enrichment dbt)."""
|
|
178
|
+
return self._graph.clear_dbt_model_dependency_lineage(connection_name)
|
|
179
|
+
|
|
180
|
+
def clear_intelligence_artifacts(self) -> None:
|
|
181
|
+
"""Remove clusters and precomputed join paths (keeps tables and FK edges)."""
|
|
182
|
+
return self._graph.clear_intelligence_artifacts()
|
|
183
|
+
|
|
184
|
+
def upsert_external_consumer(self, node: ExternalConsumerNode) -> None:
|
|
185
|
+
"""Insert or update an ``ExternalConsumer`` node (idempotent by ``node_id``)."""
|
|
186
|
+
return self._graph.upsert_external_consumer(node)
|
|
187
|
+
|
|
188
|
+
def upsert_consumes_edge(self, edge: ConsumesEdge) -> None:
|
|
189
|
+
"""Insert or update a ``CONSUMES`` edge (idempotent by ``edge_id``)."""
|
|
190
|
+
return self._graph.upsert_consumes_edge(edge)
|
|
191
|
+
|
|
192
|
+
def mark_tables_have_external_consumers(self, table_node_ids: list[str]) -> None:
|
|
193
|
+
"""Set ``has_external_consumers = true`` on the given ``SchemaTable`` nodes."""
|
|
194
|
+
return self._graph.mark_tables_have_external_consumers(table_node_ids)
|
|
195
|
+
|
|
196
|
+
def sweep_stale_consumers(
|
|
197
|
+
self, service_name: str, connection_name: str, current_scan_run_id: str
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Delete stale consumers/edges from prior runs of one service and clear
|
|
200
|
+
``has_external_consumers`` on tables that lost their last consumer."""
|
|
201
|
+
return self._graph.sweep_stale_consumers(
|
|
202
|
+
service_name, connection_name, current_scan_run_id
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def upsert_column_for_table(
|
|
206
|
+
self,
|
|
207
|
+
*,
|
|
208
|
+
column_node_id: str,
|
|
209
|
+
connection_name: str,
|
|
210
|
+
database: str,
|
|
211
|
+
schema_name: str,
|
|
212
|
+
table_name: str,
|
|
213
|
+
column_name: str,
|
|
214
|
+
data_type: str,
|
|
215
|
+
nullable: bool,
|
|
216
|
+
is_primary_key: bool,
|
|
217
|
+
is_foreign_key: bool,
|
|
218
|
+
table_node_id: str,
|
|
219
|
+
comment: str | None = None,
|
|
220
|
+
description: str | None = None,
|
|
221
|
+
default_value: str | None = None,
|
|
222
|
+
is_indexed: bool = False,
|
|
223
|
+
check_constraints: list[str] | None = None,
|
|
224
|
+
ordinal_position: int | None = None,
|
|
225
|
+
most_common_values_json: str | None = None,
|
|
226
|
+
histogram_bounds_json: str | None = None,
|
|
227
|
+
stats_correlation: float | None = None,
|
|
228
|
+
column_cardinality: int | None = None,
|
|
229
|
+
index_type: str | None = None,
|
|
230
|
+
index_is_unique: bool | None = None,
|
|
231
|
+
parent_column_id: str | None = None,
|
|
232
|
+
is_array: bool = False,
|
|
233
|
+
) -> None:
|
|
234
|
+
"""Insert or update a ``SchemaColumn`` and link it to table or parent column."""
|
|
235
|
+
return self._graph.upsert_column_for_table(
|
|
236
|
+
column_node_id=column_node_id,
|
|
237
|
+
connection_name=connection_name,
|
|
238
|
+
database=database,
|
|
239
|
+
schema_name=schema_name,
|
|
240
|
+
table_name=table_name,
|
|
241
|
+
column_name=column_name,
|
|
242
|
+
data_type=data_type,
|
|
243
|
+
nullable=nullable,
|
|
244
|
+
is_primary_key=is_primary_key,
|
|
245
|
+
is_foreign_key=is_foreign_key,
|
|
246
|
+
table_node_id=table_node_id,
|
|
247
|
+
comment=comment,
|
|
248
|
+
description=description,
|
|
249
|
+
default_value=default_value,
|
|
250
|
+
is_indexed=is_indexed,
|
|
251
|
+
check_constraints=check_constraints,
|
|
252
|
+
ordinal_position=ordinal_position,
|
|
253
|
+
most_common_values_json=most_common_values_json,
|
|
254
|
+
histogram_bounds_json=histogram_bounds_json,
|
|
255
|
+
stats_correlation=stats_correlation,
|
|
256
|
+
column_cardinality=column_cardinality,
|
|
257
|
+
index_type=index_type,
|
|
258
|
+
index_is_unique=index_is_unique,
|
|
259
|
+
parent_column_id=parent_column_id,
|
|
260
|
+
is_array=is_array,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def delete_column_node(self, column_node_id: str) -> None:
|
|
264
|
+
"""Remove a column node (``HAS_COLUMN`` edges removed by cascade)."""
|
|
265
|
+
return self._graph.delete_column_node(column_node_id)
|
|
266
|
+
|
|
267
|
+
def upsert_metric_template(
|
|
268
|
+
self,
|
|
269
|
+
*,
|
|
270
|
+
node_id: str,
|
|
271
|
+
connection_name: str,
|
|
272
|
+
database: str,
|
|
273
|
+
dialect: str = "postgresql",
|
|
274
|
+
name: str,
|
|
275
|
+
display_name: str,
|
|
276
|
+
description: str,
|
|
277
|
+
sql_template: str,
|
|
278
|
+
tables_used: list[str],
|
|
279
|
+
validated: bool,
|
|
280
|
+
validation_errors: list[str],
|
|
281
|
+
generated_at_iso: str,
|
|
282
|
+
stale: bool,
|
|
283
|
+
depends_on_table_node_ids: list[str],
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Insert or replace a ``MetricTemplate`` and its ``METRIC_DEPENDS`` edges."""
|
|
286
|
+
return self._graph.upsert_metric_template(
|
|
287
|
+
node_id=node_id,
|
|
288
|
+
connection_name=connection_name,
|
|
289
|
+
database=database,
|
|
290
|
+
dialect=dialect,
|
|
291
|
+
name=name,
|
|
292
|
+
display_name=display_name,
|
|
293
|
+
description=description,
|
|
294
|
+
sql_template=sql_template,
|
|
295
|
+
tables_used=tables_used,
|
|
296
|
+
validated=validated,
|
|
297
|
+
validation_errors=validation_errors,
|
|
298
|
+
generated_at_iso=generated_at_iso,
|
|
299
|
+
stale=stale,
|
|
300
|
+
depends_on_table_node_ids=depends_on_table_node_ids,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def delete_fk_edges_touching_column(
|
|
304
|
+
self, table_node_id: str, column_name: str
|
|
305
|
+
) -> int:
|
|
306
|
+
"""Delete FK edges where the column appears as source or target. Returns count."""
|
|
307
|
+
return self._graph.delete_fk_edges_touching_column(table_node_id, column_name)
|
|
308
|
+
|
|
309
|
+
def delete_inferred_joins_touching_column(
|
|
310
|
+
self, table_node_id: str, column_name: str
|
|
311
|
+
) -> int:
|
|
312
|
+
"""Delete inferred join edges touching a column on either endpoint."""
|
|
313
|
+
return self._graph.delete_inferred_joins_touching_column(
|
|
314
|
+
table_node_id, column_name
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def delete_table_node_cascade(self, table_node_id: str) -> None:
|
|
318
|
+
"""Remove a table and its column nodes; caller should handle intelligence cleanup."""
|
|
319
|
+
return self._graph.delete_table_node_cascade(table_node_id)
|
|
320
|
+
|
|
321
|
+
def mark_clusters_stale_for_table(
|
|
322
|
+
self, table_node_id: str, database_key: str
|
|
323
|
+
) -> None:
|
|
324
|
+
"""Set ``stale`` on clusters linked to the table."""
|
|
325
|
+
return self._graph.mark_clusters_stale_for_table(table_node_id, database_key)
|
|
326
|
+
|
|
327
|
+
def remove_table_from_clusters(self, table_node_id: str) -> None:
|
|
328
|
+
"""Drop ``IN_CLUSTER`` edges for a table (cluster nodes kept)."""
|
|
329
|
+
return self._graph.remove_table_from_clusters(table_node_id)
|
|
330
|
+
|
|
331
|
+
def mark_join_paths_stale_for_table(
|
|
332
|
+
self, table_node_id: str, database_key: str
|
|
333
|
+
) -> None:
|
|
334
|
+
"""Flag precomputed paths that start or end at this table."""
|
|
335
|
+
return self._graph.mark_join_paths_stale_for_table(table_node_id, database_key)
|
|
336
|
+
|
|
337
|
+
def delete_join_paths_for_table(
|
|
338
|
+
self, table_node_id: str, database_key: str
|
|
339
|
+
) -> None:
|
|
340
|
+
"""Remove precomputed paths touching a table (used when removing a table)."""
|
|
341
|
+
return self._graph.delete_join_paths_for_table(table_node_id, database_key)
|
|
342
|
+
|
|
343
|
+
def upsert_inferred_join(self, candidate: RelationshipCandidate) -> None:
|
|
344
|
+
"""Insert an inferred join edge (assumes endpoint table nodes exist)."""
|
|
345
|
+
return self._graph.upsert_inferred_join(candidate)
|
|
346
|
+
|
|
347
|
+
def upsert_cluster(
|
|
348
|
+
self,
|
|
349
|
+
*,
|
|
350
|
+
node_id: str,
|
|
351
|
+
database_key: str,
|
|
352
|
+
label: str,
|
|
353
|
+
description: str,
|
|
354
|
+
cohesion_score: float,
|
|
355
|
+
table_count: int,
|
|
356
|
+
stale: bool = False,
|
|
357
|
+
schema_pattern: str | None = None,
|
|
358
|
+
) -> None:
|
|
359
|
+
"""Insert or replace a ``Cluster`` node."""
|
|
360
|
+
return self._graph.upsert_cluster(
|
|
361
|
+
node_id=node_id,
|
|
362
|
+
database_key=database_key,
|
|
363
|
+
label=label,
|
|
364
|
+
description=description,
|
|
365
|
+
cohesion_score=cohesion_score,
|
|
366
|
+
table_count=table_count,
|
|
367
|
+
stale=stale,
|
|
368
|
+
schema_pattern=schema_pattern,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def upsert_in_cluster(self, table_node_id: str, cluster_node_id: str) -> None:
|
|
372
|
+
"""Link a table to a cluster (idempotent)."""
|
|
373
|
+
return self._graph.upsert_in_cluster(table_node_id, cluster_node_id)
|
|
374
|
+
|
|
375
|
+
def set_cluster_schema_pattern(
|
|
376
|
+
self, cluster_node_id: str, schema_pattern: str
|
|
377
|
+
) -> None:
|
|
378
|
+
"""Set ``schema_pattern`` on an existing ``Cluster`` node."""
|
|
379
|
+
return self._graph.set_cluster_schema_pattern(cluster_node_id, schema_pattern)
|
|
380
|
+
|
|
381
|
+
def upsert_join_path(
|
|
382
|
+
self,
|
|
383
|
+
*,
|
|
384
|
+
node_id: str,
|
|
385
|
+
database_key: str,
|
|
386
|
+
from_table_id: str,
|
|
387
|
+
to_table_id: str,
|
|
388
|
+
depth: int,
|
|
389
|
+
confidence: float,
|
|
390
|
+
ambiguous: bool,
|
|
391
|
+
steps_json: str,
|
|
392
|
+
semantic_label: str,
|
|
393
|
+
stale: bool = False,
|
|
394
|
+
) -> None:
|
|
395
|
+
"""Insert or replace a precomputed ``JoinPath`` node."""
|
|
396
|
+
return self._graph.upsert_join_path(
|
|
397
|
+
node_id=node_id,
|
|
398
|
+
database_key=database_key,
|
|
399
|
+
from_table_id=from_table_id,
|
|
400
|
+
to_table_id=to_table_id,
|
|
401
|
+
depth=depth,
|
|
402
|
+
confidence=confidence,
|
|
403
|
+
ambiguous=ambiguous,
|
|
404
|
+
steps_json=steps_json,
|
|
405
|
+
semantic_label=semantic_label,
|
|
406
|
+
stale=stale,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
def merge_same_entity_edge(
|
|
410
|
+
self,
|
|
411
|
+
*,
|
|
412
|
+
edge_id: str,
|
|
413
|
+
from_entity_node_id: str,
|
|
414
|
+
to_entity_node_id: str,
|
|
415
|
+
status: str,
|
|
416
|
+
score: float,
|
|
417
|
+
join_columns: str | None,
|
|
418
|
+
reasoning: str | None,
|
|
419
|
+
created_at: str,
|
|
420
|
+
confirmed_at: str | None = None,
|
|
421
|
+
confirmed_by: str | None = None,
|
|
422
|
+
) -> None:
|
|
423
|
+
"""Upsert a ``SAME_ENTITY`` edge between two ``Entity`` nodes (idempotent by ``edge_id``)."""
|
|
424
|
+
return self._graph.merge_same_entity_edge(
|
|
425
|
+
edge_id=edge_id,
|
|
426
|
+
from_entity_node_id=from_entity_node_id,
|
|
427
|
+
to_entity_node_id=to_entity_node_id,
|
|
428
|
+
status=status,
|
|
429
|
+
score=score,
|
|
430
|
+
join_columns=join_columns,
|
|
431
|
+
reasoning=reasoning,
|
|
432
|
+
created_at=created_at,
|
|
433
|
+
confirmed_at=confirmed_at,
|
|
434
|
+
confirmed_by=confirmed_by,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
def delete_same_entity_edge(self, edge_id: str) -> None:
|
|
438
|
+
"""Remove a ``SAME_ENTITY`` edge by ``edge_id``."""
|
|
439
|
+
return self._graph.delete_same_entity_edge(edge_id)
|
|
440
|
+
|
|
441
|
+
def same_entity_edge_endpoints(self, edge_id: str) -> tuple[str, str] | None:
|
|
442
|
+
"""Return ``(from_entity_node_id, to_entity_node_id)`` for an existing edge."""
|
|
443
|
+
return self._graph.same_entity_edge_endpoints(edge_id)
|
|
444
|
+
|
|
445
|
+
def list_same_entity_edges(
|
|
446
|
+
self, *, status: str | None = None
|
|
447
|
+
) -> list[dict[str, Any]]:
|
|
448
|
+
"""Return same-entity links with table context for CLI and MCP."""
|
|
449
|
+
return self._graph.list_same_entity_edges(status=status)
|
|
450
|
+
|
|
451
|
+
def list_entities_with_primary_table(
|
|
452
|
+
self, connection_name: str
|
|
453
|
+
) -> list[tuple[str, str, str]]:
|
|
454
|
+
"""Return ``(entity_name, schema.table, entity_node_id)`` per entity."""
|
|
455
|
+
return self._graph.list_entities_with_primary_table(connection_name)
|
|
456
|
+
|
|
457
|
+
def table_node_id_for_entity(self, entity_node_id: str) -> str | None:
|
|
458
|
+
"""First ``SchemaTable`` linked by ``REPRESENTS`` (ordered by name)."""
|
|
459
|
+
return self._graph.table_node_id_for_entity(entity_node_id)
|
|
460
|
+
|
|
461
|
+
def columns_for_table(
|
|
462
|
+
self, table_node_id: str
|
|
463
|
+
) -> list[tuple[str, str, bool, bool]]:
|
|
464
|
+
"""Return ``(column_name, data_type, nullable, is_pk)`` rows."""
|
|
465
|
+
return self._graph.columns_for_table(table_node_id)
|
|
466
|
+
|
|
467
|
+
def count_same_entity_by_status(self, status: str) -> int:
|
|
468
|
+
"""Count ``SAME_ENTITY`` edges with the given status."""
|
|
469
|
+
return self._graph.count_same_entity_by_status(status)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Optional graph enrichment passes (e.g. dbt manifest)."""
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""SQL fingerprint classifier: keyword regex + trial sqlglot parse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import sqlglot
|
|
8
|
+
from sqlglot.errors import ErrorLevel
|
|
9
|
+
|
|
10
|
+
from pretensor.enrichment.analyze.parse import normalize_sql, quiet_sqlglot
|
|
11
|
+
|
|
12
|
+
__all__ = ["CONFIDENCE_FLOAT", "classify_sql"]
|
|
13
|
+
|
|
14
|
+
_SQL_START_RE = re.compile(
|
|
15
|
+
r"^\s*(SELECT|INSERT|UPDATE|DELETE|MERGE|WITH|REPLACE)\b", re.IGNORECASE
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
CONFIDENCE_FLOAT: dict[str, float] = {
|
|
19
|
+
"high": 0.9,
|
|
20
|
+
"medium": 0.7,
|
|
21
|
+
"low": 0.5,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def classify_sql(text: str) -> tuple[str, float]:
|
|
26
|
+
"""Return ``(confidence_bucket, confidence_float)`` or ``('', 0.0)`` if not SQL.
|
|
27
|
+
|
|
28
|
+
Steps:
|
|
29
|
+
1. If ``text`` does not start with a SQL keyword, return ``('', 0.0)``.
|
|
30
|
+
2. If sqlglot can parse it without raising, return ``('high', 0.9)``.
|
|
31
|
+
3. Keyword match only → ``('medium', 0.7)``.
|
|
32
|
+
|
|
33
|
+
DBAPI bind placeholders (``%s``, ``%(name)s``, ``:name``, ``?``) are
|
|
34
|
+
normalized away before the trial parse, so parameterized statements — the
|
|
35
|
+
norm in scanned application code — classify on their structure. MySQL
|
|
36
|
+
``REPLACE INTO`` is rewritten to ``INSERT INTO`` first (sqlglot cannot
|
|
37
|
+
parse it in any dialect).
|
|
38
|
+
"""
|
|
39
|
+
if not text or not text.strip():
|
|
40
|
+
return "", 0.0
|
|
41
|
+
if not _SQL_START_RE.match(text):
|
|
42
|
+
return "", 0.0
|
|
43
|
+
try:
|
|
44
|
+
with quiet_sqlglot():
|
|
45
|
+
sqlglot.parse_one(normalize_sql(text), error_level=ErrorLevel.RAISE)
|
|
46
|
+
bucket = "high"
|
|
47
|
+
except Exception:
|
|
48
|
+
bucket = "medium"
|
|
49
|
+
return bucket, CONFIDENCE_FLOAT[bucket]
|