ripple-sql 0.1.4__tar.gz → 0.1.5__tar.gz

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 (76) hide show
  1. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/CHANGELOG.md +7 -1
  2. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/PKG-INFO +1 -1
  3. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/pyproject.toml +1 -1
  4. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/home.py +17 -0
  5. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/static/answer.css +4 -0
  6. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/static/answer.html +1 -0
  7. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/static/explore.js +27 -13
  8. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/static/focus.js +1 -14
  9. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/.gitignore +0 -0
  10. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/LICENSE +0 -0
  11. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/README.md +0 -0
  12. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/__init__.py +0 -0
  13. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/answer.py +0 -0
  14. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/answer_page.py +0 -0
  15. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/cache.py +0 -0
  16. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/ci.py +0 -0
  17. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/ci_signature.py +0 -0
  18. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/cli.py +0 -0
  19. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/doctor.py +0 -0
  20. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/__init__.py +0 -0
  21. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/budget.py +0 -0
  22. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/column_lineage.py +0 -0
  23. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/column_ref.py +0 -0
  24. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/cte_tracing.py +0 -0
  25. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/dependencies.py +0 -0
  26. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/dialect.py +0 -0
  27. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/dispatch.py +0 -0
  28. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/extraction.py +0 -0
  29. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/generators.py +0 -0
  30. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/jinja.py +0 -0
  31. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/json_sources.py +0 -0
  32. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/macro_source.py +0 -0
  33. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/pipeline.py +0 -0
  34. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/preprocess.py +0 -0
  35. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/safe_gen.py +0 -0
  36. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/schema_qualification.py +0 -0
  37. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/scope.py +0 -0
  38. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/select_sources.py +0 -0
  39. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/sql_script.py +0 -0
  40. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/statement.py +0 -0
  41. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/tech_debt.py +0 -0
  42. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/tsql_catalog.py +0 -0
  43. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/tsql_scalar_vars.py +0 -0
  44. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/tsql_tvf.py +0 -0
  45. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/tsql_xml.py +0 -0
  46. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/types.py +0 -0
  47. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/unused_deps.py +0 -0
  48. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/engine/validation.py +0 -0
  49. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/graph.py +0 -0
  50. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/loaders/__init__.py +0 -0
  51. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/loaders/dbt.py +0 -0
  52. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/loaders/dbt_config.py +0 -0
  53. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/loaders/identity.py +0 -0
  54. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/loaders/sidecar.py +0 -0
  55. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/loaders/sqldir.py +0 -0
  56. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/loaders/types.py +0 -0
  57. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/lookml.py +0 -0
  58. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/mcp_server.py +0 -0
  59. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/names.py +0 -0
  60. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/project.py +0 -0
  61. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/py.typed +0 -0
  62. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/render.py +0 -0
  63. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/render_shims.py +0 -0
  64. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/schemas.py +0 -0
  65. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/semantic.py +0 -0
  66. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/server.py +0 -0
  67. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/sourcefiles.py +0 -0
  68. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/star_resolution.py +0 -0
  69. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/static/answer_twin.js +0 -0
  70. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/static/find.js +0 -0
  71. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/usage/__init__.py +0 -0
  72. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/usage/cli.py +0 -0
  73. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/usage/collect.py +0 -0
  74. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/usage/discover.py +0 -0
  75. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/usage/ingest.py +0 -0
  76. {ripple_sql-0.1.4 → ripple_sql-0.1.5}/src/ripple/usage/report.py +0 -0
@@ -6,6 +6,11 @@ Notable changes to Ripple. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.5] - 2026-09-09
10
+
11
+ ### Added
12
+ - Every column under a model on the page carries the number of models that read it directly, the same number a model chip carries, so the cost of a change shows before the question is asked. A column no link touches (a constant, or one the engine could not trace) is drawn loose, dashed and faint, with "no link found" in the key; r/dataengineering asked for "a clear view of mapped vs unmapped fields (with some kind of legend)", and a traced-looking constant would have been a lie.
13
+
9
14
  ## [0.1.4] - 2026-09-09
10
15
 
11
16
  ### Changed
@@ -249,7 +254,8 @@ First public release.
249
254
  ### Removed
250
255
  - The dark whole-graph canvas that `ripple serve` used to open (`static/index.html`). Every surface now draws one answer.
251
256
 
252
- [Unreleased]: https://github.com/bteh/ripple/compare/v0.1.4...HEAD
257
+ [Unreleased]: https://github.com/bteh/ripple/compare/v0.1.5...HEAD
258
+ [0.1.5]: https://github.com/bteh/ripple/releases/tag/v0.1.5
253
259
  [0.1.4]: https://github.com/bteh/ripple/releases/tag/v0.1.4
254
260
  [0.1.3]: https://github.com/bteh/ripple/releases/tag/v0.1.3
255
261
  [0.1.2]: https://github.com/bteh/ripple/releases/tag/v0.1.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ripple-sql
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Offline column-level SQL lineage. See what breaks before you merge.
5
5
  Project-URL: Homepage, https://github.com/bteh/ripple
6
6
  Project-URL: Repository, https://github.com/bteh/ripple
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ripple-sql"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  description = "Offline column-level SQL lineage. See what breaks before you merge."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -130,10 +130,19 @@ def model_map(nodes: list[dict], edges) -> dict:
130
130
  }
131
131
  fed = {e.dst_model for e in edges}
132
132
  seen: dict[str, dict[str, None]] = {}
133
+ readers: dict[tuple[str, str], set[str]] = {}
134
+ linked: set[tuple[str, str]] = set()
133
135
  for e in edges:
134
136
  for model, column in ((e.src_model, e.src_column), (e.dst_model, e.dst_column)):
135
137
  if _is_column(column):
136
138
  seen.setdefault(model, {})[column] = None
139
+ if e.kind != "value" or not (_is_column(e.src_column) and _is_column(e.dst_column)):
140
+ continue
141
+ linked.add((e.src_model, e.src_column))
142
+ linked.add((e.dst_model, e.dst_column))
143
+ # a dashboard number is not a reader; only models count (as in starters)
144
+ if not e.dst_model.startswith(DASHBOARD_PREFIXES):
145
+ readers.setdefault((e.src_model, e.src_column), set()).add(e.dst_model)
137
146
  for name in sorted({e.src_model for e in edges} | fed):
138
147
  if name in members:
139
148
  known = members[name]["columns"]
@@ -151,6 +160,14 @@ def model_map(nodes: list[dict], edges) -> dict:
151
160
  "status": None,
152
161
  "path": None,
153
162
  }
163
+ for m in members.values():
164
+ # per column, aligned with `columns`: how many models read it directly;
165
+ # -1 when no link touches it at all (a constant, or one the engine
166
+ # could not trace), so the page never draws such a column as traced
167
+ m["reads"] = [
168
+ len(readers.get((m["id"], c), ())) if (m["id"], c) in linked else -1
169
+ for c in m["columns"]
170
+ ]
154
171
  links = _links(edges)
155
172
  layers = _layers(members, links)
156
173
  models = [{**m, "layer": layers[m["id"]]} for m in members.values()]
@@ -77,6 +77,7 @@ h1 code{font:500 24px/1 var(--mono); letter-spacing:0}
77
77
  .key i.r{border-top-style:dashed; border-color:var(--review)}
78
78
  .key s{display:inline-block; width:11px; height:11px; border-radius:2px; background:var(--ink); border:1px solid var(--ink); text-decoration:none}
79
79
  .key s.src{background:var(--well); border:1px dashed var(--line-strong)}
80
+ .key s.unk{background:transparent; border:1px dashed var(--faint)}
80
81
  .key small.n{font:400 11.5px/1 var(--sans); color:var(--faint); border:1px solid var(--line-strong); border-radius:var(--r); padding:2px 5px; background:var(--surface)}
81
82
  .hint{margin-left:auto; font-size:12.5px; color:var(--faint)}
82
83
  .rows{padding:0 22px 14px; font-size:13.5px; color:var(--muted)}
@@ -110,6 +111,9 @@ pre{margin:0; padding:16px 22px 18px; overflow-x:auto; font:400 13px/1.65 var(--
110
111
  .cols{display:flex; flex-direction:column; gap:5px; margin:0 0 6px 10px; padding-left:10px; border-left:1px solid var(--line)}
111
112
  .cols .gname{max-width:36ch; white-space:normal; line-height:1.45; padding-top:2px}
112
113
  .cols .node{cursor:pointer}
114
+ .cols .node small{font:400 11px/1 var(--sans); color:var(--faint); margin-left:5px}
115
+ .cols .node.unk{color:var(--muted)}
116
+ .cols .node.unk:hover{border-color:var(--accent)}
113
117
  .cols .node:hover{border-color:var(--accent); color:var(--accent-strong)}
114
118
  .more-chips{font:500 12.5px var(--sans); color:var(--accent); background:none; border:0; padding:4px 0; cursor:pointer; text-align:left}
115
119
  .more-chips:hover{text-decoration:underline}
@@ -35,6 +35,7 @@
35
35
  <span class="key"><i class="r"></i> needs review</span>
36
36
  <span class="key" id="key-chain" hidden><s class="up"></s> feeds it <s class="sel"></s> chosen <s class="down"></s> reads it</span>
37
37
  <span class="key"><small class="n">3</small> models that read it</span>
38
+ <span class="key"><s class="unk"></s> no link found</span>
38
39
  <span class="hidden-note" id="maphidden" hidden></span>
39
40
  <span class="hint" id="maphint">Hover lights the chain. Click a model for its columns, a column to ask.</span>
40
41
  </div>
@@ -8,6 +8,32 @@
8
8
  // nothing here fetches.
9
9
  var COMPACT_AT = 40, MAP_COLUMNS = 60, INDEX_PEEK = 6, INDEX_UNFOLD = 60, INDEX_GROUPS = 24;
10
10
 
11
+ // a model's columns as chips: each with the number of models that read it
12
+ // directly, and a column no link touches (a constant, or one the engine could
13
+ // not trace) drawn loose so nobody reads it as traced. Used under a model on
14
+ // the small map and under the chosen model in focus.
15
+ function columnChips(m, opts){
16
+ var esc = opts.esc, cap = opts.cap || MAP_COLUMNS, cols = document.createElement('div');
17
+ cols.className = 'cols';
18
+ m.columns.slice(0, cap).forEach(function(c, i){
19
+ // reads[i] is the model count, or -1 when no link touches the column
20
+ var n = m.reads ? m.reads[i] : 0, loose = n < 0, s = document.createElement('button');
21
+ s.type = 'button'; s.className = 'node in' + (loose ? ' unk' : '');
22
+ s.innerHTML = esc(c) + (n > 0 ? ' <small>' + n + '</small>' : '');
23
+ s.title = (loose ? 'no link found: a constant, or one Ripple could not trace' :
24
+ n ? 'read by ' + n + ' model' + (n === 1 ? '' : 's') + ' directly' : 'nothing reads it directly') + ' · click to ask about ' + m.id + '.' + c;
25
+ s.addEventListener('click', function(){ opts.onAsk(m.id + '.' + c); });
26
+ cols.appendChild(s);
27
+ });
28
+ var note = null;
29
+ if (m.columns.length > cap) note = '+ ' + (m.columns.length - cap) + ' more, ask by name';
30
+ else if (!m.declared && m.kind === 'source') note = (m.columns.length ? 'only the columns this project names; the full list is' : 'no column list; it is') +
31
+ ' not declared in the project (a select * reads it unnamed). Coverage below adds it.';
32
+ else if (!m.columns.length) note = 'no columns known';
33
+ if (note){ var g = document.createElement('span'); g.className = 'gname'; g.textContent = note; cols.appendChild(g); }
34
+ return cols;
35
+ }
36
+
11
37
  function renderMap(root, map, opts){
12
38
  var inner = root.querySelector('.board-inner'), svg = root.querySelector('.wires'), head = root.querySelector('.map-head');
13
39
  var esc = opts.esc, compact = map.models.length > COMPACT_AT;
@@ -77,19 +103,7 @@ function renderMap(root, map, opts){
77
103
  if (pinned === m.id){ pinned = null; unlight(); }
78
104
  draw(); return;
79
105
  }
80
- var cols = el('div', 'cols');
81
- m.columns.slice(0, MAP_COLUMNS).forEach(function(c){
82
- var s = el('button', 'node in'); s.type = 'button'; s.textContent = c;
83
- s.title = 'ask about ' + m.id + '.' + c;
84
- s.addEventListener('click', function(){ opts.onAsk(m.id + '.' + c); });
85
- cols.appendChild(s);
86
- });
87
- var note = null;
88
- if (m.columns.length > MAP_COLUMNS) note = '+ ' + (m.columns.length - MAP_COLUMNS) + ' more, ask by name';
89
- else if (!m.declared && m.kind === 'source') note = (m.columns.length ? 'only the columns this project names; the full list is' : 'no column list; it is') +
90
- ' not declared in the project (a select * reads it unnamed). Coverage below adds it.';
91
- else if (!m.columns.length) note = 'no columns known';
92
- if (note){ var n = el('span', 'gname'); n.textContent = note; cols.appendChild(n); }
106
+ var cols = columnChips(m, {esc: esc, onAsk: opts.onAsk, cap: MAP_COLUMNS});
93
107
  g.appendChild(cols); g.classList.add('open'); b.setAttribute('aria-expanded', 'true');
94
108
  // a click keeps the chain lit so the mouse can travel to the columns
95
109
  if (pinned && pinned !== m.id){ var was = groups[pinned].querySelector('.cols'); if (was){ was.remove(); groups[pinned].classList.remove('open'); chips[pinned].setAttribute('aria-expanded', 'false'); } }
@@ -81,20 +81,7 @@ function renderFocus(inner, svg, map, idx, id, opts){
81
81
  b.classList.add('sel', 'anchor'); b.setAttribute('aria-current', 'true');
82
82
  dist[id] = 0;
83
83
  g.appendChild(b);
84
- var cols = el('div', 'cols');
85
- m.columns.slice(0, FOCUS_COLUMNS).forEach(function(c){
86
- var s = el('button', 'node in'); s.type = 'button'; s.textContent = c;
87
- s.title = 'ask about ' + m.id + '.' + c;
88
- s.addEventListener('click', function(){ opts.onAsk(m.id + '.' + c); });
89
- cols.appendChild(s);
90
- });
91
- var note = null;
92
- if (m.columns.length > FOCUS_COLUMNS) note = '+ ' + (m.columns.length - FOCUS_COLUMNS) + ' more, ask by name';
93
- else if (!m.declared && m.kind === 'source') note = (m.columns.length ? 'only the columns this project names; the full list is' : 'no column list; it is') +
94
- ' not declared in the project (a select * reads it unnamed). Coverage below adds it.';
95
- else if (!m.columns.length) note = 'no columns known';
96
- if (note){ var n = el('span', 'gname'); n.textContent = note; cols.appendChild(n); }
97
- g.appendChild(cols); lvl.appendChild(g);
84
+ g.appendChild(columnChips(m, {esc: esc, onAsk: opts.onAsk, cap: FOCUS_COLUMNS})); lvl.appendChild(g);
98
85
  return lvl;
99
86
  }
100
87
  function render(){
File without changes
File without changes
File without changes
File without changes
File without changes