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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core graph storage, schema DDL, and build pipeline."""
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Build a Kuzu graph from a pipeline :class:`SchemaSnapshot`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from pretensor.config import GraphConfig, PretensorConfig
|
|
9
|
+
from pretensor.connectors.models import Column, SchemaSnapshot, ViewDependency
|
|
10
|
+
from pretensor.core.ids import (
|
|
11
|
+
column_node_id,
|
|
12
|
+
fk_edge_id,
|
|
13
|
+
lineage_edge_id,
|
|
14
|
+
table_node_id,
|
|
15
|
+
)
|
|
16
|
+
from pretensor.core.store import KuzuStore
|
|
17
|
+
from pretensor.graph_models.edge import GraphEdge, LineageEdge
|
|
18
|
+
from pretensor.graph_models.node import GraphNode
|
|
19
|
+
from pretensor.intelligence.discovery import RelationshipDiscovery
|
|
20
|
+
from pretensor.intelligence.embeddings import embeddings_disabled_via_env
|
|
21
|
+
from pretensor.intelligence.pipeline import run_intelligence_layer_sync
|
|
22
|
+
from pretensor.intelligence.steps_embedding import compute_table_embeddings
|
|
23
|
+
from pretensor.visibility.filter import VisibilityFilter
|
|
24
|
+
|
|
25
|
+
__all__ = ["GraphBuilder", "table_node_id", "fk_edge_id"]
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _filtered_snapshot_for_visibility(
|
|
31
|
+
snapshot: SchemaSnapshot,
|
|
32
|
+
visibility_filter: VisibilityFilter,
|
|
33
|
+
) -> SchemaSnapshot:
|
|
34
|
+
"""Drop hidden tables and view dependencies that touch hidden tables."""
|
|
35
|
+
visible_tables = [
|
|
36
|
+
t
|
|
37
|
+
for t in snapshot.tables
|
|
38
|
+
if visibility_filter.is_table_visible(
|
|
39
|
+
snapshot.connection_name, t.schema_name, t.name
|
|
40
|
+
)
|
|
41
|
+
]
|
|
42
|
+
keys = {(t.schema_name, t.name) for t in visible_tables}
|
|
43
|
+
deps: list[ViewDependency] = []
|
|
44
|
+
for dep in snapshot.view_dependencies:
|
|
45
|
+
if (dep.source_schema, dep.source_table) not in keys:
|
|
46
|
+
continue
|
|
47
|
+
if (dep.target_schema, dep.target_table) not in keys:
|
|
48
|
+
continue
|
|
49
|
+
deps.append(dep)
|
|
50
|
+
return snapshot.model_copy(
|
|
51
|
+
update={"tables": visible_tables, "view_dependencies": deps}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class GraphBuilder:
|
|
56
|
+
"""Writes table nodes and foreign-key edges from a snapshot."""
|
|
57
|
+
|
|
58
|
+
def build(
|
|
59
|
+
self,
|
|
60
|
+
snapshot: SchemaSnapshot,
|
|
61
|
+
store: KuzuStore,
|
|
62
|
+
*,
|
|
63
|
+
run_relationship_discovery: bool = True,
|
|
64
|
+
replace_mode: str = "full",
|
|
65
|
+
graph_config: GraphConfig | None = None,
|
|
66
|
+
config: PretensorConfig | None = None,
|
|
67
|
+
visibility_filter: VisibilityFilter | None = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Populate ``store`` from ``snapshot`` (replaces existing graph content).
|
|
70
|
+
|
|
71
|
+
When ``run_relationship_discovery`` is True, runs heuristic discovery and
|
|
72
|
+
writes ``INFERRED_JOIN`` edges that are not already explicit FKs.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
replace_mode: ``full`` clears the entire graph (default per-connection index).
|
|
76
|
+
``connection`` removes only nodes/edges for ``snapshot.connection_name``
|
|
77
|
+
(unified multi-DB graph file).
|
|
78
|
+
graph_config: Deprecated. Use ``config.graph`` instead. If both are given,
|
|
79
|
+
``config.graph`` takes precedence.
|
|
80
|
+
config: Full pluggable configuration. When provided, its ``scorer_registry``
|
|
81
|
+
and ``combiner`` are forwarded to :class:`RelationshipDiscovery` and
|
|
82
|
+
its ``graph`` field is forwarded to the intelligence pipeline.
|
|
83
|
+
"""
|
|
84
|
+
store.ensure_schema()
|
|
85
|
+
effective_embeddings = (config or PretensorConfig()).embeddings
|
|
86
|
+
if not effective_embeddings.index_tables and store.has_any_table_embeddings():
|
|
87
|
+
logger.warning(
|
|
88
|
+
"Rebuilding without --embeddings: previously computed table "
|
|
89
|
+
"embeddings will be dropped with the old graph rows. Re-run "
|
|
90
|
+
"with --embeddings to recompute them."
|
|
91
|
+
)
|
|
92
|
+
if replace_mode == "connection":
|
|
93
|
+
store.clear_connection_subgraph(snapshot.connection_name)
|
|
94
|
+
else:
|
|
95
|
+
store.clear_graph()
|
|
96
|
+
|
|
97
|
+
snap = (
|
|
98
|
+
_filtered_snapshot_for_visibility(snapshot, visibility_filter)
|
|
99
|
+
if visibility_filter is not None
|
|
100
|
+
else snapshot
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
for table in snap.tables:
|
|
104
|
+
grants_json = (
|
|
105
|
+
json.dumps(table.grants, ensure_ascii=False) if table.grants else None
|
|
106
|
+
)
|
|
107
|
+
node = GraphNode(
|
|
108
|
+
node_id=table_node_id(
|
|
109
|
+
snap.connection_name, table.schema_name, table.name
|
|
110
|
+
),
|
|
111
|
+
connection_name=snap.connection_name,
|
|
112
|
+
database=snap.database,
|
|
113
|
+
schema_name=table.schema_name,
|
|
114
|
+
table_name=table.name,
|
|
115
|
+
row_count=table.row_count,
|
|
116
|
+
row_count_source=table.row_count_source,
|
|
117
|
+
comment=table.comment,
|
|
118
|
+
table_type=table.table_type,
|
|
119
|
+
seq_scan_count=table.seq_scan_count,
|
|
120
|
+
idx_scan_count=table.idx_scan_count,
|
|
121
|
+
insert_count=table.insert_count,
|
|
122
|
+
update_count=table.update_count,
|
|
123
|
+
delete_count=table.delete_count,
|
|
124
|
+
is_partitioned=table.is_partitioned,
|
|
125
|
+
partition_key=table.partition_key,
|
|
126
|
+
grants_json=grants_json,
|
|
127
|
+
access_read_count=table.access_read_count,
|
|
128
|
+
access_write_count=table.access_write_count,
|
|
129
|
+
days_since_last_access=table.days_since_last_access,
|
|
130
|
+
potentially_unused=table.potentially_unused,
|
|
131
|
+
table_bytes=table.table_bytes,
|
|
132
|
+
clustering_key=table.clustering_key,
|
|
133
|
+
)
|
|
134
|
+
store.upsert_table(node)
|
|
135
|
+
col_names = [c.name for c in table.columns]
|
|
136
|
+
if visibility_filter is not None:
|
|
137
|
+
allowed_cols = visibility_filter.visible_columns(
|
|
138
|
+
snap.connection_name,
|
|
139
|
+
table.schema_name,
|
|
140
|
+
table.name,
|
|
141
|
+
col_names,
|
|
142
|
+
)
|
|
143
|
+
allowed_set = set(allowed_cols)
|
|
144
|
+
pending = [c for c in table.columns if c.name in allowed_set]
|
|
145
|
+
else:
|
|
146
|
+
pending = list(table.columns)
|
|
147
|
+
written: set[str] = set()
|
|
148
|
+
while pending:
|
|
149
|
+
progressed = False
|
|
150
|
+
next_pass: list[Column] = []
|
|
151
|
+
for col in pending:
|
|
152
|
+
parent_id: str | None = None
|
|
153
|
+
if col.parent_column is not None:
|
|
154
|
+
if col.parent_column not in written:
|
|
155
|
+
next_pass.append(col)
|
|
156
|
+
continue
|
|
157
|
+
parent_id = column_node_id(
|
|
158
|
+
snap.connection_name,
|
|
159
|
+
table.schema_name,
|
|
160
|
+
table.name,
|
|
161
|
+
col.parent_column,
|
|
162
|
+
)
|
|
163
|
+
cid = column_node_id(
|
|
164
|
+
snap.connection_name, table.schema_name, table.name, col.name
|
|
165
|
+
)
|
|
166
|
+
mcv_json = (
|
|
167
|
+
json.dumps(col.most_common_values, ensure_ascii=False)
|
|
168
|
+
if col.most_common_values
|
|
169
|
+
else None
|
|
170
|
+
)
|
|
171
|
+
hb_json = (
|
|
172
|
+
json.dumps(col.histogram_bounds, ensure_ascii=False)
|
|
173
|
+
if col.histogram_bounds
|
|
174
|
+
else None
|
|
175
|
+
)
|
|
176
|
+
store.upsert_column_for_table(
|
|
177
|
+
column_node_id=cid,
|
|
178
|
+
connection_name=snap.connection_name,
|
|
179
|
+
database=snap.database,
|
|
180
|
+
schema_name=table.schema_name,
|
|
181
|
+
table_name=table.name,
|
|
182
|
+
column_name=col.name,
|
|
183
|
+
data_type=col.data_type,
|
|
184
|
+
nullable=col.nullable,
|
|
185
|
+
is_primary_key=col.is_primary_key,
|
|
186
|
+
is_foreign_key=col.is_foreign_key,
|
|
187
|
+
table_node_id=node.node_id,
|
|
188
|
+
comment=col.comment,
|
|
189
|
+
default_value=col.default_value,
|
|
190
|
+
is_indexed=col.is_indexed,
|
|
191
|
+
check_constraints=col.check_constraints,
|
|
192
|
+
ordinal_position=col.ordinal_position,
|
|
193
|
+
most_common_values_json=mcv_json,
|
|
194
|
+
histogram_bounds_json=hb_json,
|
|
195
|
+
stats_correlation=col.stats_correlation,
|
|
196
|
+
column_cardinality=col.column_cardinality,
|
|
197
|
+
index_type=col.index_type,
|
|
198
|
+
index_is_unique=col.index_is_unique,
|
|
199
|
+
parent_column_id=parent_id,
|
|
200
|
+
is_array=col.is_array,
|
|
201
|
+
)
|
|
202
|
+
written.add(col.name)
|
|
203
|
+
progressed = True
|
|
204
|
+
if not progressed and next_pass:
|
|
205
|
+
logger.warning(
|
|
206
|
+
"build: %d column(s) for table %s.%s skipped — "
|
|
207
|
+
"unresolvable parent_column reference(s): %s",
|
|
208
|
+
len(next_pass),
|
|
209
|
+
table.schema_name,
|
|
210
|
+
table.name,
|
|
211
|
+
", ".join(col.name for col in next_pass),
|
|
212
|
+
)
|
|
213
|
+
break
|
|
214
|
+
pending = next_pass
|
|
215
|
+
|
|
216
|
+
known = {
|
|
217
|
+
table_node_id(snap.connection_name, t.schema_name, t.name)
|
|
218
|
+
for t in snap.tables
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for table in snap.tables:
|
|
222
|
+
for fk in table.foreign_keys:
|
|
223
|
+
src_id = table_node_id(
|
|
224
|
+
snap.connection_name, fk.source_schema, fk.source_table
|
|
225
|
+
)
|
|
226
|
+
dst_id = table_node_id(
|
|
227
|
+
snap.connection_name, fk.target_schema, fk.target_table
|
|
228
|
+
)
|
|
229
|
+
if src_id not in known or dst_id not in known:
|
|
230
|
+
continue
|
|
231
|
+
edge = GraphEdge(
|
|
232
|
+
edge_id=fk_edge_id(fk),
|
|
233
|
+
source_node_id=src_id,
|
|
234
|
+
target_node_id=dst_id,
|
|
235
|
+
source_column=fk.source_column,
|
|
236
|
+
target_column=fk.target_column,
|
|
237
|
+
constraint_name=fk.constraint_name,
|
|
238
|
+
)
|
|
239
|
+
store.merge_fk_edge(edge)
|
|
240
|
+
|
|
241
|
+
lineage_by_id: dict[str, LineageEdge] = {}
|
|
242
|
+
for dep in snap.view_dependencies:
|
|
243
|
+
src_id = table_node_id(
|
|
244
|
+
snap.connection_name, dep.source_schema, dep.source_table
|
|
245
|
+
)
|
|
246
|
+
dst_id = table_node_id(
|
|
247
|
+
snap.connection_name, dep.target_schema, dep.target_table
|
|
248
|
+
)
|
|
249
|
+
if src_id not in known or dst_id not in known:
|
|
250
|
+
continue
|
|
251
|
+
eid = lineage_edge_id(snap.connection_name, dep)
|
|
252
|
+
src_label = f"{snap.connection_name}:{dep.object_name}"
|
|
253
|
+
cand = LineageEdge(
|
|
254
|
+
edge_id=eid,
|
|
255
|
+
source_node_id=src_id,
|
|
256
|
+
target_node_id=dst_id,
|
|
257
|
+
source=src_label,
|
|
258
|
+
lineage_type=dep.lineage_type,
|
|
259
|
+
confidence=float(dep.confidence),
|
|
260
|
+
)
|
|
261
|
+
existing = lineage_by_id.get(eid)
|
|
262
|
+
if existing is None or cand.confidence > existing.confidence:
|
|
263
|
+
lineage_by_id[eid] = cand
|
|
264
|
+
for ledge in lineage_by_id.values():
|
|
265
|
+
store.upsert_lineage_edge(ledge)
|
|
266
|
+
|
|
267
|
+
effective_config = config or PretensorConfig()
|
|
268
|
+
effective_graph_cfg = (
|
|
269
|
+
effective_config.graph if config else (graph_config or GraphConfig())
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Compute embeddings right after schema rows land and BEFORE
|
|
273
|
+
# relationship discovery, so the embedding relationship scorer, the
|
|
274
|
+
# clustering blend, and the classify role vote all read vectors
|
|
275
|
+
# from THIS run instead of lagging one index cycle behind.
|
|
276
|
+
embeddings_precomputed = False
|
|
277
|
+
if (
|
|
278
|
+
effective_config.embeddings.index_tables
|
|
279
|
+
and not embeddings_disabled_via_env()
|
|
280
|
+
):
|
|
281
|
+
compute_table_embeddings(store, snap.database)
|
|
282
|
+
embeddings_precomputed = True
|
|
283
|
+
|
|
284
|
+
if run_relationship_discovery:
|
|
285
|
+
from pretensor.intelligence.semantic import (
|
|
286
|
+
extend_with_embedding_scorer,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
scorers = extend_with_embedding_scorer(
|
|
290
|
+
effective_config.scorer_registry,
|
|
291
|
+
store=store,
|
|
292
|
+
database_key=snap.database,
|
|
293
|
+
threshold=effective_config.embeddings.join_threshold,
|
|
294
|
+
)
|
|
295
|
+
RelationshipDiscovery(
|
|
296
|
+
store,
|
|
297
|
+
scorers=scorers,
|
|
298
|
+
combiner=effective_config.combiner,
|
|
299
|
+
graph_config=effective_graph_cfg,
|
|
300
|
+
).discover(snap)
|
|
301
|
+
|
|
302
|
+
run_intelligence_layer_sync(
|
|
303
|
+
store,
|
|
304
|
+
snap.database,
|
|
305
|
+
config=effective_config,
|
|
306
|
+
embeddings_precomputed=embeddings_precomputed,
|
|
307
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Fernet encryption for DSN strings at rest under ``.pretensor/``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from cryptography.fernet import Fernet, InvalidToken
|
|
8
|
+
|
|
9
|
+
from pretensor.core.secure_io import (
|
|
10
|
+
atomic_write_bytes,
|
|
11
|
+
secure_mkdir,
|
|
12
|
+
warn_if_world_readable,
|
|
13
|
+
)
|
|
14
|
+
from pretensor.errors import PretensorError
|
|
15
|
+
|
|
16
|
+
__all__ = ["DSNEncryptor", "DSNDecryptError"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DSNDecryptError(PretensorError, RuntimeError):
|
|
20
|
+
"""Raised when an encrypted DSN cannot be decrypted."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DSNEncryptor:
|
|
24
|
+
"""Symmetric encryption for connection strings (key file beside registry)."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, key_path: Path) -> None:
|
|
27
|
+
self._key_path = key_path
|
|
28
|
+
self._fernet = Fernet(self._load_or_create_key())
|
|
29
|
+
|
|
30
|
+
def _load_or_create_key(self) -> bytes:
|
|
31
|
+
# The directory holding the sole secret that decrypts every DSN is
|
|
32
|
+
# created 0o700, and the key file 0o600, so a local user on a shared
|
|
33
|
+
# host cannot read it. An existing too-loose key triggers a warning.
|
|
34
|
+
secure_mkdir(self._key_path.parent)
|
|
35
|
+
if self._key_path.exists():
|
|
36
|
+
warn_if_world_readable(self._key_path)
|
|
37
|
+
return self._key_path.read_bytes().strip()
|
|
38
|
+
key = Fernet.generate_key()
|
|
39
|
+
atomic_write_bytes(self._key_path, key + b"\n", mode=0o600)
|
|
40
|
+
return key
|
|
41
|
+
|
|
42
|
+
def encrypt(self, dsn: str) -> str:
|
|
43
|
+
"""Return urlsafe base64 ciphertext."""
|
|
44
|
+
return self._fernet.encrypt(dsn.encode("utf-8")).decode("ascii")
|
|
45
|
+
|
|
46
|
+
def decrypt(self, token: str) -> str:
|
|
47
|
+
"""Recover the original DSN or raise :class:`DSNDecryptError`."""
|
|
48
|
+
try:
|
|
49
|
+
return self._fernet.decrypt(token.encode("ascii")).decode("utf-8")
|
|
50
|
+
except InvalidToken as exc:
|
|
51
|
+
raise DSNDecryptError("Invalid encrypted DSN or wrong keystore") from exc
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Schema creation and idempotent column migration for the Kuzu graph.
|
|
2
|
+
|
|
3
|
+
Extracted from :class:`~pretensor.core.store.KuzuStore` to isolate the
|
|
4
|
+
schema-migration concern from the rest of the store layer.
|
|
5
|
+
:class:`GraphSchemaManager` is composed inside ``KuzuStore``; it owns every
|
|
6
|
+
``ensure_schema`` DDL call and every ``_ensure_*`` / ``_drop_*`` column-migration
|
|
7
|
+
helper so that ``KuzuStore`` can focus on query, upsert, and read operations.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
import kuzu
|
|
15
|
+
|
|
16
|
+
from pretensor.core import schema as graph_schema
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Kept in sync with ``pretensor.intelligence.embeddings.EMBEDDING_DIM`` (384).
|
|
21
|
+
# Duplicated here to avoid the circular import created by
|
|
22
|
+
# ``intelligence/__init__.py`` eagerly loading ``cluster_labeler`` (which itself
|
|
23
|
+
# imports ``KuzuStore``). The schema DDL literal ``FLOAT[384]`` and this
|
|
24
|
+
# constant are both asserted against the real ``EMBEDDING_DIM`` in tests.
|
|
25
|
+
_SCHEMA_TABLE_EMBEDDING_DIM = 384
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GraphSchemaManager:
|
|
29
|
+
"""Owns schema creation and idempotent column migrations for the Kuzu graph."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, conn: kuzu.Connection) -> None:
|
|
32
|
+
self._conn = conn
|
|
33
|
+
|
|
34
|
+
def _add_column_if_missing(self, table: str, col_name: str, col_type: str) -> None:
|
|
35
|
+
try:
|
|
36
|
+
self._conn.execute(f"ALTER TABLE {table} ADD {col_name} {col_type}")
|
|
37
|
+
except RuntimeError as exc:
|
|
38
|
+
message = str(exc).lower()
|
|
39
|
+
if col_name.lower() in message and "already has" in message:
|
|
40
|
+
return
|
|
41
|
+
logger.warning("%s ALTER for %s: %s", table, col_name, exc)
|
|
42
|
+
|
|
43
|
+
def _drop_column_if_present(self, table: str, col_name: str) -> None:
|
|
44
|
+
try:
|
|
45
|
+
self._conn.execute(f"ALTER TABLE {table} DROP {col_name}")
|
|
46
|
+
except RuntimeError as exc:
|
|
47
|
+
message = str(exc).lower()
|
|
48
|
+
if "does not have" in message or "does not exist" in message:
|
|
49
|
+
return
|
|
50
|
+
logger.debug("%s DROP for %s: %s", table, col_name, exc)
|
|
51
|
+
|
|
52
|
+
def ensure_schema(self) -> None:
|
|
53
|
+
"""Create node and relationship tables if they do not exist."""
|
|
54
|
+
self._conn.execute(graph_schema.DDL_CREATE_TABLE_NODE)
|
|
55
|
+
self._ensure_schema_table_entity_type_column()
|
|
56
|
+
self._ensure_schema_table_table_type_column()
|
|
57
|
+
self._ensure_schema_table_catalog_enrichment_columns()
|
|
58
|
+
self._ensure_schema_table_classification_columns()
|
|
59
|
+
self._ensure_schema_table_dbt_enrichment_columns()
|
|
60
|
+
self._ensure_schema_table_description_tags_columns()
|
|
61
|
+
self._ensure_schema_table_staleness_columns()
|
|
62
|
+
self._ensure_schema_table_row_count_source_column()
|
|
63
|
+
self._ensure_schema_table_embedding_column()
|
|
64
|
+
self._drop_schema_table_legacy_dbt_columns()
|
|
65
|
+
self._conn.execute(graph_schema.DDL_CREATE_COLUMN_NODE)
|
|
66
|
+
self._ensure_schema_column_description_column()
|
|
67
|
+
self._ensure_schema_column_enrichment_columns()
|
|
68
|
+
self._ensure_schema_column_stats_columns()
|
|
69
|
+
self._ensure_schema_column_catalog_enrichment_columns()
|
|
70
|
+
self._ensure_schema_column_nested_columns()
|
|
71
|
+
self._conn.execute(graph_schema.DDL_CREATE_HAS_COLUMN_REL)
|
|
72
|
+
self._conn.execute(graph_schema.DDL_CREATE_HAS_SUBCOLUMN_REL)
|
|
73
|
+
self._conn.execute(graph_schema.DDL_CREATE_ENTITY_NODE)
|
|
74
|
+
self._conn.execute(graph_schema.DDL_CREATE_REPRESENTS_REL)
|
|
75
|
+
self._conn.execute(graph_schema.DDL_CREATE_FK_REL)
|
|
76
|
+
self._ensure_fk_constraint_name_column()
|
|
77
|
+
self._conn.execute(graph_schema.DDL_CREATE_INFERRED_JOIN_REL)
|
|
78
|
+
self._conn.execute(graph_schema.DDL_CREATE_LINEAGE_REL)
|
|
79
|
+
self._conn.execute(graph_schema.DDL_CREATE_SAME_ENTITY_REL)
|
|
80
|
+
self._conn.execute(graph_schema.DDL_CREATE_CLUSTER_NODE)
|
|
81
|
+
self._ensure_cluster_stale_column()
|
|
82
|
+
self._ensure_cluster_schema_pattern_column()
|
|
83
|
+
self._conn.execute(graph_schema.DDL_CREATE_IN_CLUSTER_REL)
|
|
84
|
+
self._conn.execute(graph_schema.DDL_CREATE_JOIN_PATH_NODE)
|
|
85
|
+
self._ensure_join_path_stale_column()
|
|
86
|
+
self._conn.execute(graph_schema.DDL_CREATE_METRIC_TEMPLATE_NODE)
|
|
87
|
+
self._ensure_metric_template_stale_column()
|
|
88
|
+
self._ensure_metric_template_dialect_column()
|
|
89
|
+
self._conn.execute(graph_schema.DDL_CREATE_METRIC_DEPENDS_REL)
|
|
90
|
+
self._conn.execute(graph_schema.DDL_CREATE_SEMANTIC_METRIC_NODE)
|
|
91
|
+
self._conn.execute(graph_schema.DDL_CREATE_SEMANTIC_DIMENSION_NODE)
|
|
92
|
+
self._conn.execute(graph_schema.DDL_CREATE_SEMANTIC_BUSINESS_RULE_NODE)
|
|
93
|
+
self._conn.execute(graph_schema.DDL_CREATE_SEMANTIC_METRIC_DEPENDS_REL)
|
|
94
|
+
self._conn.execute(graph_schema.DDL_CREATE_SEMANTIC_DIMENSION_LEVEL_REL)
|
|
95
|
+
self._conn.execute(graph_schema.DDL_CREATE_SEMANTIC_RULE_APPLIES_TO_REL)
|
|
96
|
+
self._conn.execute(graph_schema.DDL_CREATE_EXTERNAL_CONSUMER_NODE)
|
|
97
|
+
self._conn.execute(graph_schema.DDL_CREATE_CONSUMES_REL)
|
|
98
|
+
|
|
99
|
+
def _ensure_schema_table_entity_type_column(self) -> None:
|
|
100
|
+
"""Add ``entity_type`` to ``SchemaTable`` when upgrading older graph files."""
|
|
101
|
+
self._add_column_if_missing("SchemaTable", "entity_type", "STRING")
|
|
102
|
+
|
|
103
|
+
def _ensure_schema_table_table_type_column(self) -> None:
|
|
104
|
+
"""Add ``table_type`` to ``SchemaTable`` when upgrading older graph files."""
|
|
105
|
+
self._add_column_if_missing("SchemaTable", "table_type", "STRING")
|
|
106
|
+
|
|
107
|
+
def _ensure_schema_table_catalog_enrichment_columns(self) -> None:
|
|
108
|
+
"""Add usage, partition, grant, and Snowflake catalog fields for older graphs."""
|
|
109
|
+
for col_name, col_type in (
|
|
110
|
+
("seq_scan_count", "INT64"),
|
|
111
|
+
("idx_scan_count", "INT64"),
|
|
112
|
+
("insert_count", "INT64"),
|
|
113
|
+
("update_count", "INT64"),
|
|
114
|
+
("delete_count", "INT64"),
|
|
115
|
+
("is_partitioned", "BOOL"),
|
|
116
|
+
("partition_key", "STRING"),
|
|
117
|
+
("grants_json", "STRING"),
|
|
118
|
+
("access_read_count", "INT64"),
|
|
119
|
+
("access_write_count", "INT64"),
|
|
120
|
+
("days_since_last_access", "INT64"),
|
|
121
|
+
("potentially_unused", "BOOL"),
|
|
122
|
+
("table_bytes", "INT64"),
|
|
123
|
+
("clustering_key", "STRING"),
|
|
124
|
+
):
|
|
125
|
+
self._add_column_if_missing("SchemaTable", col_name, col_type)
|
|
126
|
+
|
|
127
|
+
def _ensure_schema_table_classification_columns(self) -> None:
|
|
128
|
+
"""Add classifier fields for older graph files."""
|
|
129
|
+
for col_name, col_type in (
|
|
130
|
+
("role", "STRING"),
|
|
131
|
+
("role_confidence", "DOUBLE"),
|
|
132
|
+
("classification_signals", "STRING"),
|
|
133
|
+
):
|
|
134
|
+
self._add_column_if_missing("SchemaTable", col_name, col_type)
|
|
135
|
+
|
|
136
|
+
def _ensure_schema_table_dbt_enrichment_columns(self) -> None:
|
|
137
|
+
"""Add dbt semantic-layer fields for older graph files."""
|
|
138
|
+
for col_name, col_type in (
|
|
139
|
+
("has_external_consumers", "BOOL"),
|
|
140
|
+
("test_count", "INT64"),
|
|
141
|
+
):
|
|
142
|
+
self._add_column_if_missing("SchemaTable", col_name, col_type)
|
|
143
|
+
|
|
144
|
+
def _ensure_schema_table_description_tags_columns(self) -> None:
|
|
145
|
+
"""Add first-class ``description`` and ``tags`` for dbt / LLM context."""
|
|
146
|
+
for col_name, col_type in (
|
|
147
|
+
("description", "STRING"),
|
|
148
|
+
("tags", "STRING[]"),
|
|
149
|
+
):
|
|
150
|
+
self._add_column_if_missing("SchemaTable", col_name, col_type)
|
|
151
|
+
|
|
152
|
+
def _ensure_schema_table_staleness_columns(self) -> None:
|
|
153
|
+
"""Add dbt source-freshness fields (``staleness_status``/``staleness_as_of``)."""
|
|
154
|
+
for col_name, col_type in (
|
|
155
|
+
("staleness_status", "STRING"),
|
|
156
|
+
("staleness_as_of", "STRING"),
|
|
157
|
+
):
|
|
158
|
+
self._add_column_if_missing("SchemaTable", col_name, col_type)
|
|
159
|
+
|
|
160
|
+
def _ensure_schema_table_row_count_source_column(self) -> None:
|
|
161
|
+
"""Add ``row_count_source`` for older graph files (view-count provenance)."""
|
|
162
|
+
self._add_column_if_missing("SchemaTable", "row_count_source", "STRING")
|
|
163
|
+
|
|
164
|
+
def _ensure_schema_table_embedding_column(self) -> None:
|
|
165
|
+
"""Add ``embedding FLOAT[384]`` to ``SchemaTable`` for older graph files."""
|
|
166
|
+
self._add_column_if_missing(
|
|
167
|
+
"SchemaTable",
|
|
168
|
+
"embedding",
|
|
169
|
+
f"FLOAT[{_SCHEMA_TABLE_EMBEDDING_DIM}]",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _drop_schema_table_legacy_dbt_columns(self) -> None:
|
|
173
|
+
"""Drop legacy duplicates ``dbt_description`` and ``tags_json``.
|
|
174
|
+
|
|
175
|
+
These were superseded by the first-class ``description`` / ``tags``
|
|
176
|
+
fields added in the dbt enrichment update. Older graph files that were
|
|
177
|
+
written before the consolidation may still have them; silently drop if
|
|
178
|
+
present.
|
|
179
|
+
"""
|
|
180
|
+
for col_name in ("dbt_description", "tags_json"):
|
|
181
|
+
self._drop_column_if_present("SchemaTable", col_name)
|
|
182
|
+
|
|
183
|
+
def _ensure_schema_column_description_column(self) -> None:
|
|
184
|
+
"""Add ``description`` on ``SchemaColumn`` for dbt column docs."""
|
|
185
|
+
self._add_column_if_missing("SchemaColumn", "description", "STRING")
|
|
186
|
+
|
|
187
|
+
def _ensure_schema_column_enrichment_columns(self) -> None:
|
|
188
|
+
"""Add column comment / constraint fields for older Kuzu files."""
|
|
189
|
+
for col_name, col_type in (
|
|
190
|
+
("comment", "STRING"),
|
|
191
|
+
("default_value", "STRING"),
|
|
192
|
+
("is_indexed", "BOOL"),
|
|
193
|
+
("check_constraints_json", "STRING"),
|
|
194
|
+
("ordinal_position", "INT64"),
|
|
195
|
+
):
|
|
196
|
+
self._add_column_if_missing("SchemaColumn", col_name, col_type)
|
|
197
|
+
|
|
198
|
+
def _ensure_schema_column_stats_columns(self) -> None:
|
|
199
|
+
"""Add planner statistics mirror fields for older Kuzu files."""
|
|
200
|
+
for col_name, col_type in (
|
|
201
|
+
("most_common_values_json", "STRING"),
|
|
202
|
+
("histogram_bounds_json", "STRING"),
|
|
203
|
+
("stats_correlation", "DOUBLE"),
|
|
204
|
+
):
|
|
205
|
+
self._add_column_if_missing("SchemaColumn", col_name, col_type)
|
|
206
|
+
|
|
207
|
+
def _ensure_schema_column_catalog_enrichment_columns(self) -> None:
|
|
208
|
+
"""Add catalog cardinality and index-detail fields for older Kuzu files."""
|
|
209
|
+
for col_name, col_type in (
|
|
210
|
+
("column_cardinality", "INT64"),
|
|
211
|
+
("index_type", "STRING"),
|
|
212
|
+
("index_is_unique", "BOOL"),
|
|
213
|
+
):
|
|
214
|
+
self._add_column_if_missing("SchemaColumn", col_name, col_type)
|
|
215
|
+
|
|
216
|
+
def _ensure_schema_column_nested_columns(self) -> None:
|
|
217
|
+
"""Add nested-column linkage fields for older Kuzu files."""
|
|
218
|
+
for col_name, col_type in (
|
|
219
|
+
("parent_column_id", "STRING"),
|
|
220
|
+
("is_array", "BOOL"),
|
|
221
|
+
):
|
|
222
|
+
self._add_column_if_missing("SchemaColumn", col_name, col_type)
|
|
223
|
+
|
|
224
|
+
def _ensure_fk_constraint_name_column(self) -> None:
|
|
225
|
+
"""Add ``constraint_name`` to ``FK_REFERENCES`` when upgrading older graph files."""
|
|
226
|
+
self._add_column_if_missing("FK_REFERENCES", "constraint_name", "STRING")
|
|
227
|
+
|
|
228
|
+
def _ensure_cluster_stale_column(self) -> None:
|
|
229
|
+
"""Add ``stale`` to ``Cluster`` when upgrading older graph files."""
|
|
230
|
+
self._add_column_if_missing("Cluster", "stale", "BOOL")
|
|
231
|
+
|
|
232
|
+
def _ensure_cluster_schema_pattern_column(self) -> None:
|
|
233
|
+
"""Add ``schema_pattern`` to ``Cluster`` when upgrading older graph files."""
|
|
234
|
+
self._add_column_if_missing("Cluster", "schema_pattern", "STRING")
|
|
235
|
+
|
|
236
|
+
def _ensure_join_path_stale_column(self) -> None:
|
|
237
|
+
"""Add ``stale`` to ``JoinPath`` when upgrading older graph files."""
|
|
238
|
+
self._add_column_if_missing("JoinPath", "stale", "BOOL")
|
|
239
|
+
|
|
240
|
+
def _ensure_metric_template_stale_column(self) -> None:
|
|
241
|
+
"""Add ``stale`` to ``MetricTemplate`` when upgrading older graph files."""
|
|
242
|
+
self._add_column_if_missing("MetricTemplate", "stale", "BOOL")
|
|
243
|
+
|
|
244
|
+
def _ensure_metric_template_dialect_column(self) -> None:
|
|
245
|
+
"""Add ``dialect`` to ``MetricTemplate`` when upgrading older graph files."""
|
|
246
|
+
self._add_column_if_missing("MetricTemplate", "dialect", "STRING")
|