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.
Files changed (198) hide show
  1. pretensor/__init__.py +50 -0
  2. pretensor/benchmark/__init__.py +54 -0
  3. pretensor/benchmark/cli.py +294 -0
  4. pretensor/benchmark/fixtures.py +84 -0
  5. pretensor/benchmark/l1/__init__.py +23 -0
  6. pretensor/benchmark/l1/metrics.py +141 -0
  7. pretensor/benchmark/l1/pipeline.py +188 -0
  8. pretensor/benchmark/l1/runner.py +245 -0
  9. pretensor/benchmark/l2/__init__.py +27 -0
  10. pretensor/benchmark/l2/gold.py +236 -0
  11. pretensor/benchmark/l2/metrics.py +146 -0
  12. pretensor/benchmark/l2/pipeline.py +124 -0
  13. pretensor/benchmark/l2/runner.py +530 -0
  14. pretensor/benchmark/l3/__init__.py +73 -0
  15. pretensor/benchmark/l3/agent.py +316 -0
  16. pretensor/benchmark/l3/db.py +188 -0
  17. pretensor/benchmark/l3/gold.py +85 -0
  18. pretensor/benchmark/l3/llm_client.py +395 -0
  19. pretensor/benchmark/l3/mcp_client.py +357 -0
  20. pretensor/benchmark/l3/pretensor_runner.py +456 -0
  21. pretensor/benchmark/l3/prompt.py +132 -0
  22. pretensor/benchmark/l3/runner.py +358 -0
  23. pretensor/benchmark/l3/sql_equivalence.py +176 -0
  24. pretensor/benchmark/release_gate.py +448 -0
  25. pretensor/benchmark/results.py +298 -0
  26. pretensor/benchmark/runner.py +109 -0
  27. pretensor/cli/__init__.py +1 -0
  28. pretensor/cli/commands/_source_runner.py +147 -0
  29. pretensor/cli/commands/analyze.py +201 -0
  30. pretensor/cli/commands/connections/__init__.py +7 -0
  31. pretensor/cli/commands/connections/add_remove.py +126 -0
  32. pretensor/cli/commands/connections/register.py +12 -0
  33. pretensor/cli/commands/export.py +131 -0
  34. pretensor/cli/commands/index.py +559 -0
  35. pretensor/cli/commands/list.py +76 -0
  36. pretensor/cli/commands/quickstart.py +207 -0
  37. pretensor/cli/commands/reindex.py +646 -0
  38. pretensor/cli/commands/semantic.py +190 -0
  39. pretensor/cli/commands/serve.py +144 -0
  40. pretensor/cli/commands/sync_grants.py +149 -0
  41. pretensor/cli/commands/validate.py +176 -0
  42. pretensor/cli/config_file.py +442 -0
  43. pretensor/cli/constants.py +10 -0
  44. pretensor/cli/dbt_enrichment.py +96 -0
  45. pretensor/cli/main.py +109 -0
  46. pretensor/cli/paths.py +43 -0
  47. pretensor/cli/plugin.py +52 -0
  48. pretensor/config.py +226 -0
  49. pretensor/connectors/__init__.py +29 -0
  50. pretensor/connectors/base.py +165 -0
  51. pretensor/connectors/bigquery.py +468 -0
  52. pretensor/connectors/inspect.py +321 -0
  53. pretensor/connectors/lineage_sqlglot.py +97 -0
  54. pretensor/connectors/models.py +130 -0
  55. pretensor/connectors/mysql.py +402 -0
  56. pretensor/connectors/pg_array_parse.py +53 -0
  57. pretensor/connectors/postgres.py +938 -0
  58. pretensor/connectors/registry.py +93 -0
  59. pretensor/connectors/snapshot.py +244 -0
  60. pretensor/connectors/snowflake.py +908 -0
  61. pretensor/core/__init__.py +1 -0
  62. pretensor/core/builder.py +307 -0
  63. pretensor/core/dsn_crypto.py +51 -0
  64. pretensor/core/graph_schema_manager.py +246 -0
  65. pretensor/core/graph_store.py +1226 -0
  66. pretensor/core/ids.py +101 -0
  67. pretensor/core/portable_export.py +276 -0
  68. pretensor/core/query_runner.py +67 -0
  69. pretensor/core/registry.py +209 -0
  70. pretensor/core/schema.py +473 -0
  71. pretensor/core/secure_io.py +93 -0
  72. pretensor/core/store.py +469 -0
  73. pretensor/enrichment/__init__.py +1 -0
  74. pretensor/enrichment/analyze/__init__.py +0 -0
  75. pretensor/enrichment/analyze/classify.py +49 -0
  76. pretensor/enrichment/analyze/extract_python.py +196 -0
  77. pretensor/enrichment/analyze/parse.py +141 -0
  78. pretensor/enrichment/analyze/pipeline.py +195 -0
  79. pretensor/enrichment/analyze/summary.py +38 -0
  80. pretensor/enrichment/analyze/walker.py +98 -0
  81. pretensor/enrichment/analyze/writers.py +214 -0
  82. pretensor/enrichment/dbt/__init__.py +30 -0
  83. pretensor/enrichment/dbt/lineage.py +100 -0
  84. pretensor/enrichment/dbt/manifest.py +300 -0
  85. pretensor/enrichment/dbt/metadata.py +263 -0
  86. pretensor/enrichment/dbt/pipeline.py +77 -0
  87. pretensor/enrichment/dbt/resolution.py +101 -0
  88. pretensor/enrichment/dbt/signals.py +305 -0
  89. pretensor/entities/__init__.py +27 -0
  90. pretensor/entities/builder.py +63 -0
  91. pretensor/entities/classifier.py +383 -0
  92. pretensor/entities/llm_extract.py +66 -0
  93. pretensor/errors.py +35 -0
  94. pretensor/graph_models/__init__.py +17 -0
  95. pretensor/graph_models/base.py +11 -0
  96. pretensor/graph_models/consumer.py +71 -0
  97. pretensor/graph_models/edge.py +35 -0
  98. pretensor/graph_models/entity.py +21 -0
  99. pretensor/graph_models/node.py +79 -0
  100. pretensor/graph_models/relationship.py +33 -0
  101. pretensor/integrations/__init__.py +42 -0
  102. pretensor/integrations/_base.py +138 -0
  103. pretensor/integrations/google_adk.py +49 -0
  104. pretensor/integrations/langchain.py +55 -0
  105. pretensor/integrations/llamaindex.py +53 -0
  106. pretensor/intelligence/__init__.py +33 -0
  107. pretensor/intelligence/cluster_labeler.py +425 -0
  108. pretensor/intelligence/clustering.py +168 -0
  109. pretensor/intelligence/combining.py +32 -0
  110. pretensor/intelligence/discovery.py +114 -0
  111. pretensor/intelligence/embeddings.py +317 -0
  112. pretensor/intelligence/graph_export.py +200 -0
  113. pretensor/intelligence/heuristic.py +544 -0
  114. pretensor/intelligence/join_paths/__init__.py +130 -0
  115. pretensor/intelligence/join_paths/on_demand.py +516 -0
  116. pretensor/intelligence/join_paths/storage.py +70 -0
  117. pretensor/intelligence/llm_infer.py +78 -0
  118. pretensor/intelligence/llm_runtime.py +62 -0
  119. pretensor/intelligence/metric_templates.py +193 -0
  120. pretensor/intelligence/pipeline.py +364 -0
  121. pretensor/intelligence/role_exemplars.py +263 -0
  122. pretensor/intelligence/schema_classification.py +360 -0
  123. pretensor/intelligence/scoring.py +76 -0
  124. pretensor/intelligence/semantic.py +240 -0
  125. pretensor/intelligence/shadow_alias.py +101 -0
  126. pretensor/intelligence/statistical.py +50 -0
  127. pretensor/intelligence/steps.py +191 -0
  128. pretensor/intelligence/steps_embedding.py +168 -0
  129. pretensor/introspection/__init__.py +6 -0
  130. pretensor/introspection/inspector.py +5 -0
  131. pretensor/introspection/models/__init__.py +0 -0
  132. pretensor/introspection/models/base.py +5 -0
  133. pretensor/introspection/models/config.py +237 -0
  134. pretensor/introspection/models/dsn.py +550 -0
  135. pretensor/introspection/models/plan.py +116 -0
  136. pretensor/introspection/models/schema.py +10 -0
  137. pretensor/introspection/models/semantic.py +121 -0
  138. pretensor/introspection/models/validation.py +116 -0
  139. pretensor/introspection/snapshot.py +46 -0
  140. pretensor/mcp/__init__.py +16 -0
  141. pretensor/mcp/config_json.py +24 -0
  142. pretensor/mcp/payload_types.py +274 -0
  143. pretensor/mcp/resources/__init__.py +17 -0
  144. pretensor/mcp/resources/markdown.py +314 -0
  145. pretensor/mcp/server.py +285 -0
  146. pretensor/mcp/service.py +49 -0
  147. pretensor/mcp/service_context.py +142 -0
  148. pretensor/mcp/service_registry.py +294 -0
  149. pretensor/mcp/store_cache.py +43 -0
  150. pretensor/mcp/tool_registry.py +136 -0
  151. pretensor/mcp/tools/__init__.py +1 -0
  152. pretensor/mcp/tools/_rank.py +244 -0
  153. pretensor/mcp/tools/_timed.py +26 -0
  154. pretensor/mcp/tools/compile_metric.py +144 -0
  155. pretensor/mcp/tools/consumers.py +161 -0
  156. pretensor/mcp/tools/context.py +1121 -0
  157. pretensor/mcp/tools/cypher.py +509 -0
  158. pretensor/mcp/tools/detect_changes.py +254 -0
  159. pretensor/mcp/tools/impact.py +271 -0
  160. pretensor/mcp/tools/list.py +131 -0
  161. pretensor/mcp/tools/schema.py +170 -0
  162. pretensor/mcp/tools/search.py +316 -0
  163. pretensor/mcp/tools/semantic_search.py +282 -0
  164. pretensor/mcp/tools/traverse.py +1027 -0
  165. pretensor/mcp/tools/validate_sql.py +150 -0
  166. pretensor/observability.py +203 -0
  167. pretensor/py.typed +0 -0
  168. pretensor/quickstart/README.md +29 -0
  169. pretensor/quickstart/__init__.py +6 -0
  170. pretensor/quickstart/docker-compose.yml +18 -0
  171. pretensor/quickstart/pagila_data.sql +63 -0
  172. pretensor/quickstart/pagila_ddl.sql +92 -0
  173. pretensor/search/__init__.py +6 -0
  174. pretensor/search/base.py +80 -0
  175. pretensor/search/index.py +435 -0
  176. pretensor/semantic/__init__.py +24 -0
  177. pretensor/semantic/base.py +123 -0
  178. pretensor/semantic/compiler.py +487 -0
  179. pretensor/semantic/yaml_layer.py +180 -0
  180. pretensor/skills/__init__.py +5 -0
  181. pretensor/skills/generator.py +235 -0
  182. pretensor/staleness/__init__.py +15 -0
  183. pretensor/staleness/graph_patcher.py +355 -0
  184. pretensor/staleness/impact_analyzer.py +162 -0
  185. pretensor/staleness/snapshot_store.py +38 -0
  186. pretensor/validation/__init__.py +9 -0
  187. pretensor/validation/query_validator.py +436 -0
  188. pretensor/visibility/__init__.py +23 -0
  189. pretensor/visibility/config.py +126 -0
  190. pretensor/visibility/filter.py +143 -0
  191. pretensor/visibility/kuzu_helpers.py +32 -0
  192. pretensor/visibility/runtime.py +36 -0
  193. pretensor/visibility/sync_grants.py +188 -0
  194. pretensor-0.1.0.dist-info/METADATA +251 -0
  195. pretensor-0.1.0.dist-info/RECORD +198 -0
  196. pretensor-0.1.0.dist-info/WHEEL +4 -0
  197. pretensor-0.1.0.dist-info/entry_points.txt +2 -0
  198. pretensor-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,150 @@
1
+ """MCP ``validate_sql`` tool payload — sqlglot + graph-backed SQL validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from pretensor.mcp.tool_registry import McpTool
10
+ from pretensor.validation.query_validator import QueryValidator
11
+
12
+ from ..service_registry import (
13
+ load_registry,
14
+ open_store_for_entry,
15
+ release_store,
16
+ resolve_registry_entry,
17
+ )
18
+
19
+ __all__ = ["create_tool", "validate_sql_payload"]
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ _DEFAULT_DIALECT = "postgres"
24
+
25
+
26
+ def validate_sql_payload(
27
+ graph_dir: Path,
28
+ *,
29
+ sql: str,
30
+ database: str,
31
+ dialect: str = _DEFAULT_DIALECT,
32
+ ) -> dict[str, Any]:
33
+ """Build JSON-serializable payload for ``validate_sql``.
34
+
35
+ Args:
36
+ graph_dir: Root directory of the Pretensor graph store.
37
+ sql: SQL statement to validate.
38
+ database: ``connection_name`` or logical ``database`` key.
39
+ dialect: sqlglot dialect (default ``postgres``).
40
+ """
41
+ if not sql.strip():
42
+ return {"error": "Missing or empty `sql`"}
43
+ if not database.strip():
44
+ return {"error": "Missing or empty `database`"}
45
+
46
+ reg = load_registry(graph_dir)
47
+ entry = resolve_registry_entry(reg, database)
48
+ if entry is None:
49
+ return {"error": f"No registry entry matches database {database!r}"}
50
+
51
+ try:
52
+ store = open_store_for_entry(entry)
53
+ except Exception:
54
+ # Keep the on-disk graph path and engine error server-side; the client
55
+ # sees a stable, connection-named message only.
56
+ logger.exception(
57
+ "validate_sql: failed to open graph for connection %r",
58
+ entry.connection_name,
59
+ )
60
+ return {
61
+ "error": (
62
+ f"Could not open the graph for database {database!r}. "
63
+ "The graph may be missing or corrupt — re-run indexing."
64
+ )
65
+ }
66
+ try:
67
+ validator = QueryValidator(
68
+ store,
69
+ connection_name=entry.connection_name,
70
+ database_key=entry.database,
71
+ dialect=dialect,
72
+ )
73
+ result = validator.validate(sql)
74
+ finally:
75
+ release_store(store)
76
+
77
+ return {
78
+ "valid": result.valid,
79
+ "dialect": dialect,
80
+ "syntax_errors": result.syntax_errors,
81
+ "missing_tables": result.missing_tables,
82
+ "missing_columns": result.missing_columns,
83
+ "invalid_joins": [
84
+ {
85
+ "message": j.message,
86
+ "left_table": j.left_table,
87
+ "right_table": j.right_table,
88
+ }
89
+ for j in result.invalid_joins
90
+ ],
91
+ "suggestions": result.suggestions,
92
+ }
93
+
94
+
95
+ def create_tool(graph_dir: Path) -> McpTool:
96
+ from ._timed import timed_tool
97
+
98
+ async def _handle(args: dict) -> dict:
99
+ sql_s = str(args.get("sql", ""))
100
+ db_t = str(args.get("database", "")).strip()
101
+ dialect_raw = args.get("dialect")
102
+ dialect_s = str(dialect_raw).strip() if dialect_raw is not None else "postgres"
103
+ if not dialect_s:
104
+ dialect_s = "postgres"
105
+ if not sql_s.strip():
106
+ return {"error": "Missing `sql`"}
107
+ if not db_t:
108
+ return {"error": "Missing `database`"}
109
+ with timed_tool("validate_sql", graph_dir, database=db_t, dialect=dialect_s):
110
+ return validate_sql_payload(
111
+ graph_dir, sql=sql_s, database=db_t, dialect=dialect_s
112
+ )
113
+
114
+ return McpTool(
115
+ name="validate_sql",
116
+ description=(
117
+ "Validate a SQL statement against the indexed graph before executing it. "
118
+ "Catches: unknown tables, unknown columns, invalid joins (pairs not in "
119
+ "FK_REFERENCES or INFERRED_JOIN edges), sqlglot parse errors. Returns "
120
+ "fuzzy suggestions for misspelled identifiers. Read-only.\n\n"
121
+ "Examples:\n"
122
+ " sql='SELECT * FROM custmer' -> missing_tables=['custmer'], "
123
+ "suggestions=['customer']\n"
124
+ " sql='SELECT c.foo FROM customer c' -> "
125
+ "missing_columns=[{'table':'customer','column':'foo'}]\n"
126
+ " sql='SELECT * FROM orders o JOIN customer c ON o.x = c.y' -> "
127
+ "invalid_joins=[...] when no FK or inferred edge exists between columns"
128
+ ),
129
+ input_schema={
130
+ "type": "object",
131
+ "properties": {
132
+ "sql": {
133
+ "type": "string",
134
+ "description": "SQL statement to validate",
135
+ },
136
+ "database": {
137
+ "type": "string",
138
+ "description": "Connection name or logical database",
139
+ },
140
+ "dialect": {
141
+ "type": "string",
142
+ "default": "postgres",
143
+ "description": "sqlglot dialect (default 'postgres')",
144
+ },
145
+ },
146
+ "required": ["sql", "database"],
147
+ "additionalProperties": False,
148
+ },
149
+ handler=_handle,
150
+ )
@@ -0,0 +1,203 @@
1
+ """Process-wide logging configuration and timing helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import time
7
+ from contextlib import contextmanager
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import Any, Awaitable, Callable, Iterator, Literal, TypeVar
11
+
12
+ __all__ = [
13
+ "LogFormat",
14
+ "LogLevel",
15
+ "configure_logging",
16
+ "log_timed_operation",
17
+ "run_timed_async",
18
+ ]
19
+
20
+ LogLevel = Literal["debug", "info", "warning", "error"]
21
+ LogFormat = Literal["text", "json"]
22
+
23
+ _LOG_LEVELS: dict[LogLevel, int] = {
24
+ "debug": logging.DEBUG,
25
+ "info": logging.INFO,
26
+ "warning": logging.WARNING,
27
+ "error": logging.ERROR,
28
+ }
29
+ _PRETENSOR_HANDLER_ATTR = "_pretensor_logging_handler"
30
+ _T = TypeVar("_T")
31
+
32
+
33
+ class _JsonFormatter(logging.Formatter):
34
+ """Render log records as one JSON object per line."""
35
+
36
+ def format(self, record: logging.LogRecord) -> str:
37
+ payload: dict[str, Any] = {
38
+ "timestamp": datetime.fromtimestamp(
39
+ record.created, tz=timezone.utc
40
+ ).isoformat(),
41
+ "level": record.levelname.lower(),
42
+ "logger": record.name,
43
+ "event": getattr(record, "event", None),
44
+ "status": getattr(record, "status", None),
45
+ "message": record.getMessage(),
46
+ }
47
+ if hasattr(record, "duration_ms"):
48
+ payload["duration_ms"] = getattr(record, "duration_ms")
49
+ for key, value in record.__dict__.items():
50
+ if key.startswith("_"):
51
+ continue
52
+ if key in payload:
53
+ continue
54
+ if key in ("args", "msg", "exc_info", "exc_text", "stack_info"):
55
+ continue
56
+ if key in (
57
+ "name",
58
+ "levelname",
59
+ "levelno",
60
+ "pathname",
61
+ "filename",
62
+ "module",
63
+ "lineno",
64
+ "funcName",
65
+ "created",
66
+ "msecs",
67
+ "relativeCreated",
68
+ "thread",
69
+ "threadName",
70
+ "processName",
71
+ "process",
72
+ "taskName",
73
+ "asctime",
74
+ ):
75
+ continue
76
+ payload[key] = value
77
+ if record.exc_info:
78
+ payload["exception"] = self.formatException(record.exc_info)
79
+ return _json_dumps(payload)
80
+
81
+
82
+ def _json_dumps(payload: dict[str, Any]) -> str:
83
+ import json
84
+
85
+ return json.dumps(payload, ensure_ascii=False, default=str)
86
+
87
+
88
+ def configure_logging(
89
+ *,
90
+ level: LogLevel = "info",
91
+ log_format: LogFormat = "text",
92
+ log_file: Path | None = None,
93
+ ) -> None:
94
+ """Configure root logging handlers for CLI and MCP code paths."""
95
+ root = logging.getLogger()
96
+ target_level = _LOG_LEVELS[level]
97
+ root.setLevel(target_level)
98
+ logging.getLogger("pretensor").setLevel(logging.NOTSET)
99
+ _remove_pretensor_handlers(root)
100
+
101
+ formatter: logging.Formatter
102
+ if log_format == "json":
103
+ formatter = _JsonFormatter()
104
+ else:
105
+ formatter = logging.Formatter(
106
+ "%(asctime)s %(levelname)s %(name)s %(message)s",
107
+ datefmt="%Y-%m-%dT%H:%M:%S%z",
108
+ )
109
+
110
+ stream_handler = logging.StreamHandler()
111
+ stream_handler.setFormatter(formatter)
112
+ stream_handler.addFilter(_build_handler_filter(target_level))
113
+ setattr(stream_handler, _PRETENSOR_HANDLER_ATTR, True)
114
+ root.addHandler(stream_handler)
115
+
116
+ if log_file is not None:
117
+ log_file.parent.mkdir(parents=True, exist_ok=True)
118
+ file_handler = logging.FileHandler(log_file, encoding="utf-8")
119
+ file_handler.setFormatter(formatter)
120
+ file_handler.addFilter(_build_handler_filter(target_level))
121
+ setattr(file_handler, _PRETENSOR_HANDLER_ATTR, True)
122
+ root.addHandler(file_handler)
123
+
124
+
125
+ def _remove_pretensor_handlers(root: logging.Logger) -> None:
126
+ for handler in list(root.handlers):
127
+ if not getattr(handler, _PRETENSOR_HANDLER_ATTR, False):
128
+ continue
129
+ root.removeHandler(handler)
130
+ handler.close()
131
+
132
+
133
+ def _build_handler_filter(min_level: int) -> Callable[[logging.LogRecord], bool]:
134
+ def _handler_filter(record: logging.LogRecord) -> bool:
135
+ if record.name.startswith("pretensor"):
136
+ if record.levelno < min_level:
137
+ return False
138
+ if (
139
+ min_level == logging.INFO
140
+ and record.levelno == logging.INFO
141
+ and not hasattr(record, "event")
142
+ ):
143
+ return False
144
+ return True
145
+ return record.levelno >= logging.WARNING
146
+
147
+ return _handler_filter
148
+
149
+
150
+ @contextmanager
151
+ def log_timed_operation(
152
+ logger: logging.Logger,
153
+ *,
154
+ event: str,
155
+ level: int = logging.INFO,
156
+ **fields: Any,
157
+ ) -> Iterator[None]:
158
+ """Log operation latency and status with structured fields."""
159
+ started = time.perf_counter()
160
+ try:
161
+ yield
162
+ except Exception:
163
+ duration_ms = (time.perf_counter() - started) * 1000
164
+ logger.log(
165
+ level,
166
+ "%s failed in %.2fms",
167
+ event,
168
+ duration_ms,
169
+ extra={
170
+ "event": event,
171
+ "status": "error",
172
+ "duration_ms": duration_ms,
173
+ **fields,
174
+ },
175
+ exc_info=True,
176
+ )
177
+ raise
178
+ duration_ms = (time.perf_counter() - started) * 1000
179
+ logger.log(
180
+ level,
181
+ "%s completed in %.2fms",
182
+ event,
183
+ duration_ms,
184
+ extra={
185
+ "event": event,
186
+ "status": "ok",
187
+ "duration_ms": duration_ms,
188
+ **fields,
189
+ },
190
+ )
191
+
192
+
193
+ async def run_timed_async(
194
+ logger: logging.Logger,
195
+ *,
196
+ event: str,
197
+ callback: Callable[[], Awaitable[_T]],
198
+ level: int = logging.INFO,
199
+ **fields: Any,
200
+ ) -> _T:
201
+ """Await ``callback`` and emit a structured timing log."""
202
+ with log_timed_operation(logger, event=event, level=level, **fields):
203
+ return await callback()
pretensor/py.typed ADDED
File without changes
@@ -0,0 +1,29 @@
1
+ # Pretensor quickstart database
2
+
3
+ A throwaway Postgres instance pre-loaded with the Pagila sample schema (DVD
4
+ rental store), sized for `pretensor quickstart`. Listens on **localhost:55432**.
5
+
6
+ ## Use it
7
+
8
+ The recommended path is `pretensor quickstart`, which starts the container,
9
+ indexes the DB into `./.pretensor/`, and prints an MCP config snippet.
10
+
11
+ To run the container directly from a clone of the repo:
12
+
13
+ ```bash
14
+ docker compose -f src/pretensor/quickstart/docker-compose.yml up -d
15
+ # DSN: postgresql://postgres:postgres@localhost:55432/pagila
16
+ docker compose -f src/pretensor/quickstart/docker-compose.yml down -v
17
+ ```
18
+
19
+ ## Contents
20
+
21
+ - 8 tables across `public` and `staff` schemas
22
+ - ~35 seed rows total — enough for `query`, `context`, and `traverse` to
23
+ return non-empty results
24
+ - FK chain: `payment → rental → inventory → film` plus `rental → customer`
25
+
26
+ The schema and seed live next to this README as `pagila_ddl.sql` and
27
+ `pagila_data.sql` — a curated subset of upstream Pagila. They ship inside
28
+ the wheel so `pretensor quickstart` works after a plain `pip install`
29
+ without needing a clone of the repo.
@@ -0,0 +1,6 @@
1
+ """Quickstart assets shipped with the wheel.
2
+
3
+ Holds the docker-compose file and pagila SQL fixtures consumed by
4
+ ``pretensor quickstart``. Resolved at runtime via ``importlib.resources`` so
5
+ they work the same in editable installs and in pip-installed wheels.
6
+ """
@@ -0,0 +1,18 @@
1
+ services:
2
+ pretensor-quickstart-db:
3
+ image: postgres:16-alpine
4
+ container_name: pretensor-quickstart-db
5
+ environment:
6
+ POSTGRES_USER: postgres
7
+ POSTGRES_PASSWORD: postgres
8
+ POSTGRES_DB: pagila
9
+ ports:
10
+ - "55432:5432"
11
+ volumes:
12
+ - ./pagila_ddl.sql:/docker-entrypoint-initdb.d/01_pagila_ddl.sql:ro
13
+ - ./pagila_data.sql:/docker-entrypoint-initdb.d/02_pagila_data.sql:ro
14
+ healthcheck:
15
+ test: ["CMD-SHELL", "pg_isready -U postgres -d pagila"]
16
+ interval: 2s
17
+ timeout: 3s
18
+ retries: 30
@@ -0,0 +1,63 @@
1
+ -- Minimal Pagila seed for the hello-world quickstart.
2
+ -- Curated subset: enough rows for `query` / `context` / `traverse` to return
3
+ -- non-empty results, while staying small enough to commit to the repo.
4
+
5
+ INSERT INTO public.film (title, description, release_year, rental_rate, length, rating) VALUES
6
+ ('Academy Dinosaur', 'A Boring Saga of a Dinosaur', 2006, 0.99, 86, 'PG'),
7
+ ('Ace Goldfinger', 'An Astounding Epistle in Hong Kong', 2006, 4.99, 48, 'G'),
8
+ ('Adaptation Holes', 'A Astounding Reflection of a Lumberjack', 2006, 2.99, 50, 'NC-17'),
9
+ ('Affair Prejudice', 'A Fanciful Documentary of a Frisbee', 2006, 2.99, 117, 'G'),
10
+ ('African Egg', 'A Fast-Paced Documentary of a Pastry Chef', 2006, 2.99, 130, 'G');
11
+
12
+ INSERT INTO public.actor (first_name, last_name, birth_date) VALUES
13
+ ('Penelope', 'Guiness', '1970-04-12'),
14
+ ('Nick', 'Wahlberg', '1971-01-30'),
15
+ ('Ed', 'Chase', '1955-07-08'),
16
+ ('Jennifer', 'Davis', '1980-09-15'),
17
+ ('Johnny', 'Lollobrigida', '1948-11-22');
18
+
19
+ INSERT INTO public.film_actor (actor_id, film_id, role) VALUES
20
+ (1, 1, 'lead'),
21
+ (2, 1, 'support'),
22
+ (3, 2, 'lead'),
23
+ (4, 3, 'lead'),
24
+ (5, 4, 'support');
25
+
26
+ INSERT INTO public.inventory (film_id, store_id, condition, acquired_at) VALUES
27
+ (1, 1, 'good', '2024-01-15'),
28
+ (1, 1, 'good', '2024-01-15'),
29
+ (2, 1, 'fair', '2024-02-01'),
30
+ (3, 2, 'good', '2024-02-10'),
31
+ (4, 1, 'good', '2024-03-05');
32
+
33
+ INSERT INTO public.customer (first_name, last_name, email) VALUES
34
+ ('Mary', 'Smith', 'mary.smith@example.com'),
35
+ ('Patricia','Johnson', 'patricia.j@example.com'),
36
+ ('Linda', 'Williams','linda.w@example.com');
37
+
38
+ INSERT INTO public.rental (inventory_id, customer_id, rental_date, return_date) VALUES
39
+ (1, 1, '2024-05-24 22:53:30+00', '2024-05-26 22:04:30+00'),
40
+ (2, 2, '2024-05-24 22:54:33+00', '2024-05-28 19:40:33+00'),
41
+ (3, 1, '2024-05-25 11:30:37+00', '2024-06-01 14:25:37+00'),
42
+ (4, 3, '2024-05-26 09:15:00+00', '2024-05-30 12:00:00+00'),
43
+ (5, 2, '2024-05-27 13:42:11+00', NULL);
44
+
45
+ INSERT INTO public.payment (rental_id, customer_id, amount, method) VALUES
46
+ (1, 1, 2.99, 'cash'),
47
+ (2, 2, 4.99, 'card'),
48
+ (3, 1, 0.99, 'card'),
49
+ (4, 3, 2.99, 'cash'),
50
+ (5, 2, 4.99, 'card');
51
+
52
+ INSERT INTO staff.staff (first_name, last_name, email, store_id, username) VALUES
53
+ ('Mike', 'Hillyer', 'mike@example.com', 1, 'mike'),
54
+ ('Jon', 'Stephens', 'jon@example.com', 2, 'jon');
55
+
56
+ -- Reset sequences so subsequent inserts (if any) start past seeded ids.
57
+ SELECT setval(pg_get_serial_sequence('public.film', 'film_id'), (SELECT MAX(film_id) FROM public.film));
58
+ SELECT setval(pg_get_serial_sequence('public.actor', 'actor_id'), (SELECT MAX(actor_id) FROM public.actor));
59
+ SELECT setval(pg_get_serial_sequence('public.inventory', 'inventory_id'), (SELECT MAX(inventory_id) FROM public.inventory));
60
+ SELECT setval(pg_get_serial_sequence('public.customer', 'customer_id'), (SELECT MAX(customer_id) FROM public.customer));
61
+ SELECT setval(pg_get_serial_sequence('public.rental', 'rental_id'), (SELECT MAX(rental_id) FROM public.rental));
62
+ SELECT setval(pg_get_serial_sequence('public.payment', 'payment_id'), (SELECT MAX(payment_id) FROM public.payment));
63
+ SELECT setval(pg_get_serial_sequence('staff.staff', 'staff_id'), (SELECT MAX(staff_id) FROM staff.staff));
@@ -0,0 +1,92 @@
1
+ -- Minimal Pagila-derived schema for E2E testing
2
+ -- 2 schemas: public, staff
3
+ -- 7 tables in public: film, actor, film_actor, inventory, customer, rental, payment
4
+ -- 1 table in staff: staff
5
+ -- FK chains: inventory→film, rental→inventory, rental→customer, payment→rental
6
+
7
+ CREATE SCHEMA IF NOT EXISTS staff;
8
+
9
+ -- ============================================================
10
+ -- public schema
11
+ -- ============================================================
12
+
13
+ CREATE TABLE public.film (
14
+ film_id SERIAL PRIMARY KEY,
15
+ title VARCHAR(255) NOT NULL,
16
+ description TEXT,
17
+ release_year SMALLINT,
18
+ rental_rate NUMERIC(4,2) NOT NULL DEFAULT 4.99,
19
+ length SMALLINT,
20
+ rating VARCHAR(10) DEFAULT 'G',
21
+ last_update TIMESTAMPTZ NOT NULL DEFAULT NOW()
22
+ );
23
+
24
+ CREATE TABLE public.actor (
25
+ actor_id SERIAL PRIMARY KEY,
26
+ first_name VARCHAR(45) NOT NULL,
27
+ last_name VARCHAR(45) NOT NULL,
28
+ birth_date DATE,
29
+ active BOOLEAN NOT NULL DEFAULT TRUE,
30
+ last_update TIMESTAMPTZ NOT NULL DEFAULT NOW()
31
+ );
32
+
33
+ CREATE TABLE public.film_actor (
34
+ actor_id INT NOT NULL REFERENCES public.actor(actor_id),
35
+ film_id INT NOT NULL REFERENCES public.film(film_id),
36
+ role VARCHAR(50),
37
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
38
+ PRIMARY KEY (actor_id, film_id)
39
+ );
40
+
41
+ CREATE TABLE public.inventory (
42
+ inventory_id SERIAL PRIMARY KEY,
43
+ film_id INT NOT NULL REFERENCES public.film(film_id),
44
+ store_id SMALLINT NOT NULL DEFAULT 1,
45
+ condition VARCHAR(20) NOT NULL DEFAULT 'good',
46
+ acquired_at DATE,
47
+ last_update TIMESTAMPTZ NOT NULL DEFAULT NOW()
48
+ );
49
+
50
+ CREATE TABLE public.customer (
51
+ customer_id SERIAL PRIMARY KEY,
52
+ first_name VARCHAR(45) NOT NULL,
53
+ last_name VARCHAR(45) NOT NULL,
54
+ email VARCHAR(100),
55
+ active BOOLEAN NOT NULL DEFAULT TRUE,
56
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
57
+ last_update TIMESTAMPTZ NOT NULL DEFAULT NOW()
58
+ );
59
+
60
+ CREATE TABLE public.rental (
61
+ rental_id SERIAL PRIMARY KEY,
62
+ inventory_id INT NOT NULL REFERENCES public.inventory(inventory_id),
63
+ customer_id INT NOT NULL REFERENCES public.customer(customer_id),
64
+ rental_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
65
+ return_date TIMESTAMPTZ,
66
+ staff_id SMALLINT NOT NULL DEFAULT 1,
67
+ last_update TIMESTAMPTZ NOT NULL DEFAULT NOW()
68
+ );
69
+
70
+ CREATE TABLE public.payment (
71
+ payment_id SERIAL PRIMARY KEY,
72
+ rental_id INT NOT NULL REFERENCES public.rental(rental_id),
73
+ customer_id INT NOT NULL REFERENCES public.customer(customer_id),
74
+ amount NUMERIC(5,2) NOT NULL,
75
+ payment_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
76
+ method VARCHAR(20) NOT NULL DEFAULT 'cash'
77
+ );
78
+
79
+ -- ============================================================
80
+ -- staff schema (multi-schema coverage)
81
+ -- ============================================================
82
+
83
+ CREATE TABLE staff.staff (
84
+ staff_id SERIAL PRIMARY KEY,
85
+ first_name VARCHAR(45) NOT NULL,
86
+ last_name VARCHAR(45) NOT NULL,
87
+ email VARCHAR(100),
88
+ store_id SMALLINT NOT NULL DEFAULT 1,
89
+ active BOOLEAN NOT NULL DEFAULT TRUE,
90
+ username VARCHAR(50) NOT NULL,
91
+ last_update TIMESTAMPTZ NOT NULL DEFAULT NOW()
92
+ );
@@ -0,0 +1,6 @@
1
+ """Keyword search over graph metadata (FTS5)."""
2
+
3
+ from pretensor.search.base import BaseSearchIndex, SearchResult
4
+ from pretensor.search.index import KeywordSearchIndex, SearchIndex
5
+
6
+ __all__ = ["BaseSearchIndex", "KeywordSearchIndex", "SearchIndex", "SearchResult"]
@@ -0,0 +1,80 @@
1
+ """Abstract base for search index implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass
7
+
8
+ from pretensor.core.registry import GraphRegistry
9
+
10
+ __all__ = ["BaseSearchIndex", "SearchResult"]
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class SearchResult:
15
+ """One ranked hit from a search index."""
16
+
17
+ node_type: str
18
+ name: str
19
+ database_name: str
20
+ connection_name: str
21
+ description: str
22
+ score: float
23
+
24
+
25
+ class BaseSearchIndex(ABC):
26
+ """Abstract search index over graph metadata.
27
+
28
+ Concrete implementations may use keyword (BM25), vector (embeddings),
29
+ or hybrid strategies. OSS ships ``KeywordSearchIndex``; plugins can register
30
+ a ``HybridSearchIndex`` subclass.
31
+ """
32
+
33
+ @abstractmethod
34
+ def search(
35
+ self,
36
+ q: str,
37
+ *,
38
+ db: str | None = None,
39
+ limit: int = 10,
40
+ ) -> list[SearchResult]:
41
+ """Search indexed metadata.
42
+
43
+ Args:
44
+ q: Free-text query string.
45
+ db: Optional connection or logical database name filter.
46
+ limit: Maximum number of results to return.
47
+
48
+ Returns:
49
+ Ranked list of search results.
50
+ """
51
+
52
+ @abstractmethod
53
+ def similar(
54
+ self,
55
+ name: str,
56
+ *,
57
+ db: str | None = None,
58
+ limit: int = 10,
59
+ ) -> list[SearchResult]:
60
+ """Return nodes related to the named node.
61
+
62
+ OSS implementations use graph proximity (FK / join edges). Other
63
+ implementations may additionally use vector similarity.
64
+
65
+ Args:
66
+ name: Fully-qualified node name, e.g. ``schema.table``.
67
+ db: Optional connection or logical database name filter.
68
+ limit: Maximum number of results to return.
69
+
70
+ Returns:
71
+ Related nodes ranked by proximity or similarity.
72
+ """
73
+
74
+ @abstractmethod
75
+ def index_graph(self, registry: GraphRegistry) -> None:
76
+ """Build or rebuild the search index from all graphs in *registry*.
77
+
78
+ Args:
79
+ registry: Loaded registry describing available graph files.
80
+ """