ripple-sql 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.
- ripple/__init__.py +31 -0
- ripple/answer.py +473 -0
- ripple/answer_page.py +214 -0
- ripple/cache.py +80 -0
- ripple/ci.py +422 -0
- ripple/ci_signature.py +374 -0
- ripple/cli.py +733 -0
- ripple/doctor.py +225 -0
- ripple/engine/__init__.py +111 -0
- ripple/engine/budget.py +86 -0
- ripple/engine/column_lineage.py +112 -0
- ripple/engine/column_ref.py +818 -0
- ripple/engine/cte_tracing.py +1309 -0
- ripple/engine/dependencies.py +466 -0
- ripple/engine/dialect.py +132 -0
- ripple/engine/dispatch.py +12 -0
- ripple/engine/extraction.py +27 -0
- ripple/engine/jinja.py +282 -0
- ripple/engine/json_sources.py +241 -0
- ripple/engine/macro_source.py +127 -0
- ripple/engine/pipeline.py +265 -0
- ripple/engine/preprocess.py +174 -0
- ripple/engine/safe_gen.py +21 -0
- ripple/engine/schema_qualification.py +151 -0
- ripple/engine/scope.py +488 -0
- ripple/engine/select_sources.py +1038 -0
- ripple/engine/sql_script.py +729 -0
- ripple/engine/statement.py +449 -0
- ripple/engine/tech_debt.py +169 -0
- ripple/engine/tsql_catalog.py +83 -0
- ripple/engine/tsql_scalar_vars.py +248 -0
- ripple/engine/tsql_tvf.py +653 -0
- ripple/engine/tsql_xml.py +97 -0
- ripple/engine/types.py +167 -0
- ripple/engine/unused_deps.py +555 -0
- ripple/engine/validation.py +158 -0
- ripple/graph.py +1499 -0
- ripple/home.py +232 -0
- ripple/loaders/__init__.py +7 -0
- ripple/loaders/dbt.py +359 -0
- ripple/loaders/dbt_config.py +339 -0
- ripple/loaders/identity.py +328 -0
- ripple/loaders/sidecar.py +65 -0
- ripple/loaders/sqldir.py +262 -0
- ripple/loaders/types.py +197 -0
- ripple/lookml.py +163 -0
- ripple/mcp_server.py +600 -0
- ripple/names.py +40 -0
- ripple/project.py +167 -0
- ripple/py.typed +0 -0
- ripple/render.py +426 -0
- ripple/render_shims.py +209 -0
- ripple/schemas.py +155 -0
- ripple/semantic.py +232 -0
- ripple/server.py +184 -0
- ripple/sourcefiles.py +64 -0
- ripple/star_resolution.py +100 -0
- ripple/static/answer.css +146 -0
- ripple/static/answer.html +358 -0
- ripple/static/answer_twin.js +299 -0
- ripple/static/explore.js +133 -0
- ripple/usage/__init__.py +18 -0
- ripple/usage/cli.py +78 -0
- ripple/usage/collect.py +315 -0
- ripple/usage/discover.py +190 -0
- ripple/usage/ingest.py +414 -0
- ripple/usage/report.py +131 -0
- ripple_sql-0.1.0.dist-info/METADATA +285 -0
- ripple_sql-0.1.0.dist-info/RECORD +72 -0
- ripple_sql-0.1.0.dist-info/WHEEL +4 -0
- ripple_sql-0.1.0.dist-info/entry_points.txt +3 -0
- ripple_sql-0.1.0.dist-info/licenses/LICENSE +202 -0
ripple/doctor.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""`ripple doctor`: prove this install can serve an MCP client before a client tries.
|
|
2
|
+
|
|
3
|
+
The documented MCP failure modes are environmental: the client spawns the wrong
|
|
4
|
+
python, the server prints something that isn't JSON-RPC, or startup blows the
|
|
5
|
+
client's budget (Codex kills servers at 10s). All of them look identical from
|
|
6
|
+
the client side: the tool "connects and then does nothing." So the doctor does
|
|
7
|
+
what a client does: spawns this install's server from an undefined working
|
|
8
|
+
directory and drives a real handshake through it.
|
|
9
|
+
|
|
10
|
+
Warnings never fail the command; only a broken handshake does (exit 1).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
PASS, WARN, FAIL = "ok", "warning", "failed"
|
|
24
|
+
GLYPH = {PASS: "✓", WARN: "!", FAIL: "✗"}
|
|
25
|
+
|
|
26
|
+
# Codex gives a server 10s to start; stay comfortably under it.
|
|
27
|
+
STARTUP_BUDGET_SECONDS = 5.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _version() -> str:
|
|
31
|
+
try:
|
|
32
|
+
from importlib.metadata import version
|
|
33
|
+
|
|
34
|
+
return version("ripple-sql")
|
|
35
|
+
except Exception:
|
|
36
|
+
return "unknown"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def server_command() -> list[str]:
|
|
40
|
+
"""The command a client should be given, absolute paths only.
|
|
41
|
+
|
|
42
|
+
Clients spawn stdio servers with a minimal environment and an undefined
|
|
43
|
+
working directory, so a bare `ripple` that depends on the user's shell
|
|
44
|
+
PATH is exactly what breaks.
|
|
45
|
+
"""
|
|
46
|
+
exe = shutil.which("ripple")
|
|
47
|
+
if exe:
|
|
48
|
+
return [str(Path(exe).resolve()), "mcp"]
|
|
49
|
+
# sys.argv[1:] forwards appended flags: without it the printed
|
|
50
|
+
# `... --path <project>` was decoration and the server silently served
|
|
51
|
+
# the client's working directory, usually /
|
|
52
|
+
return [
|
|
53
|
+
sys.executable,
|
|
54
|
+
"-c",
|
|
55
|
+
"import sys; from ripple.cli import main; main(['mcp'] + sys.argv[1:])",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _check_install() -> dict:
|
|
60
|
+
return {
|
|
61
|
+
"name": "install",
|
|
62
|
+
"status": PASS,
|
|
63
|
+
"detail": f"ripple {_version()} on python {sys.version.split()[0]} at {sys.executable}",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _check_path() -> dict:
|
|
68
|
+
exe = shutil.which("ripple")
|
|
69
|
+
if exe is None:
|
|
70
|
+
return {
|
|
71
|
+
"name": "ripple on PATH",
|
|
72
|
+
"status": WARN,
|
|
73
|
+
"detail": (
|
|
74
|
+
"no `ripple` on PATH (fine for uvx; otherwise give your client "
|
|
75
|
+
"the absolute command printed below)"
|
|
76
|
+
),
|
|
77
|
+
}
|
|
78
|
+
# no resolve(): a venv python is a symlink out of the venv, and following
|
|
79
|
+
# it would flag every healthy venv as a mismatch
|
|
80
|
+
same_env = Path(exe).parent == Path(sys.executable).parent
|
|
81
|
+
if not same_env:
|
|
82
|
+
return {
|
|
83
|
+
"name": "ripple on PATH",
|
|
84
|
+
"status": WARN,
|
|
85
|
+
"detail": (
|
|
86
|
+
f"{exe} belongs to a different environment than this python "
|
|
87
|
+
f"({sys.executable}); a client using the PATH one may get a "
|
|
88
|
+
"different ripple version"
|
|
89
|
+
),
|
|
90
|
+
}
|
|
91
|
+
return {"name": "ripple on PATH", "status": PASS, "detail": exe}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _check_handshake(command: list[str]) -> list[dict]:
|
|
95
|
+
requests = [
|
|
96
|
+
{
|
|
97
|
+
"jsonrpc": "2.0",
|
|
98
|
+
"id": 1,
|
|
99
|
+
"method": "initialize",
|
|
100
|
+
"params": {
|
|
101
|
+
"protocolVersion": "2025-06-18",
|
|
102
|
+
"capabilities": {},
|
|
103
|
+
"clientInfo": {"name": "ripple-doctor", "version": _version()},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{"jsonrpc": "2.0", "method": "notifications/initialized"},
|
|
107
|
+
{"jsonrpc": "2.0", "id": 2, "method": "tools/list"},
|
|
108
|
+
]
|
|
109
|
+
payload = "".join(json.dumps(r) + "\n" for r in requests)
|
|
110
|
+
started = time.monotonic()
|
|
111
|
+
try:
|
|
112
|
+
# cwd "/" reproduces the undefined working directory clients start
|
|
113
|
+
# servers from; initialize and tools/list must not touch the filesystem
|
|
114
|
+
proc = subprocess.run(
|
|
115
|
+
command,
|
|
116
|
+
input=payload,
|
|
117
|
+
capture_output=True,
|
|
118
|
+
text=True,
|
|
119
|
+
cwd="/",
|
|
120
|
+
timeout=30,
|
|
121
|
+
)
|
|
122
|
+
except subprocess.TimeoutExpired:
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
"name": "mcp handshake",
|
|
126
|
+
"status": FAIL,
|
|
127
|
+
"detail": f"server did not answer within 30s: {' '.join(command)}",
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
except OSError as e:
|
|
131
|
+
return [
|
|
132
|
+
{
|
|
133
|
+
"name": "mcp handshake",
|
|
134
|
+
"status": FAIL,
|
|
135
|
+
"detail": f"could not start {' '.join(command)}: {e}",
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
elapsed = time.monotonic() - started
|
|
139
|
+
|
|
140
|
+
checks: list[dict] = []
|
|
141
|
+
responses = {}
|
|
142
|
+
dirty = None
|
|
143
|
+
for raw in proc.stdout.splitlines():
|
|
144
|
+
if not raw.strip():
|
|
145
|
+
continue
|
|
146
|
+
try:
|
|
147
|
+
message = json.loads(raw)
|
|
148
|
+
if message.get("jsonrpc") != "2.0":
|
|
149
|
+
raise ValueError("not a JSON-RPC message")
|
|
150
|
+
except ValueError:
|
|
151
|
+
dirty = raw
|
|
152
|
+
continue
|
|
153
|
+
responses[message.get("id")] = message
|
|
154
|
+
|
|
155
|
+
init = responses.get(1, {}).get("result", {})
|
|
156
|
+
tools = responses.get(2, {}).get("result", {}).get("tools", [])
|
|
157
|
+
if init.get("serverInfo", {}).get("name") == "ripple" and tools:
|
|
158
|
+
checks.append(
|
|
159
|
+
{
|
|
160
|
+
"name": "mcp handshake",
|
|
161
|
+
"status": PASS,
|
|
162
|
+
"detail": f"initialize + tools/list answered, {len(tools)} tools",
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
checks.append(
|
|
167
|
+
{
|
|
168
|
+
"name": "mcp handshake",
|
|
169
|
+
"status": FAIL,
|
|
170
|
+
"detail": (
|
|
171
|
+
f"no valid handshake from {' '.join(command)}"
|
|
172
|
+
+ (f"; stderr: {proc.stderr.strip()[:300]}" if proc.stderr.strip() else "")
|
|
173
|
+
),
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
checks.append(
|
|
178
|
+
{
|
|
179
|
+
"name": "clean stdout",
|
|
180
|
+
"status": PASS if dirty is None else FAIL,
|
|
181
|
+
"detail": (
|
|
182
|
+
"every stdout line is JSON-RPC"
|
|
183
|
+
if dirty is None
|
|
184
|
+
else f"stdout carried a non-protocol line, which breaks clients: {dirty[:200]!r}"
|
|
185
|
+
),
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
slow = elapsed > STARTUP_BUDGET_SECONDS
|
|
189
|
+
checks.append(
|
|
190
|
+
{
|
|
191
|
+
"name": "startup speed",
|
|
192
|
+
"status": WARN if slow else PASS,
|
|
193
|
+
"detail": f"handshake took {elapsed:.1f}s"
|
|
194
|
+
+ (" (Codex allows 10s; investigate)" if slow else ""),
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
return checks
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def run(as_json: bool = False) -> int:
|
|
201
|
+
command = server_command()
|
|
202
|
+
checks = [_check_install(), _check_path(), *_check_handshake(command)]
|
|
203
|
+
ok = all(c["status"] != FAIL for c in checks)
|
|
204
|
+
|
|
205
|
+
if as_json:
|
|
206
|
+
print(json.dumps({"ok": ok, "command": command, "checks": checks}, indent=2))
|
|
207
|
+
return 0 if ok else 1
|
|
208
|
+
|
|
209
|
+
for check in checks:
|
|
210
|
+
print(f"[{GLYPH[check['status']]}] {check['name']}: {check['detail']}")
|
|
211
|
+
print()
|
|
212
|
+
if ok:
|
|
213
|
+
project = Path(os.getcwd()).resolve()
|
|
214
|
+
print("This install can serve MCP clients. Point yours at it with absolute paths")
|
|
215
|
+
print("(clients often start servers from / with a minimal environment):")
|
|
216
|
+
import shlex
|
|
217
|
+
|
|
218
|
+
# shlex, not join: the no-binary fallback is python -c "import ...; ..."
|
|
219
|
+
# and unquoted it splits at the semicolon when pasted into a shell
|
|
220
|
+
printable = shlex.join([*command, "--path", str(project)])
|
|
221
|
+
print(f" {printable}")
|
|
222
|
+
print(f" e.g. claude mcp add ripple -- {printable}")
|
|
223
|
+
else:
|
|
224
|
+
print("Something above is broken. Attach `ripple doctor --json` to a bug report.")
|
|
225
|
+
return 0 if ok else 1
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""SQL analysis engine.
|
|
2
|
+
|
|
3
|
+
Works on any SQL, dbt projects or plain files alike.
|
|
4
|
+
|
|
5
|
+
Key exports:
|
|
6
|
+
|
|
7
|
+
Column Lineage (C_con - Contributing Columns):
|
|
8
|
+
- extract_column_lineage_fast: Fast single-pass column extraction
|
|
9
|
+
- extract_column_lineage_with_ctes: Full extraction with CTE mappings
|
|
10
|
+
- extract_cte_column_lineage: Per-CTE column lineage
|
|
11
|
+
- extract_cte_column_mappings: CTE name -> column mappings
|
|
12
|
+
- extract_primary_source_from_macro: Primary source detection
|
|
13
|
+
- trace_through_ctes: Recursive CTE tracing
|
|
14
|
+
- extract_lineage_complete: Unified API with C_con + C_ref
|
|
15
|
+
|
|
16
|
+
Column Dependencies (C_ref - Referenced Columns):
|
|
17
|
+
- extract_column_dependencies: WHERE/HAVING/QUALIFY clause columns
|
|
18
|
+
- extract_all_referenced_columns: All column references with context
|
|
19
|
+
|
|
20
|
+
Types:
|
|
21
|
+
- FilterDependency: Column used in filter clauses
|
|
22
|
+
- JoinKeyPair: Columns used in JOIN conditions
|
|
23
|
+
- ColumnDependencies: Complete C_ref result
|
|
24
|
+
- UnifiedLineageResult: Combined C_con + C_ref
|
|
25
|
+
|
|
26
|
+
Tech Debt:
|
|
27
|
+
- analyze_unused_deps_safe: Find unused JOINs/CTEs safely
|
|
28
|
+
- get_analysis_limited_message: Human-readable limitation explanation
|
|
29
|
+
|
|
30
|
+
Dialect Mapping:
|
|
31
|
+
- ADAPTER_TO_SQLGLOT_DIALECT: Adapter -> SQLGlot dialect mapping
|
|
32
|
+
- DEFAULT_DIALECT: Default dialect (snowflake)
|
|
33
|
+
- get_sqlglot_dialect: Get dialect from dbt manifest
|
|
34
|
+
- get_dialect_from_adapter_type: Get dialect from adapter type string
|
|
35
|
+
- get_dialect_from_config: Get dialect from a config dict with adapter_type
|
|
36
|
+
- get_supported_adapters: List of supported adapter types
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Column lineage (main extraction)
|
|
40
|
+
from ripple.engine.column_lineage import (
|
|
41
|
+
extract_column_lineage_fast,
|
|
42
|
+
extract_column_lineage_with_ctes,
|
|
43
|
+
extract_cte_column_lineage,
|
|
44
|
+
extract_cte_column_mappings,
|
|
45
|
+
extract_lineage_complete,
|
|
46
|
+
extract_primary_source_from_macro,
|
|
47
|
+
trace_through_ctes,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Column dependencies (C_ref)
|
|
51
|
+
from ripple.engine.dependencies import (
|
|
52
|
+
extract_all_referenced_columns,
|
|
53
|
+
extract_column_dependencies,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Dialect mapping
|
|
57
|
+
from ripple.engine.dialect import (
|
|
58
|
+
ADAPTER_TO_SQLGLOT_DIALECT,
|
|
59
|
+
DEFAULT_DIALECT,
|
|
60
|
+
get_dialect_from_adapter_type,
|
|
61
|
+
get_dialect_from_config,
|
|
62
|
+
get_sqlglot_dialect,
|
|
63
|
+
get_supported_adapters,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Tech debt analysis
|
|
67
|
+
from ripple.engine.tech_debt import (
|
|
68
|
+
analyze_unused_deps_safe,
|
|
69
|
+
get_analysis_limited_message,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Types
|
|
73
|
+
from ripple.engine.types import (
|
|
74
|
+
ColumnDependencies,
|
|
75
|
+
FilterDependency,
|
|
76
|
+
JoinKeyPair,
|
|
77
|
+
TrustLevel,
|
|
78
|
+
UnifiedLineageResult,
|
|
79
|
+
WarehouseColumns,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
__all__ = [
|
|
83
|
+
# Column lineage (C_con)
|
|
84
|
+
"extract_column_lineage_fast",
|
|
85
|
+
"extract_column_lineage_with_ctes",
|
|
86
|
+
"extract_cte_column_lineage",
|
|
87
|
+
"extract_cte_column_mappings",
|
|
88
|
+
"extract_primary_source_from_macro",
|
|
89
|
+
"trace_through_ctes",
|
|
90
|
+
"extract_lineage_complete",
|
|
91
|
+
# Column dependencies (C_ref) - NEW
|
|
92
|
+
"extract_column_dependencies",
|
|
93
|
+
"extract_all_referenced_columns",
|
|
94
|
+
# Types - NEW
|
|
95
|
+
"TrustLevel",
|
|
96
|
+
"WarehouseColumns",
|
|
97
|
+
"FilterDependency",
|
|
98
|
+
"JoinKeyPair",
|
|
99
|
+
"ColumnDependencies",
|
|
100
|
+
"UnifiedLineageResult",
|
|
101
|
+
# Tech debt
|
|
102
|
+
"analyze_unused_deps_safe",
|
|
103
|
+
"get_analysis_limited_message",
|
|
104
|
+
# Dialect
|
|
105
|
+
"ADAPTER_TO_SQLGLOT_DIALECT",
|
|
106
|
+
"DEFAULT_DIALECT",
|
|
107
|
+
"get_dialect_from_adapter_type",
|
|
108
|
+
"get_dialect_from_config",
|
|
109
|
+
"get_sqlglot_dialect",
|
|
110
|
+
"get_supported_adapters",
|
|
111
|
+
]
|
ripple/engine/budget.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Wall-clock budget for per-file work that can wedge.
|
|
2
|
+
|
|
3
|
+
Pathological SQL can wedge sqlglot for half an hour: redshift-utils admin DDL
|
|
4
|
+
at parse time, and a generated 3.7MB single-line SELECT that parses in 13s and
|
|
5
|
+
then spends another 139s inside qualify_tables during analysis. One budget
|
|
6
|
+
covers both phases (RIPPLE_PARSE_BUDGET_S, default 20s, 0 disables).
|
|
7
|
+
|
|
8
|
+
Work runs on a daemon thread abandoned at the budget. An abandoned thread
|
|
9
|
+
stays busy until process exit, so after MAX_ABANDONED of them further budgeted
|
|
10
|
+
work is not attempted and reports timed out immediately.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import threading
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
MAX_ABANDONED = 8
|
|
21
|
+
_abandoned = 0 # kept for tests that pin the cap; the live count is _zombies
|
|
22
|
+
_zombies: list[threading.Thread] = []
|
|
23
|
+
|
|
24
|
+
TIMED_OUT = object()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _alive_zombies() -> int:
|
|
28
|
+
"""Abandoned workers still burning CPU. A cumulative count never reset,
|
|
29
|
+
so a long-lived MCP session that had ever abandoned eight parses refused
|
|
30
|
+
every budgeted call after that, and each rebuild reported the whole repo
|
|
31
|
+
as timed_out."""
|
|
32
|
+
_zombies[:] = [t for t in _zombies if t.is_alive()]
|
|
33
|
+
return len(_zombies)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def budget_seconds() -> float:
|
|
37
|
+
try:
|
|
38
|
+
return float(os.environ.get("RIPPLE_PARSE_BUDGET_S", "20"))
|
|
39
|
+
except ValueError:
|
|
40
|
+
return 20.0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def max_sql_bytes() -> int:
|
|
44
|
+
"""Statements over this size are refused before any parse or analysis.
|
|
45
|
+
|
|
46
|
+
Preferable to the thread budget wherever it applies: an abandoned worker
|
|
47
|
+
keeps holding the GIL, slowing everything after it, while a size refusal
|
|
48
|
+
costs nothing. 0 disables."""
|
|
49
|
+
try:
|
|
50
|
+
return int(os.environ.get("RIPPLE_MAX_SQL_BYTES", "1000000"))
|
|
51
|
+
except ValueError:
|
|
52
|
+
return 1_000_000
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def run_within_budget(label: str, fn: Callable[[], Any], budget: float | None = None):
|
|
56
|
+
"""fn() under a wall-clock cap; returns TIMED_OUT when the budget blows.
|
|
57
|
+
|
|
58
|
+
fn must not mutate shared state: an abandoned worker runs to completion in
|
|
59
|
+
the background, and anything it wrote would race the caller.
|
|
60
|
+
"""
|
|
61
|
+
global _abandoned
|
|
62
|
+
if budget is None:
|
|
63
|
+
budget = budget_seconds()
|
|
64
|
+
if budget <= 0:
|
|
65
|
+
return fn()
|
|
66
|
+
if max(_abandoned, _alive_zombies()) >= MAX_ABANDONED:
|
|
67
|
+
return TIMED_OUT
|
|
68
|
+
box: list = []
|
|
69
|
+
|
|
70
|
+
def run():
|
|
71
|
+
try:
|
|
72
|
+
box.append(fn())
|
|
73
|
+
except Exception as e:
|
|
74
|
+
box.append(e)
|
|
75
|
+
|
|
76
|
+
worker = threading.Thread(target=run, daemon=True, name=f"ripple-budget-{label}")
|
|
77
|
+
worker.start()
|
|
78
|
+
worker.join(budget)
|
|
79
|
+
if worker.is_alive():
|
|
80
|
+
_zombies.append(worker)
|
|
81
|
+
_abandoned = 0 # the live list is the count; _abandoned only pins tests
|
|
82
|
+
return TIMED_OUT
|
|
83
|
+
result = box[0] if box else None
|
|
84
|
+
if isinstance(result, Exception):
|
|
85
|
+
raise result
|
|
86
|
+
return result
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Column lineage: re-exports from the engine submodules.
|
|
2
|
+
|
|
3
|
+
Re-exports the engine's public functions from one place. Prefer importing from
|
|
4
|
+
the specific submodule.
|
|
5
|
+
|
|
6
|
+
Submodules:
|
|
7
|
+
- types.py: Type definitions and dataclasses
|
|
8
|
+
- schema_qualification.py: Warehouse schema disambiguation
|
|
9
|
+
- validation.py: Trust scoring and UNION validation
|
|
10
|
+
- cte_tracing.py: CTE extraction and recursive tracing
|
|
11
|
+
- extraction.py: Main extraction entry points
|
|
12
|
+
- dependencies.py: WHERE/JOIN column tracking (C_ref)
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
from ripple.engine.extraction import extract_column_lineage_fast
|
|
16
|
+
from ripple.engine.dependencies import extract_column_dependencies
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Types (from types.py)
|
|
20
|
+
# CTE tracing (from cte_tracing.py)
|
|
21
|
+
from ripple.engine.cte_tracing import (
|
|
22
|
+
extract_cte_column_lineage,
|
|
23
|
+
extract_cte_column_mappings,
|
|
24
|
+
trace_through_ctes,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Dependencies (from dependencies.py)
|
|
28
|
+
from ripple.engine.dependencies import (
|
|
29
|
+
extract_all_referenced_columns,
|
|
30
|
+
extract_column_dependencies,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Main extraction (from extraction.py)
|
|
34
|
+
from ripple.engine.extraction import (
|
|
35
|
+
extract_column_lineage_fast,
|
|
36
|
+
extract_column_lineage_with_ctes,
|
|
37
|
+
extract_lineage_complete,
|
|
38
|
+
extract_primary_source_from_macro,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Schema qualification (from schema_qualification.py)
|
|
42
|
+
from ripple.engine.schema_qualification import (
|
|
43
|
+
# Underscore-prefixed aliases
|
|
44
|
+
_build_sqlglot_schema,
|
|
45
|
+
_qualify_sql_with_schema,
|
|
46
|
+
_resolve_column_table,
|
|
47
|
+
build_sqlglot_schema,
|
|
48
|
+
qualify_sql_with_schema,
|
|
49
|
+
resolve_column_table,
|
|
50
|
+
)
|
|
51
|
+
from ripple.engine.types import (
|
|
52
|
+
MAX_REAL_TABLES,
|
|
53
|
+
MAX_SQL_LENGTH,
|
|
54
|
+
MAX_WAREHOUSE_TABLES,
|
|
55
|
+
ColumnDependencies,
|
|
56
|
+
FilterDependency,
|
|
57
|
+
JoinKeyPair,
|
|
58
|
+
TrustLevel,
|
|
59
|
+
UnifiedLineageResult,
|
|
60
|
+
WarehouseColumns,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Validation (from validation.py)
|
|
64
|
+
from ripple.engine.validation import (
|
|
65
|
+
_get_select_column_names,
|
|
66
|
+
# Underscore-prefixed aliases
|
|
67
|
+
_get_trust_level,
|
|
68
|
+
_validate_union_columns,
|
|
69
|
+
get_select_column_names,
|
|
70
|
+
get_trust_level,
|
|
71
|
+
validate_union_columns,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
# Main extraction functions
|
|
76
|
+
"extract_column_lineage_fast",
|
|
77
|
+
"extract_column_lineage_with_ctes",
|
|
78
|
+
"extract_lineage_complete",
|
|
79
|
+
# CTE functions
|
|
80
|
+
"extract_cte_column_lineage",
|
|
81
|
+
"extract_cte_column_mappings",
|
|
82
|
+
"extract_primary_source_from_macro",
|
|
83
|
+
"trace_through_ctes",
|
|
84
|
+
# Dependency extraction
|
|
85
|
+
"extract_column_dependencies",
|
|
86
|
+
"extract_all_referenced_columns",
|
|
87
|
+
# Types
|
|
88
|
+
"TrustLevel",
|
|
89
|
+
"WarehouseColumns",
|
|
90
|
+
"FilterDependency",
|
|
91
|
+
"JoinKeyPair",
|
|
92
|
+
"ColumnDependencies",
|
|
93
|
+
"UnifiedLineageResult",
|
|
94
|
+
# Constants
|
|
95
|
+
"MAX_WAREHOUSE_TABLES",
|
|
96
|
+
"MAX_REAL_TABLES",
|
|
97
|
+
"MAX_SQL_LENGTH",
|
|
98
|
+
# Public helpers
|
|
99
|
+
"build_sqlglot_schema",
|
|
100
|
+
"resolve_column_table",
|
|
101
|
+
"qualify_sql_with_schema",
|
|
102
|
+
"get_trust_level",
|
|
103
|
+
"get_select_column_names",
|
|
104
|
+
"validate_union_columns",
|
|
105
|
+
# Underscore-prefixed aliases
|
|
106
|
+
"_build_sqlglot_schema",
|
|
107
|
+
"_resolve_column_table",
|
|
108
|
+
"_qualify_sql_with_schema",
|
|
109
|
+
"_get_trust_level",
|
|
110
|
+
"_validate_union_columns",
|
|
111
|
+
"_get_select_column_names",
|
|
112
|
+
]
|