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,473 @@
1
+ """Kuzu DDL for the Phase 1 schema graph.
2
+
3
+ All ``CREATE`` statements for node and relationship tables live here and are
4
+ applied only through :class:`pretensor.core.store.KuzuStore`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ __all__ = [
10
+ "CATALOG_NODE_LABELS",
11
+ "CATALOG_EDGE_TYPES",
12
+ "format_catalog_summary",
13
+ "DDL_CREATE_TABLE_NODE",
14
+ "DDL_CREATE_COLUMN_NODE",
15
+ "DDL_CREATE_HAS_COLUMN_REL",
16
+ "DDL_CREATE_HAS_SUBCOLUMN_REL",
17
+ "DDL_CREATE_ENTITY_NODE",
18
+ "DDL_CREATE_REPRESENTS_REL",
19
+ "DDL_CREATE_FK_REL",
20
+ "DDL_CREATE_INFERRED_JOIN_REL",
21
+ "DDL_CREATE_LINEAGE_REL",
22
+ "DDL_CREATE_SAME_ENTITY_REL",
23
+ "DDL_CREATE_CLUSTER_NODE",
24
+ "DDL_CREATE_IN_CLUSTER_REL",
25
+ "DDL_CREATE_JOIN_PATH_NODE",
26
+ "DDL_CREATE_METRIC_TEMPLATE_NODE",
27
+ "DDL_CREATE_METRIC_DEPENDS_REL",
28
+ # Semantic-layer extension point: reserved tables, empty until populated.
29
+ "DDL_CREATE_SEMANTIC_METRIC_NODE",
30
+ "DDL_CREATE_SEMANTIC_DIMENSION_NODE",
31
+ "DDL_CREATE_SEMANTIC_BUSINESS_RULE_NODE",
32
+ "DDL_CREATE_SEMANTIC_METRIC_DEPENDS_REL",
33
+ "DDL_CREATE_SEMANTIC_DIMENSION_LEVEL_REL",
34
+ "DDL_CREATE_SEMANTIC_RULE_APPLIES_TO_REL",
35
+ # Analyze enrichment: external code consumers of physical tables.
36
+ "DDL_CREATE_EXTERNAL_CONSUMER_NODE",
37
+ "DDL_CREATE_CONSUMES_REL",
38
+ ]
39
+
40
+ # Table node — one row per physical table in an indexed database.
41
+ DDL_CREATE_TABLE_NODE = """
42
+ CREATE NODE TABLE IF NOT EXISTS SchemaTable(
43
+ node_id STRING,
44
+ connection_name STRING,
45
+ database STRING,
46
+ schema_name STRING,
47
+ table_name STRING,
48
+ row_count INT64,
49
+ row_count_source STRING,
50
+ comment STRING,
51
+ description STRING,
52
+ tags STRING[],
53
+ entity_type STRING,
54
+ table_type STRING,
55
+ seq_scan_count INT64,
56
+ idx_scan_count INT64,
57
+ insert_count INT64,
58
+ update_count INT64,
59
+ delete_count INT64,
60
+ is_partitioned BOOL,
61
+ partition_key STRING,
62
+ grants_json STRING,
63
+ access_read_count INT64,
64
+ access_write_count INT64,
65
+ days_since_last_access INT64,
66
+ potentially_unused BOOL,
67
+ table_bytes INT64,
68
+ clustering_key STRING,
69
+ role STRING,
70
+ role_confidence DOUBLE,
71
+ classification_signals STRING,
72
+ has_external_consumers BOOL,
73
+ test_count INT64,
74
+ staleness_status STRING,
75
+ staleness_as_of STRING,
76
+ embedding FLOAT[384],
77
+ PRIMARY KEY (node_id)
78
+ )
79
+ """
80
+
81
+ # Physical column (for staleness, impact, and FK join tracing).
82
+ DDL_CREATE_COLUMN_NODE = """
83
+ CREATE NODE TABLE IF NOT EXISTS SchemaColumn(
84
+ node_id STRING,
85
+ connection_name STRING,
86
+ database STRING,
87
+ schema_name STRING,
88
+ table_name STRING,
89
+ column_name STRING,
90
+ description STRING,
91
+ data_type STRING,
92
+ nullable BOOL,
93
+ is_primary_key BOOL,
94
+ is_foreign_key BOOL,
95
+ comment STRING,
96
+ default_value STRING,
97
+ is_indexed BOOL,
98
+ check_constraints_json STRING,
99
+ ordinal_position INT64,
100
+ most_common_values_json STRING,
101
+ histogram_bounds_json STRING,
102
+ stats_correlation DOUBLE,
103
+ column_cardinality INT64,
104
+ index_type STRING,
105
+ index_is_unique BOOL,
106
+ parent_column_id STRING,
107
+ is_array BOOL,
108
+ PRIMARY KEY (node_id)
109
+ )
110
+ """
111
+
112
+ DDL_CREATE_HAS_COLUMN_REL = """
113
+ CREATE REL TABLE IF NOT EXISTS HAS_COLUMN(
114
+ FROM SchemaTable TO SchemaColumn
115
+ )
116
+ """
117
+
118
+ # Nested column (e.g. BigQuery STRUCT sub-field) linked from parent SchemaColumn.
119
+ DDL_CREATE_HAS_SUBCOLUMN_REL = """
120
+ CREATE REL TABLE IF NOT EXISTS HAS_SUBCOLUMN(
121
+ FROM SchemaColumn TO SchemaColumn
122
+ )
123
+ """
124
+
125
+ # Business entity node — groups one or more physical tables (LLM + classifier).
126
+ DDL_CREATE_ENTITY_NODE = """
127
+ CREATE NODE TABLE IF NOT EXISTS Entity(
128
+ node_id STRING,
129
+ connection_name STRING,
130
+ database STRING,
131
+ name STRING,
132
+ description STRING,
133
+ PRIMARY KEY (node_id)
134
+ )
135
+ """
136
+
137
+ # Entity → physical table projection.
138
+ DDL_CREATE_REPRESENTS_REL = """
139
+ CREATE REL TABLE IF NOT EXISTS REPRESENTS(
140
+ FROM Entity TO SchemaTable
141
+ )
142
+ """
143
+
144
+ # Directed foreign-key reference between two SchemaTable nodes.
145
+ DDL_CREATE_FK_REL = """
146
+ CREATE REL TABLE IF NOT EXISTS FK_REFERENCES(
147
+ FROM SchemaTable TO SchemaTable,
148
+ edge_id STRING,
149
+ source_column STRING,
150
+ target_column STRING,
151
+ constraint_name STRING
152
+ )
153
+ """
154
+
155
+ # Heuristic / LLM / statistically scored implicit join (non-declared FK).
156
+ DDL_CREATE_INFERRED_JOIN_REL = """
157
+ CREATE REL TABLE IF NOT EXISTS INFERRED_JOIN(
158
+ FROM SchemaTable TO SchemaTable,
159
+ edge_id STRING,
160
+ source_column STRING,
161
+ target_column STRING,
162
+ source STRING,
163
+ confidence DOUBLE,
164
+ reasoning STRING
165
+ )
166
+ """
167
+
168
+ # Table-level data lineage (views, triggers, Snowflake tasks/streams) — structural metadata only.
169
+ DDL_CREATE_LINEAGE_REL = """
170
+ CREATE REL TABLE IF NOT EXISTS LINEAGE(
171
+ FROM SchemaTable TO SchemaTable,
172
+ edge_id STRING,
173
+ source STRING,
174
+ lineage_type STRING,
175
+ confidence DOUBLE
176
+ )
177
+ """
178
+
179
+ # Cross-database / cross-connection business-entity equivalence (human-confirmed or suggested).
180
+ DDL_CREATE_SAME_ENTITY_REL = """
181
+ CREATE REL TABLE IF NOT EXISTS SAME_ENTITY(
182
+ FROM Entity TO Entity,
183
+ edge_id STRING,
184
+ status STRING,
185
+ score DOUBLE,
186
+ join_columns STRING,
187
+ reasoning STRING,
188
+ created_at STRING,
189
+ confirmed_at STRING,
190
+ confirmed_by STRING
191
+ )
192
+ """
193
+
194
+ # Domain cluster (Leiden + optional LLM label).
195
+ DDL_CREATE_CLUSTER_NODE = """
196
+ CREATE NODE TABLE IF NOT EXISTS Cluster(
197
+ node_id STRING,
198
+ database_key STRING,
199
+ label STRING,
200
+ description STRING,
201
+ cohesion_score DOUBLE,
202
+ table_count INT64,
203
+ stale BOOL,
204
+ schema_pattern STRING,
205
+ PRIMARY KEY (node_id)
206
+ )
207
+ """
208
+
209
+ DDL_CREATE_IN_CLUSTER_REL = """
210
+ CREATE REL TABLE IF NOT EXISTS IN_CLUSTER(
211
+ FROM SchemaTable TO Cluster
212
+ )
213
+ """
214
+
215
+ # Precomputed join path (steps as JSON until column nodes exist in the graph).
216
+ DDL_CREATE_JOIN_PATH_NODE = """
217
+ CREATE NODE TABLE IF NOT EXISTS JoinPath(
218
+ node_id STRING,
219
+ database_key STRING,
220
+ from_table_id STRING,
221
+ to_table_id STRING,
222
+ depth INT64,
223
+ confidence DOUBLE,
224
+ ambiguous BOOL,
225
+ steps_json STRING,
226
+ semantic_label STRING,
227
+ stale BOOL,
228
+ PRIMARY KEY (node_id)
229
+ )
230
+ """
231
+
232
+ # Pre-validated SQL metric pattern (built during indexing after classification).
233
+ DDL_CREATE_METRIC_TEMPLATE_NODE = """
234
+ CREATE NODE TABLE IF NOT EXISTS MetricTemplate(
235
+ node_id STRING,
236
+ connection_name STRING,
237
+ database STRING,
238
+ dialect STRING,
239
+ name STRING,
240
+ display_name STRING,
241
+ description STRING,
242
+ sql_template STRING,
243
+ tables_used_json STRING,
244
+ validated BOOL,
245
+ validation_errors_json STRING,
246
+ generated_at_iso STRING,
247
+ stale BOOL,
248
+ PRIMARY KEY (node_id)
249
+ )
250
+ """
251
+
252
+ DDL_CREATE_METRIC_DEPENDS_REL = """
253
+ CREATE REL TABLE IF NOT EXISTS METRIC_DEPENDS(
254
+ FROM MetricTemplate TO SchemaTable
255
+ )
256
+ """
257
+
258
+ # ── Semantic-layer extension point: reserved node and edge tables ─────────────
259
+ #
260
+ # These tables are created by ``KuzuStore.ensure_schema()`` so downstream
261
+ # semantic-layer implementations can populate them without schema migrations.
262
+ # Empty by default.
263
+
264
+ DDL_CREATE_SEMANTIC_METRIC_NODE = """
265
+ CREATE NODE TABLE IF NOT EXISTS Metric(
266
+ node_id STRING,
267
+ connection_name STRING,
268
+ name STRING,
269
+ display_name STRING,
270
+ description STRING,
271
+ sql_template STRING,
272
+ PRIMARY KEY (node_id)
273
+ )
274
+ """
275
+
276
+ DDL_CREATE_SEMANTIC_DIMENSION_NODE = """
277
+ CREATE NODE TABLE IF NOT EXISTS Dimension(
278
+ node_id STRING,
279
+ connection_name STRING,
280
+ name STRING,
281
+ display_name STRING,
282
+ description STRING,
283
+ dimension_type STRING,
284
+ PRIMARY KEY (node_id)
285
+ )
286
+ """
287
+
288
+ DDL_CREATE_SEMANTIC_BUSINESS_RULE_NODE = """
289
+ CREATE NODE TABLE IF NOT EXISTS BusinessRule(
290
+ node_id STRING,
291
+ connection_name STRING,
292
+ name STRING,
293
+ description STRING,
294
+ rule_type STRING,
295
+ expression STRING,
296
+ PRIMARY KEY (node_id)
297
+ )
298
+ """
299
+
300
+ DDL_CREATE_SEMANTIC_METRIC_DEPENDS_REL = """
301
+ CREATE REL TABLE IF NOT EXISTS METRIC_DEPENDS_ON(
302
+ FROM Metric TO SchemaTable
303
+ )
304
+ """
305
+
306
+ DDL_CREATE_SEMANTIC_DIMENSION_LEVEL_REL = """
307
+ CREATE REL TABLE IF NOT EXISTS DIMENSION_LEVEL(
308
+ FROM Dimension TO SchemaColumn
309
+ )
310
+ """
311
+
312
+ DDL_CREATE_SEMANTIC_RULE_APPLIES_TO_REL = """
313
+ CREATE REL TABLE IF NOT EXISTS RULE_APPLIES_TO(
314
+ FROM BusinessRule TO SchemaTable
315
+ )
316
+ """
317
+
318
+ # ── Analyze enrichment: external code consumers ───────────────────────────────
319
+ #
320
+ # ExternalConsumer is a code location (service + file + normalized SQL fingerprint)
321
+ # that issues a SQL statement against a physical table. CONSUMES links it to the
322
+ # consumed SchemaTable, one edge per (consumer, table, op). Raw SQL text is never
323
+ # stored — only the sha256[:16] fingerprint and the resolved table references.
324
+ DDL_CREATE_EXTERNAL_CONSUMER_NODE = """
325
+ CREATE NODE TABLE IF NOT EXISTS ExternalConsumer(
326
+ node_id STRING,
327
+ connection_name STRING,
328
+ service_name STRING,
329
+ file_path STRING,
330
+ language STRING,
331
+ symbol STRING,
332
+ kind STRING,
333
+ line_start INT64,
334
+ line_end INT64,
335
+ sql_fingerprint STRING,
336
+ confidence DOUBLE,
337
+ dialect_used STRING,
338
+ scan_run_id STRING,
339
+ PRIMARY KEY (node_id)
340
+ )
341
+ """
342
+
343
+ DDL_CREATE_CONSUMES_REL = """
344
+ CREATE REL TABLE IF NOT EXISTS CONSUMES(
345
+ FROM ExternalConsumer TO SchemaTable,
346
+ edge_id STRING,
347
+ op STRING,
348
+ source STRING,
349
+ confidence DOUBLE,
350
+ scan_run_id STRING
351
+ )
352
+ """
353
+
354
+ # ── Catalog summary (for cypher tool description and schema tool) ──────────────
355
+ #
356
+ # Single source of truth for the node labels and edge types exposed to MCP
357
+ # clients. Kept compact so it can be inlined into the cypher tool description
358
+ # without bloating the wire payload.
359
+
360
+ CATALOG_NODE_LABELS: tuple[tuple[str, str], ...] = (
361
+ (
362
+ "SchemaTable",
363
+ "physical table; key fields: schema_name, table_name, row_count, table_type, role",
364
+ ),
365
+ (
366
+ "SchemaColumn",
367
+ "physical column; key fields: column_name, data_type, nullable, is_primary_key, is_foreign_key",
368
+ ),
369
+ ("Entity", "business entity grouping one or more tables (name, description)"),
370
+ (
371
+ "Cluster",
372
+ "Leiden community of related tables (label, table_count, cohesion_score)",
373
+ ),
374
+ (
375
+ "JoinPath",
376
+ "precomputed join path between two tables (depth, confidence, steps_json)",
377
+ ),
378
+ (
379
+ "MetricTemplate",
380
+ "validated SQL metric pattern (name, sql_template, validated, tables_used_json)",
381
+ ),
382
+ ("Metric", "semantic metric (extension point; empty by default)"),
383
+ ("Dimension", "semantic dimension (extension point; empty by default)"),
384
+ ("BusinessRule", "semantic business rule (extension point; empty by default)"),
385
+ (
386
+ "ExternalConsumer",
387
+ "code location consuming a table via SQL (service_name, file_path, kind, sql_fingerprint, confidence, scan_run_id)",
388
+ ),
389
+ )
390
+
391
+ CATALOG_EDGE_TYPES: tuple[tuple[str, str, str, str], ...] = (
392
+ ("HAS_COLUMN", "SchemaTable", "SchemaColumn", "table → its columns"),
393
+ (
394
+ "HAS_SUBCOLUMN",
395
+ "SchemaColumn",
396
+ "SchemaColumn",
397
+ "nested column (e.g. BigQuery STRUCT field)",
398
+ ),
399
+ ("REPRESENTS", "Entity", "SchemaTable", "entity → underlying physical tables"),
400
+ (
401
+ "FK_REFERENCES",
402
+ "SchemaTable",
403
+ "SchemaTable",
404
+ "declared foreign key (source_column, target_column)",
405
+ ),
406
+ (
407
+ "INFERRED_JOIN",
408
+ "SchemaTable",
409
+ "SchemaTable",
410
+ "implicit join (source, confidence, source_column, target_column)",
411
+ ),
412
+ (
413
+ "LINEAGE",
414
+ "SchemaTable",
415
+ "SchemaTable",
416
+ "view/trigger/task lineage (lineage_type, confidence)",
417
+ ),
418
+ (
419
+ "SAME_ENTITY",
420
+ "Entity",
421
+ "Entity",
422
+ "cross-database entity equivalence (extension point; empty by default)",
423
+ ),
424
+ ("IN_CLUSTER", "SchemaTable", "Cluster", "table → cluster membership"),
425
+ (
426
+ "METRIC_DEPENDS",
427
+ "MetricTemplate",
428
+ "SchemaTable",
429
+ "metric template → tables it reads",
430
+ ),
431
+ ("METRIC_DEPENDS_ON", "Metric", "SchemaTable", "metric → tables (extension point)"),
432
+ (
433
+ "DIMENSION_LEVEL",
434
+ "Dimension",
435
+ "SchemaColumn",
436
+ "dimension → columns (extension point)",
437
+ ),
438
+ (
439
+ "RULE_APPLIES_TO",
440
+ "BusinessRule",
441
+ "SchemaTable",
442
+ "rule → tables (extension point)",
443
+ ),
444
+ (
445
+ "CONSUMES",
446
+ "ExternalConsumer",
447
+ "SchemaTable",
448
+ "code location → table it queries (op read/write, source, confidence, scan_run_id)",
449
+ ),
450
+ )
451
+
452
+
453
+ def format_catalog_summary() -> str:
454
+ """Return a compact, human-readable summary of the graph's node/edge catalog.
455
+
456
+ Used to inline the schema into the ``cypher`` tool description so MCP
457
+ clients can write valid queries without a separate round-trip.
458
+ """
459
+ node_lines = [f" ({label}) — {desc}" for label, desc in CATALOG_NODE_LABELS]
460
+ edge_lines = [
461
+ f" -[{name}]-> {src} → {dst} — {desc}"
462
+ for name, src, dst, desc in CATALOG_EDGE_TYPES
463
+ ]
464
+ return (
465
+ "Node labels:\n"
466
+ + "\n".join(node_lines)
467
+ + "\n\nEdge types:\n"
468
+ + "\n".join(edge_lines)
469
+ + "\n\nTip: when ranking by SchemaTable.row_count, exclude views and "
470
+ "unknowns — they may be NULL or -1 (timed-out view counts):\n"
471
+ " MATCH (t:SchemaTable) WHERE t.table_type = 'table' AND t.row_count > 0 "
472
+ "RETURN t ORDER BY t.row_count DESC LIMIT 25"
473
+ )
@@ -0,0 +1,93 @@
1
+ """Helpers for writing secrets and credential stores with tight permissions.
2
+
3
+ These centralise the POSIX file-permission handling for the keystore and the
4
+ connection registry so neither the Fernet key nor the (possibly cleartext)
5
+ DSN registry is written world-readable. All helpers are POSIX-oriented and
6
+ degrade to a plain write on non-POSIX platforms (e.g. Windows), where file
7
+ modes are not enforced the same way.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import os
14
+ import stat
15
+ from pathlib import Path
16
+
17
+ __all__ = [
18
+ "atomic_write_bytes",
19
+ "atomic_write_text",
20
+ "secure_mkdir",
21
+ "warn_if_world_readable",
22
+ ]
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _POSIX = os.name == "posix"
27
+
28
+
29
+ def secure_mkdir(path: Path, *, mode: int = 0o700) -> None:
30
+ """Create ``path`` (and parents) and tighten it to ``mode`` on POSIX.
31
+
32
+ ``mkdir``'s mode argument is masked by the process umask, so the mode is
33
+ applied explicitly with ``chmod`` after creation.
34
+ """
35
+ path.mkdir(parents=True, exist_ok=True)
36
+ if _POSIX:
37
+ try:
38
+ os.chmod(path, mode)
39
+ except OSError as exc: # pragma: no cover - platform/filesystem specific
40
+ logger.debug("could not chmod %s to %o: %s", path, mode, exc)
41
+
42
+
43
+ def atomic_write_bytes(path: Path, data: bytes, *, mode: int = 0o600) -> None:
44
+ """Atomically write ``data`` to ``path``.
45
+
46
+ On POSIX the temp file is created with ``O_EXCL`` at ``mode`` (default
47
+ ``0o600``) so the bytes are never visible at looser permissions, then
48
+ ``os.replace``-d into place.
49
+ """
50
+ path.parent.mkdir(parents=True, exist_ok=True)
51
+ tmp = path.with_name(f"{path.name}.{os.getpid()}.tmp")
52
+ try:
53
+ if _POSIX:
54
+ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_TRUNC, mode)
55
+ with os.fdopen(fd, "wb") as fh:
56
+ fh.write(data)
57
+ else: # pragma: no cover - exercised on Windows only
58
+ tmp.write_bytes(data)
59
+ os.replace(tmp, path)
60
+ except BaseException:
61
+ try:
62
+ tmp.unlink()
63
+ except OSError:
64
+ pass
65
+ raise
66
+
67
+
68
+ def atomic_write_text(
69
+ path: Path, text: str, *, mode: int = 0o600, encoding: str = "utf-8"
70
+ ) -> None:
71
+ """Atomically write ``text`` to ``path`` (see :func:`atomic_write_bytes`)."""
72
+ atomic_write_bytes(path, text.encode(encoding), mode=mode)
73
+
74
+
75
+ def warn_if_world_readable(path: Path, *, allowed_mode: int = 0o600) -> None:
76
+ """Log a warning if ``path`` carries permission bits outside ``allowed_mode``.
77
+
78
+ ``allowed_mode`` is a bitmask of the permission bits considered safe (default
79
+ ``0o600`` — owner read/write); any bit set on ``path`` but not in the mask
80
+ triggers the warning.
81
+ """
82
+ if not _POSIX or not path.exists():
83
+ return
84
+ current = stat.S_IMODE(path.stat().st_mode)
85
+ if current & ~allowed_mode & 0o777:
86
+ logger.warning(
87
+ "%s has permissions %o (looser than %o); tighten it with: chmod %o %s",
88
+ path,
89
+ current,
90
+ allowed_mode,
91
+ allowed_mode,
92
+ path,
93
+ )