pretensor 0.1.0a0__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/cli/__init__.py +1 -0
- pretensor/cli/commands/connections/__init__.py +7 -0
- pretensor/cli/commands/connections/add_remove.py +122 -0
- pretensor/cli/commands/connections/register.py +12 -0
- pretensor/cli/commands/export.py +120 -0
- pretensor/cli/commands/index.py +551 -0
- pretensor/cli/commands/list.py +65 -0
- pretensor/cli/commands/quickstart.py +171 -0
- pretensor/cli/commands/reindex.py +581 -0
- pretensor/cli/commands/semantic.py +190 -0
- pretensor/cli/commands/serve.py +129 -0
- pretensor/cli/commands/sync_grants.py +149 -0
- pretensor/cli/commands/validate.py +156 -0
- pretensor/cli/config_file.py +412 -0
- pretensor/cli/constants.py +10 -0
- pretensor/cli/dbt_enrichment.py +96 -0
- pretensor/cli/main.py +81 -0
- pretensor/cli/paths.py +43 -0
- pretensor/cli/plugin.py +52 -0
- pretensor/config.py +145 -0
- pretensor/connectors/SKILL.md +16 -0
- pretensor/connectors/__init__.py +29 -0
- pretensor/connectors/base.py +165 -0
- pretensor/connectors/bigquery.py +463 -0
- pretensor/connectors/inspect.py +321 -0
- pretensor/connectors/lineage_sqlglot.py +91 -0
- pretensor/connectors/models.py +130 -0
- pretensor/connectors/pg_array_parse.py +53 -0
- pretensor/connectors/postgres.py +921 -0
- pretensor/connectors/registry.py +51 -0
- pretensor/connectors/snapshot.py +244 -0
- pretensor/connectors/snowflake.py +840 -0
- pretensor/core/__init__.py +1 -0
- pretensor/core/builder.py +268 -0
- pretensor/core/dsn_crypto.py +40 -0
- pretensor/core/ids.py +68 -0
- pretensor/core/portable_export.py +253 -0
- pretensor/core/registry.py +204 -0
- pretensor/core/schema.py +368 -0
- pretensor/core/store.py +1343 -0
- pretensor/enrichment/__init__.py +1 -0
- pretensor/enrichment/dbt/__init__.py +30 -0
- pretensor/enrichment/dbt/lineage.py +100 -0
- pretensor/enrichment/dbt/manifest.py +298 -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 +359 -0
- pretensor/entities/llm_extract.py +66 -0
- pretensor/graph_models/__init__.py +14 -0
- pretensor/graph_models/base.py +11 -0
- pretensor/graph_models/edge.py +35 -0
- pretensor/graph_models/entity.py +21 -0
- pretensor/graph_models/node.py +75 -0
- pretensor/graph_models/relationship.py +27 -0
- pretensor/intelligence/__init__.py +33 -0
- pretensor/intelligence/cluster_labeler.py +335 -0
- pretensor/intelligence/clustering.py +162 -0
- pretensor/intelligence/combining.py +32 -0
- pretensor/intelligence/discovery.py +114 -0
- pretensor/intelligence/embeddings.py +198 -0
- pretensor/intelligence/graph_export.py +139 -0
- pretensor/intelligence/heuristic.py +540 -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 +60 -0
- pretensor/intelligence/metric_templates.py +193 -0
- pretensor/intelligence/pipeline.py +254 -0
- pretensor/intelligence/schema_classification.py +306 -0
- pretensor/intelligence/scoring.py +66 -0
- pretensor/intelligence/shadow_alias.py +101 -0
- pretensor/intelligence/statistical.py +50 -0
- pretensor/intelligence/steps.py +161 -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 +236 -0
- pretensor/introspection/models/dsn.py +442 -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 +248 -0
- pretensor/mcp/resources/__init__.py +19 -0
- pretensor/mcp/resources/markdown.py +420 -0
- pretensor/mcp/server.py +723 -0
- pretensor/mcp/service.py +47 -0
- pretensor/mcp/service_context.py +126 -0
- pretensor/mcp/service_registry.py +242 -0
- pretensor/mcp/tool_registry.py +126 -0
- pretensor/mcp/tools/__init__.py +1 -0
- pretensor/mcp/tools/compile_metric.py +87 -0
- pretensor/mcp/tools/context.py +894 -0
- pretensor/mcp/tools/cypher.py +333 -0
- pretensor/mcp/tools/detect_changes.py +226 -0
- pretensor/mcp/tools/impact.py +165 -0
- pretensor/mcp/tools/list.py +110 -0
- pretensor/mcp/tools/schema.py +128 -0
- pretensor/mcp/tools/search.py +108 -0
- pretensor/mcp/tools/traverse.py +753 -0
- pretensor/mcp/tools/validate_sql.py +74 -0
- pretensor/observability.py +203 -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 +346 -0
- pretensor/semantic/yaml_layer.py +182 -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.0a0.dist-info/METADATA +175 -0
- pretensor-0.1.0a0.dist-info/RECORD +138 -0
- pretensor-0.1.0a0.dist-info/WHEEL +4 -0
- pretensor-0.1.0a0.dist-info/entry_points.txt +2 -0
- pretensor-0.1.0a0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
"""``pretensor reindex`` command (snapshot diff + graph patch)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from pretensor.cli import constants as cli_constants
|
|
14
|
+
from pretensor.cli.config_file import (
|
|
15
|
+
get_cli_config,
|
|
16
|
+
resolve_optional_str_option,
|
|
17
|
+
resolve_path_option,
|
|
18
|
+
)
|
|
19
|
+
from pretensor.cli.dbt_enrichment import (
|
|
20
|
+
apply_dbt_enrichment_cli,
|
|
21
|
+
preload_dbt_manifest,
|
|
22
|
+
)
|
|
23
|
+
from pretensor.cli.paths import default_connection_name, keystore_path
|
|
24
|
+
from pretensor.config import PretensorConfig
|
|
25
|
+
from pretensor.connectors.inspect import inspect
|
|
26
|
+
from pretensor.connectors.snapshot import diff_snapshots
|
|
27
|
+
from pretensor.core.dsn_crypto import DSNEncryptor
|
|
28
|
+
from pretensor.core.registry import DatabaseNotFoundError, GraphRegistry
|
|
29
|
+
from pretensor.core.store import KuzuStore
|
|
30
|
+
from pretensor.intelligence.discovery import RelationshipDiscovery
|
|
31
|
+
from pretensor.intelligence.metric_templates import MetricTemplateBuilder
|
|
32
|
+
from pretensor.intelligence.pipeline import run_intelligence_layer_sync
|
|
33
|
+
from pretensor.introspection.models.dsn import (
|
|
34
|
+
connection_config_from_registry_dsn,
|
|
35
|
+
connection_config_from_source,
|
|
36
|
+
connection_config_from_url,
|
|
37
|
+
dsn_from_source,
|
|
38
|
+
registry_dialect_for,
|
|
39
|
+
validate_source_env_vars,
|
|
40
|
+
)
|
|
41
|
+
from pretensor.observability import log_timed_operation
|
|
42
|
+
from pretensor.skills.generator import SkillGenerator
|
|
43
|
+
from pretensor.staleness.graph_patcher import GraphPatcher
|
|
44
|
+
from pretensor.staleness.impact_analyzer import ImpactAnalyzer
|
|
45
|
+
from pretensor.staleness.snapshot_store import SnapshotStore
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def register_reindex_command(app: typer.Typer, *, console: Console) -> None:
|
|
51
|
+
@app.command("reindex")
|
|
52
|
+
def reindex_command(
|
|
53
|
+
dsn: str | None = typer.Argument(
|
|
54
|
+
None,
|
|
55
|
+
help=(
|
|
56
|
+
"Database DSN URL (must match the indexed connection unless --dialect is set). "
|
|
57
|
+
"Mutually exclusive with --source and --all."
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
source: str | None = typer.Option(
|
|
61
|
+
None,
|
|
62
|
+
"--source",
|
|
63
|
+
"-s",
|
|
64
|
+
help="Named source from .pretensor/config.yaml sources: block.",
|
|
65
|
+
),
|
|
66
|
+
all_sources: bool = typer.Option(
|
|
67
|
+
False,
|
|
68
|
+
"--all",
|
|
69
|
+
help="Reindex all sources defined in .pretensor/config.yaml.",
|
|
70
|
+
),
|
|
71
|
+
dialect: str | None = typer.Option(
|
|
72
|
+
None,
|
|
73
|
+
"--dialect",
|
|
74
|
+
help="Override connector for this run: postgres, postgresql, snowflake, or bigquery.",
|
|
75
|
+
),
|
|
76
|
+
database: str | None = typer.Option(
|
|
77
|
+
None,
|
|
78
|
+
"--database",
|
|
79
|
+
"-d",
|
|
80
|
+
help="Registry connection name (defaults to database name from the DSN).",
|
|
81
|
+
),
|
|
82
|
+
dry_run: bool = typer.Option(
|
|
83
|
+
False,
|
|
84
|
+
"--dry-run",
|
|
85
|
+
help="Show schema diff and planned mutations without writing the graph.",
|
|
86
|
+
),
|
|
87
|
+
recompute_intelligence: bool = typer.Option(
|
|
88
|
+
False,
|
|
89
|
+
"--recompute-intelligence",
|
|
90
|
+
help="After patching, re-run relationship discovery and clustering/join paths.",
|
|
91
|
+
),
|
|
92
|
+
state_dir: Path = typer.Option(
|
|
93
|
+
cli_constants.DEFAULT_STATE_DIR,
|
|
94
|
+
"--state-dir",
|
|
95
|
+
help="Directory for registry.json, snapshots, and graph files.",
|
|
96
|
+
file_okay=False,
|
|
97
|
+
dir_okay=True,
|
|
98
|
+
writable=True,
|
|
99
|
+
resolve_path=True,
|
|
100
|
+
),
|
|
101
|
+
skills_target: str = typer.Option(
|
|
102
|
+
"claude",
|
|
103
|
+
"--skills-target",
|
|
104
|
+
help=(
|
|
105
|
+
"Where to write the generated skill after a successful patch: "
|
|
106
|
+
"claude, cursor, all, or a file path."
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
dbt_manifest: Path | None = typer.Option(
|
|
110
|
+
None,
|
|
111
|
+
"--dbt-manifest",
|
|
112
|
+
help=(
|
|
113
|
+
"Path to dbt target/manifest.json. Run `dbt compile` or `dbt run` first "
|
|
114
|
+
"so the manifest exists; enrichment runs after the graph is updated."
|
|
115
|
+
),
|
|
116
|
+
file_okay=True,
|
|
117
|
+
dir_okay=False,
|
|
118
|
+
resolve_path=True,
|
|
119
|
+
),
|
|
120
|
+
dbt_sources: Path | None = typer.Option(
|
|
121
|
+
None,
|
|
122
|
+
"--dbt-sources",
|
|
123
|
+
help=(
|
|
124
|
+
"Path to dbt sources.json (e.g. from `dbt source freshness`). "
|
|
125
|
+
"Defaults to target/sources.json next to the manifest when that file exists."
|
|
126
|
+
),
|
|
127
|
+
file_okay=True,
|
|
128
|
+
dir_okay=False,
|
|
129
|
+
resolve_path=True,
|
|
130
|
+
),
|
|
131
|
+
ctx: typer.Context = typer.Option(
|
|
132
|
+
None,
|
|
133
|
+
hidden=True,
|
|
134
|
+
),
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Re-introspect the DB, diff against the last snapshot, and patch the Kuzu graph."""
|
|
137
|
+
cli_config = get_cli_config(ctx)
|
|
138
|
+
|
|
139
|
+
# --- mutual exclusion -------------------------------------------------
|
|
140
|
+
modes = sum([dsn is not None, source is not None, all_sources])
|
|
141
|
+
if modes > 1:
|
|
142
|
+
console.print(
|
|
143
|
+
"[red]DSN argument, --source, and --all are mutually exclusive.[/red]"
|
|
144
|
+
)
|
|
145
|
+
raise typer.Exit(1)
|
|
146
|
+
|
|
147
|
+
state_dir = resolve_path_option(
|
|
148
|
+
ctx,
|
|
149
|
+
param_name="state_dir",
|
|
150
|
+
cli_value=state_dir,
|
|
151
|
+
config_value=cli_config.state_dir,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# --- --all: iterate all configured sources ----------------------------
|
|
155
|
+
if all_sources:
|
|
156
|
+
if not cli_config.sources:
|
|
157
|
+
console.print(
|
|
158
|
+
"[red]No sources defined in config.[/red] "
|
|
159
|
+
"Add a `sources:` section to .pretensor/config.yaml."
|
|
160
|
+
)
|
|
161
|
+
raise typer.Exit(1)
|
|
162
|
+
results: list[tuple[str, bool, str]] = []
|
|
163
|
+
for src_name in cli_config.sources:
|
|
164
|
+
console.print(
|
|
165
|
+
f"\n[bold]{'─' * 40}[/bold]\n"
|
|
166
|
+
f"[bold blue]Reindexing source:[/bold blue] {src_name}\n"
|
|
167
|
+
)
|
|
168
|
+
src_cfg = cli_config.sources[src_name]
|
|
169
|
+
missing_vars = validate_source_env_vars(src_cfg)
|
|
170
|
+
if missing_vars:
|
|
171
|
+
msg = f"missing env vars: {', '.join(missing_vars)}"
|
|
172
|
+
console.print(f"[yellow]Skipping {src_name}:[/yellow] {msg}")
|
|
173
|
+
logger.warning("Skipping source %s: %s", src_name, msg)
|
|
174
|
+
results.append((src_name, False, f"skipped ({msg})"))
|
|
175
|
+
continue
|
|
176
|
+
try:
|
|
177
|
+
_run_single_reindex(
|
|
178
|
+
console=console,
|
|
179
|
+
cli_config=cli_config,
|
|
180
|
+
ctx=ctx,
|
|
181
|
+
dsn=None,
|
|
182
|
+
source_name=src_name,
|
|
183
|
+
dialect=dialect,
|
|
184
|
+
database=src_name,
|
|
185
|
+
dry_run=dry_run,
|
|
186
|
+
recompute_intelligence=recompute_intelligence,
|
|
187
|
+
state_dir=state_dir,
|
|
188
|
+
skills_target=skills_target,
|
|
189
|
+
dbt_manifest=dbt_manifest,
|
|
190
|
+
dbt_sources=dbt_sources,
|
|
191
|
+
)
|
|
192
|
+
results.append((src_name, True, "ok"))
|
|
193
|
+
except typer.Exit:
|
|
194
|
+
logger.warning("Source %s exited with failure", src_name)
|
|
195
|
+
results.append((src_name, False, "failed"))
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.exception("Error reindexing source %s", src_name)
|
|
198
|
+
console.print(f"[red]Error reindexing {src_name}:[/red] {e}")
|
|
199
|
+
results.append((src_name, False, str(e)))
|
|
200
|
+
|
|
201
|
+
console.print(f"\n[bold]{'─' * 40}[/bold]")
|
|
202
|
+
console.print("[bold]Summary:[/bold]")
|
|
203
|
+
failed = 0
|
|
204
|
+
for src_name, ok, msg in results:
|
|
205
|
+
status = "[green]✓[/green]" if ok else "[red]✗[/red]"
|
|
206
|
+
console.print(f" {status} {src_name}: {msg}")
|
|
207
|
+
if not ok:
|
|
208
|
+
failed += 1
|
|
209
|
+
if failed:
|
|
210
|
+
raise typer.Exit(1)
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
# --- --source: single named source ------------------------------------
|
|
214
|
+
if source is not None:
|
|
215
|
+
if source not in cli_config.sources:
|
|
216
|
+
available = ", ".join(cli_config.sources) or "(none)"
|
|
217
|
+
console.print(
|
|
218
|
+
f"[red]Unknown source {source!r}.[/red] "
|
|
219
|
+
f"Available: {available}"
|
|
220
|
+
)
|
|
221
|
+
raise typer.Exit(1)
|
|
222
|
+
_run_single_reindex(
|
|
223
|
+
console=console,
|
|
224
|
+
cli_config=cli_config,
|
|
225
|
+
ctx=ctx,
|
|
226
|
+
dsn=None,
|
|
227
|
+
source_name=source,
|
|
228
|
+
dialect=dialect,
|
|
229
|
+
database=source,
|
|
230
|
+
dry_run=dry_run,
|
|
231
|
+
recompute_intelligence=recompute_intelligence,
|
|
232
|
+
state_dir=state_dir,
|
|
233
|
+
skills_target=skills_target,
|
|
234
|
+
dbt_manifest=dbt_manifest,
|
|
235
|
+
dbt_sources=dbt_sources,
|
|
236
|
+
)
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
# --- DSN argument (original path) or no mode (use registry) -----------
|
|
240
|
+
if dsn is None:
|
|
241
|
+
# No DSN, no source, no --all: need at least a database name
|
|
242
|
+
database = resolve_optional_str_option(
|
|
243
|
+
ctx,
|
|
244
|
+
param_name="database",
|
|
245
|
+
cli_value=database,
|
|
246
|
+
config_value=cli_config.connection_defaults.name,
|
|
247
|
+
)
|
|
248
|
+
if not database:
|
|
249
|
+
console.print(
|
|
250
|
+
"[red]Provide a DSN argument, --source <name>, --all, "
|
|
251
|
+
"or --database <name>.[/red]"
|
|
252
|
+
)
|
|
253
|
+
raise typer.Exit(1)
|
|
254
|
+
|
|
255
|
+
_run_single_reindex(
|
|
256
|
+
console=console,
|
|
257
|
+
cli_config=cli_config,
|
|
258
|
+
ctx=ctx,
|
|
259
|
+
dsn=dsn,
|
|
260
|
+
source_name=None,
|
|
261
|
+
dialect=dialect,
|
|
262
|
+
database=database,
|
|
263
|
+
dry_run=dry_run,
|
|
264
|
+
recompute_intelligence=recompute_intelligence,
|
|
265
|
+
state_dir=state_dir,
|
|
266
|
+
skills_target=skills_target,
|
|
267
|
+
dbt_manifest=dbt_manifest,
|
|
268
|
+
dbt_sources=dbt_sources,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _run_single_reindex(
|
|
273
|
+
*,
|
|
274
|
+
console: Console,
|
|
275
|
+
cli_config: object,
|
|
276
|
+
ctx: typer.Context | None,
|
|
277
|
+
dsn: str | None,
|
|
278
|
+
source_name: str | None,
|
|
279
|
+
dialect: str | None,
|
|
280
|
+
database: str | None,
|
|
281
|
+
dry_run: bool,
|
|
282
|
+
recompute_intelligence: bool,
|
|
283
|
+
state_dir: Path,
|
|
284
|
+
skills_target: str,
|
|
285
|
+
dbt_manifest: Path | None,
|
|
286
|
+
dbt_sources: Path | None,
|
|
287
|
+
) -> None:
|
|
288
|
+
"""Core reindex logic shared by DSN, --source, and --all paths."""
|
|
289
|
+
from pretensor.cli.config_file import PretensorCliConfig
|
|
290
|
+
|
|
291
|
+
assert isinstance(cli_config, PretensorCliConfig)
|
|
292
|
+
|
|
293
|
+
database = resolve_optional_str_option(
|
|
294
|
+
ctx,
|
|
295
|
+
param_name="database",
|
|
296
|
+
cli_value=database,
|
|
297
|
+
config_value=cli_config.connection_defaults.name,
|
|
298
|
+
)
|
|
299
|
+
dialect = resolve_optional_str_option(
|
|
300
|
+
ctx,
|
|
301
|
+
param_name="dialect",
|
|
302
|
+
cli_value=dialect,
|
|
303
|
+
config_value=cli_config.connection_defaults.dialect,
|
|
304
|
+
)
|
|
305
|
+
total_started = time.perf_counter()
|
|
306
|
+
|
|
307
|
+
# Resolve connection_name and DSN
|
|
308
|
+
if source_name is not None and source_name in cli_config.sources:
|
|
309
|
+
src_cfg = cli_config.sources[source_name]
|
|
310
|
+
connection_name = source_name
|
|
311
|
+
try:
|
|
312
|
+
dsn_resolved = dsn_from_source(source_name, src_cfg)
|
|
313
|
+
except ValueError as e:
|
|
314
|
+
console.print(f"[red]Cannot build DSN from source:[/red] {e}")
|
|
315
|
+
raise typer.Exit(1) from e
|
|
316
|
+
elif dsn is not None:
|
|
317
|
+
connection_name = database or default_connection_name(dsn)
|
|
318
|
+
dsn_resolved = dsn
|
|
319
|
+
else:
|
|
320
|
+
connection_name = database or ""
|
|
321
|
+
dsn_resolved = None
|
|
322
|
+
|
|
323
|
+
preloaded_manifest = None
|
|
324
|
+
preloaded_sources: Path | None = None
|
|
325
|
+
if dbt_manifest is not None:
|
|
326
|
+
preloaded_manifest, preloaded_sources = preload_dbt_manifest(
|
|
327
|
+
dbt_manifest, dbt_sources, console=console
|
|
328
|
+
)
|
|
329
|
+
reg_path = state_dir / cli_constants.REGISTRY_FILENAME
|
|
330
|
+
if not reg_path.exists():
|
|
331
|
+
console.print("[red]No registry found.[/red] Run `pretensor index` first.")
|
|
332
|
+
raise typer.Exit(1)
|
|
333
|
+
reg = GraphRegistry(reg_path).load()
|
|
334
|
+
ks = keystore_path(state_dir)
|
|
335
|
+
try:
|
|
336
|
+
entry = reg.require(connection_name)
|
|
337
|
+
except DatabaseNotFoundError:
|
|
338
|
+
console.print(
|
|
339
|
+
f"[red]Unknown connection {connection_name!r}.[/red] "
|
|
340
|
+
"Use `pretensor list` or `--database`."
|
|
341
|
+
)
|
|
342
|
+
raise typer.Exit(1)
|
|
343
|
+
|
|
344
|
+
# For source-based reindex, use source config directly; otherwise use registry DSN
|
|
345
|
+
if source_name is not None and source_name in cli_config.sources:
|
|
346
|
+
src_cfg = cli_config.sources[source_name]
|
|
347
|
+
try:
|
|
348
|
+
config = connection_config_from_source(source_name, src_cfg)
|
|
349
|
+
except ValueError as e:
|
|
350
|
+
console.print(f"[red]Invalid source config:[/red] {e}")
|
|
351
|
+
raise typer.Exit(1) from e
|
|
352
|
+
dsn_for_registry = dsn_resolved or ""
|
|
353
|
+
else:
|
|
354
|
+
decryptor = DSNEncryptor(ks) if entry.dsn_encrypted else None
|
|
355
|
+
dsn_for_reindex = entry.plaintext_dsn(decryptor)
|
|
356
|
+
dsn_for_registry = dsn_resolved or dsn_for_reindex
|
|
357
|
+
|
|
358
|
+
if dialect is not None:
|
|
359
|
+
config = connection_config_from_url(
|
|
360
|
+
dsn_for_reindex, connection_name, dialect_override=dialect
|
|
361
|
+
)
|
|
362
|
+
else:
|
|
363
|
+
config = connection_config_from_registry_dsn(
|
|
364
|
+
dsn_for_reindex, connection_name, entry.dialect
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
snap_store = SnapshotStore(state_dir)
|
|
368
|
+
old_snapshot = snap_store.load(connection_name)
|
|
369
|
+
if old_snapshot is None:
|
|
370
|
+
console.print(
|
|
371
|
+
"[red]No saved snapshot for this connection.[/red] "
|
|
372
|
+
"Run `pretensor index` once, then use `reindex`."
|
|
373
|
+
)
|
|
374
|
+
raise typer.Exit(1)
|
|
375
|
+
|
|
376
|
+
console.print(f"[bold blue]Re-introspecting[/bold blue] '{connection_name}'...")
|
|
377
|
+
try:
|
|
378
|
+
with log_timed_operation(
|
|
379
|
+
logger,
|
|
380
|
+
event="reindex.inspect",
|
|
381
|
+
connection_name=connection_name,
|
|
382
|
+
dialect=str(config.type),
|
|
383
|
+
):
|
|
384
|
+
new_snapshot = inspect(config)
|
|
385
|
+
except Exception as e:
|
|
386
|
+
console.print(
|
|
387
|
+
f"[red]Cannot connect to database:[/red] {e}\n"
|
|
388
|
+
"Check that the database is reachable and the DSN is correct."
|
|
389
|
+
)
|
|
390
|
+
raise typer.Exit(1) from e
|
|
391
|
+
|
|
392
|
+
changes = diff_snapshots(old_snapshot, new_snapshot)
|
|
393
|
+
graph_path = Path(entry.unified_graph_path or entry.graph_path)
|
|
394
|
+
if not graph_path.exists():
|
|
395
|
+
console.print(f"[red]Graph file missing:[/red] {graph_path}")
|
|
396
|
+
raise typer.Exit(1)
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
store = KuzuStore(graph_path)
|
|
400
|
+
except Exception as e:
|
|
401
|
+
console.print(
|
|
402
|
+
f"[red]Cannot open graph file {graph_path}:[/red] {e}\n"
|
|
403
|
+
"The file may be corrupt. Run `pretensor index` to rebuild it."
|
|
404
|
+
)
|
|
405
|
+
raise typer.Exit(1) from e
|
|
406
|
+
try:
|
|
407
|
+
analyzer = ImpactAnalyzer(store)
|
|
408
|
+
impact = analyzer.analyze(
|
|
409
|
+
changes,
|
|
410
|
+
connection_name=connection_name,
|
|
411
|
+
database_key=str(entry.database),
|
|
412
|
+
)
|
|
413
|
+
if changes:
|
|
414
|
+
console.print(
|
|
415
|
+
f"\n[bold]Schema changes[/bold] for {connection_name!r} "
|
|
416
|
+
f"({len(changes)} item(s)):"
|
|
417
|
+
)
|
|
418
|
+
for ch in changes:
|
|
419
|
+
col = f".{ch.column_name}" if ch.column_name else ""
|
|
420
|
+
console.print(
|
|
421
|
+
f" {ch.change_type.value} {ch.target.value}: "
|
|
422
|
+
f"{ch.schema_name}.{ch.table_name}{col}"
|
|
423
|
+
+ (f" — {ch.details}" if ch.details else "")
|
|
424
|
+
)
|
|
425
|
+
console.print(f"\n[dim]{impact.summary}[/dim]\n")
|
|
426
|
+
else:
|
|
427
|
+
console.print("[green]No schema changes since last snapshot.[/green]")
|
|
428
|
+
|
|
429
|
+
patcher = GraphPatcher(store)
|
|
430
|
+
with log_timed_operation(
|
|
431
|
+
logger,
|
|
432
|
+
event="reindex.apply_patch",
|
|
433
|
+
connection_name=connection_name,
|
|
434
|
+
dry_run=dry_run,
|
|
435
|
+
change_count=len(changes),
|
|
436
|
+
):
|
|
437
|
+
patch = patcher.apply(changes, new_snapshot, dry_run=dry_run)
|
|
438
|
+
if changes and dry_run:
|
|
439
|
+
console.print("[bold]Planned graph updates (dry-run):[/bold]")
|
|
440
|
+
console.print(
|
|
441
|
+
f" tables +{patch.tables_added} / -{patch.tables_removed}, "
|
|
442
|
+
f"columns +{patch.columns_added} / -{patch.columns_removed} / "
|
|
443
|
+
f"~{patch.columns_updated}, "
|
|
444
|
+
f"FK edges removed (column touch): {patch.fk_edges_removed}, "
|
|
445
|
+
f"inferred removed: {patch.inferred_joins_removed}"
|
|
446
|
+
)
|
|
447
|
+
console.print("\n[yellow]Run without --dry-run to apply.[/yellow]")
|
|
448
|
+
return
|
|
449
|
+
|
|
450
|
+
if changes and not dry_run:
|
|
451
|
+
MetricTemplateBuilder.mark_stale_for_database(
|
|
452
|
+
store, str(new_snapshot.database)
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
if not changes:
|
|
456
|
+
if preloaded_manifest is not None and not dry_run:
|
|
457
|
+
apply_dbt_enrichment_cli(
|
|
458
|
+
manifest=preloaded_manifest,
|
|
459
|
+
sources_path=preloaded_sources,
|
|
460
|
+
store=store,
|
|
461
|
+
connection_name=connection_name,
|
|
462
|
+
console=console,
|
|
463
|
+
)
|
|
464
|
+
indexed_at = datetime.now(timezone.utc)
|
|
465
|
+
enc = DSNEncryptor(ks) if ks.exists() and entry.dsn_encrypted else None
|
|
466
|
+
reg.upsert(
|
|
467
|
+
connection_name=connection_name,
|
|
468
|
+
database=new_snapshot.database,
|
|
469
|
+
dsn=dsn_for_registry,
|
|
470
|
+
graph_path=graph_path,
|
|
471
|
+
unified_graph_path=Path(entry.unified_graph_path)
|
|
472
|
+
if entry.unified_graph_path
|
|
473
|
+
else None,
|
|
474
|
+
indexed_at=indexed_at,
|
|
475
|
+
encrypt_dsn=bool(entry.dsn_encrypted),
|
|
476
|
+
encryptor=enc,
|
|
477
|
+
dialect=registry_dialect_for(config.type),
|
|
478
|
+
table_count=entry.table_count,
|
|
479
|
+
dbt_manifest_path=(
|
|
480
|
+
str(dbt_manifest) if dbt_manifest is not None
|
|
481
|
+
else entry.dbt_manifest_path
|
|
482
|
+
),
|
|
483
|
+
llm_enrichment_ran=entry.llm_enrichment_ran,
|
|
484
|
+
)
|
|
485
|
+
reg.save()
|
|
486
|
+
snap_store.save(connection_name, new_snapshot)
|
|
487
|
+
console.print("[dim]Registry timestamp refreshed.[/dim]")
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
if recompute_intelligence and not dry_run:
|
|
491
|
+
with log_timed_operation(
|
|
492
|
+
logger,
|
|
493
|
+
event="reindex.relationship_discovery",
|
|
494
|
+
connection_name=connection_name,
|
|
495
|
+
):
|
|
496
|
+
RelationshipDiscovery(store).discover(new_snapshot)
|
|
497
|
+
with log_timed_operation(
|
|
498
|
+
logger,
|
|
499
|
+
event="reindex.intelligence_sync",
|
|
500
|
+
connection_name=connection_name,
|
|
501
|
+
):
|
|
502
|
+
run_intelligence_layer_sync(
|
|
503
|
+
store,
|
|
504
|
+
new_snapshot.database,
|
|
505
|
+
config=PretensorConfig(graph=cli_config.graph),
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
if preloaded_manifest is not None and not dry_run:
|
|
509
|
+
apply_dbt_enrichment_cli(
|
|
510
|
+
manifest=preloaded_manifest,
|
|
511
|
+
sources_path=preloaded_sources,
|
|
512
|
+
store=store,
|
|
513
|
+
connection_name=connection_name,
|
|
514
|
+
console=console,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
snap_store.save(connection_name, new_snapshot)
|
|
518
|
+
indexed_at = datetime.now(timezone.utc)
|
|
519
|
+
tc_rows = store.query_all_rows(
|
|
520
|
+
"""
|
|
521
|
+
MATCH (t:SchemaTable {connection_name: $cn})
|
|
522
|
+
RETURN count(*)
|
|
523
|
+
""",
|
|
524
|
+
{"cn": connection_name},
|
|
525
|
+
)
|
|
526
|
+
table_count = (
|
|
527
|
+
int(tc_rows[0][0]) if tc_rows and tc_rows[0][0] is not None else 0
|
|
528
|
+
)
|
|
529
|
+
enc = DSNEncryptor(ks) if ks.exists() and entry.dsn_encrypted else None
|
|
530
|
+
reg.upsert(
|
|
531
|
+
connection_name=connection_name,
|
|
532
|
+
database=new_snapshot.database,
|
|
533
|
+
dsn=dsn_for_registry,
|
|
534
|
+
graph_path=graph_path,
|
|
535
|
+
unified_graph_path=Path(entry.unified_graph_path)
|
|
536
|
+
if entry.unified_graph_path
|
|
537
|
+
else None,
|
|
538
|
+
indexed_at=indexed_at,
|
|
539
|
+
encrypt_dsn=bool(entry.dsn_encrypted),
|
|
540
|
+
encryptor=enc,
|
|
541
|
+
dialect=registry_dialect_for(config.type),
|
|
542
|
+
table_count=table_count,
|
|
543
|
+
dbt_manifest_path=(
|
|
544
|
+
str(dbt_manifest) if dbt_manifest is not None
|
|
545
|
+
else entry.dbt_manifest_path
|
|
546
|
+
),
|
|
547
|
+
llm_enrichment_ran=entry.llm_enrichment_ran,
|
|
548
|
+
)
|
|
549
|
+
reg.save()
|
|
550
|
+
|
|
551
|
+
skill_store = KuzuStore(graph_path)
|
|
552
|
+
try:
|
|
553
|
+
refreshed = reg.get(connection_name)
|
|
554
|
+
if refreshed is not None:
|
|
555
|
+
SkillGenerator.write_for_index(
|
|
556
|
+
store=skill_store,
|
|
557
|
+
entry=refreshed,
|
|
558
|
+
skills_target=skills_target,
|
|
559
|
+
)
|
|
560
|
+
finally:
|
|
561
|
+
skill_store.close()
|
|
562
|
+
|
|
563
|
+
console.print(f"[green]Patched graph:[/green] {graph_path}")
|
|
564
|
+
console.print(
|
|
565
|
+
f"[green]Snapshot saved:[/green] {snap_store.path_for(connection_name)}"
|
|
566
|
+
)
|
|
567
|
+
logger.info(
|
|
568
|
+
"reindex completed in %.2fms",
|
|
569
|
+
(time.perf_counter() - total_started) * 1000,
|
|
570
|
+
extra={
|
|
571
|
+
"event": "reindex.total",
|
|
572
|
+
"status": "ok",
|
|
573
|
+
"duration_ms": (time.perf_counter() - total_started) * 1000,
|
|
574
|
+
"connection_name": connection_name,
|
|
575
|
+
"dry_run": dry_run,
|
|
576
|
+
"change_count": len(changes),
|
|
577
|
+
"recompute_intelligence": recompute_intelligence,
|
|
578
|
+
},
|
|
579
|
+
)
|
|
580
|
+
finally:
|
|
581
|
+
store.close()
|