quickdbd-cli 0.3.2__py3-none-any.whl → 0.3.4__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.
quickdbd_cli/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.3.2"
1
+ __version__ = "0.3.4"
quickdbd_cli/agent.py CHANGED
@@ -638,12 +638,6 @@ def diff_schema(ws: Workspace, args: dict) -> Result:
638
638
  return Result("\n".join(clip(_header(c) + text.splitlines(), "lines")))
639
639
 
640
640
 
641
- def _svg_size(svg: str) -> tuple[float, float]:
642
- w = re.search(r'\bwidth="([\d.]+)', svg)
643
- h = re.search(r'\bheight="([\d.]+)', svg)
644
- return (float(w.group(1)) if w else 1000.0, float(h.group(1)) if h else 1000.0)
645
-
646
-
647
641
  def render_diagram(ws: Workspace, args: dict) -> Result:
648
642
  c = ws.compile(ws.target(args.get("project")))
649
643
  tables = selected(c, args.get("select"))
@@ -679,7 +673,7 @@ def render_diagram(ws: Workspace, args: dict) -> Result:
679
673
  raise ToolError("the layout took too long; narrow with `select` or `focus`") from None
680
674
  pos = {k: [v[0], v[1]] for k, v in laid.items()}
681
675
  svg = export.render_svg(c.model, pos, only=ids, mode=mode, embed_font=False)
682
- scale = min(2.0, IMAGE_SIDE / max(_svg_size(svg)))
676
+ scale = min(2.0, IMAGE_SIDE / max(export.svg_size(svg)))
683
677
  if scale >= MIN_SCALE[mode]:
684
678
  png = export.svg_to_png(svg, scale)
685
679
  names = ", ".join(t["name"] for t in tables[:40]) + (", …" if n > 40 else "")
@@ -547,6 +547,54 @@
547
547
  return lines.join("\n");
548
548
  }
549
549
 
550
+ /* ------------------------------------------------------------------ lit relationships (viewer only, never exported) */
551
+ var CARD_LABEL = { one: "1", zeroOne: "0..1", many: "*", zeroMany: "0..*" };
552
+ var CARD_WORD = { one: "one", zeroOne: "zero or one", many: "many", zeroMany: "zero or many" };
553
+
554
+ /* Cardinality labels for a lit line: one just past each end, on the side the line leaves from. a end first. */
555
+ function edgeLabels(model, pos, mode, rel, geo) {
556
+ var e = edgeGeom(model, geo || geometry(model, mode), pos, rel);
557
+ if (!e) return null;
558
+ function spot(x, y, dir, card) {
559
+ return { x: n(x + dir * 8), y: n(y - 9), anchor: dir > 0 ? "start" : "end", text: CARD_LABEL[card] || "" };
560
+ }
561
+ return [spot(e.xa, e.ya, e.da, rel.a.card), spot(e.xb, e.yb, e.db, rel.b.card)];
562
+ }
563
+
564
+ /* Text of the relationship card for model.rels[i]: the child (FK / many) side first, a plain-English sentence and
565
+ * the JOIN, built like `qdbd path` so quoting and composite keys match. */
566
+ function relCard(model, i) {
567
+ var r = model.rels[i], byId = {};
568
+ model.tables.forEach(function (t) { byId[t.id] = t; });
569
+ var aMany = r.a.card === "many" || r.a.card === "zeroMany", bMany = r.b.card === "many" || r.b.card === "zeroMany";
570
+ var aIsChild = aMany || !bMany, child = aIsChild ? r.a : r.b, parent = aIsChild ? r.b : r.a;
571
+ var pairs = stepPairs(model, i, child.t);
572
+ function side(end, k) {
573
+ var t = byId[end.t], cols = pairs.map(function (p) {
574
+ var c = t.cols.filter(function (x) { return x.id === p[k]; })[0];
575
+ return c ? c.name : p[k];
576
+ });
577
+ return t.name + (cols.length > 1 ? ".(" + cols.join(", ") + ")" : cols.length ? "." + cols[0] : "");
578
+ }
579
+ var plural = child.card === "many" || child.card === "zeroMany";
580
+ return {
581
+ from: side(child, 0),
582
+ to: side(parent, 1),
583
+ cardFrom: CARD_LABEL[child.card] || "",
584
+ cardTo: CARD_LABEL[parent.card] || "",
585
+ sentence: "Each " + byId[parent.t].name + " row has " + CARD_WORD[child.card] + " " + byId[child.t].name + (plural ? " rows" : " row"),
586
+ sql: child.t === parent.t ? selfJoin(byId[child.t], pairs) : pathSql(model, child.t, [{ from: child, to: parent, pairs: pairs }]),
587
+ };
588
+ }
589
+ /* A table joined to itself needs an alias, or every column is ambiguous. */
590
+ function selfJoin(t, pairs) {
591
+ var name = sqlIdent(t.name), alias = sqlPart(t.name.split(".").pop() + "_2");
592
+ function col(c) { var x = t.cols.filter(function (k) { return k.id === c; })[0]; return sqlPart(x ? x.name : c); }
593
+ var on = pairs.map(function (p) { return alias + "." + col(p[1]) + " = " + name + "." + col(p[0]); });
594
+ return "FROM " + name + "\nJOIN " + name + " AS " + alias + " ON " +
595
+ (on.length ? on.join(" AND ") : "... -- table-level relationship: join columns not specified");
596
+ }
597
+
550
598
  root.qdbd = {
551
599
  findPath: findPath,
552
600
  stepPairs: stepPairs,
@@ -563,8 +611,12 @@
563
611
  renderSvg: renderSvg,
564
612
  bounds: bounds,
565
613
  neighbourhood: neighbourhood,
614
+ edgeLabels: edgeLabels,
615
+ relCard: relCard,
566
616
  esc: esc,
567
617
  };
618
+ root.qdbdEdgeLabels = edgeLabels;
619
+ root.qdbdRelCard = relCard;
568
620
  root.qdbdRenderSvg = renderSvg;
569
621
  root.qdbdSelect = select;
570
622
  root.qdbdRenderDrawio = renderDrawio;
@@ -34,6 +34,8 @@
34
34
  --t-edge: #b8bcc2;
35
35
  --row-hl: #dbe6f6;
36
36
  --row-hover: #eef3fa;
37
+ --lit: #4361ee;
38
+ --lit-deep: #2a3fb8;
37
39
  }
38
40
  /* Dark theme. viewer.js sets data-theme from the toggle, or from prefers-color-scheme when it's on auto. */
39
41
  :root[data-theme="dark"] {
@@ -71,6 +73,8 @@
71
73
  --t-edge: #5b6472;
72
74
  --row-hl: #24344d;
73
75
  --row-hover: #23282f;
76
+ --lit: #8aa0ff;
77
+ --lit-deep: #c3ceff;
74
78
  }
75
79
  ::selection { background: var(--pick); color: var(--ink); }
76
80
  :focus-visible { outline: 2px solid var(--amber); outline-offset: 1px; }
@@ -82,6 +86,7 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
82
86
  #stage { position: relative; flex: 1; overflow: hidden; }
83
87
  #canvas { width: 100%; height: 100%; display: block; cursor: grab; touch-action: none; user-select: none; }
84
88
  #canvas.panning { cursor: grabbing; }
89
+ #canvas.lod .t text { display: none; } /* big diagram, zoomed out: see LOD_TABLES in viewer.js */
85
90
 
86
91
  /* diagram colours: CSS beats the SVG presentation attributes, so the page can theme them */
87
92
  .tb { fill: var(--t-body); stroke: var(--t-border); }
@@ -104,6 +109,36 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
104
109
  .r.hl .rb { fill: var(--row-hl); fill-opacity: 1; }
105
110
  .r:hover .rb { fill: var(--row-hover); fill-opacity: 1; }
106
111
  .t.flash .tb { stroke: #f0a020; stroke-width: 3; }
112
+ .t .tb { transition: filter .18s; }
113
+ .t:hover .tb { filter: drop-shadow(0 6px 14px rgba(16, 24, 40, .16)); }
114
+
115
+ /* lit relationships: hovered or selected table, hovered or clicked line (viewer.js light()) */
116
+ .e.lit .ep { stroke: var(--lit); stroke-width: 2.2; }
117
+ .e.lit path, .e.lit .mc { stroke: var(--lit); }
118
+ .r.lit .rb { fill: var(--row-hl); fill-opacity: 1; }
119
+ .efocus .t:not(.on) { opacity: .18; }
120
+ .efocus .e:not(.lit) { opacity: .07; }
121
+ .fx { pointer-events: none; }
122
+ .fx .flow { fill: none; stroke: var(--lit-deep); stroke-width: 4; stroke-dasharray: 4 18; animation: qdbd-flow 1.1s linear infinite; }
123
+ .fx .flow.amber { stroke: #b86e00; }
124
+ @keyframes qdbd-flow { to { stroke-dashoffset: -22; } }
125
+ .fx .lbl { font: 600 11px Inter, "Helvetica Neue", Helvetica, Arial, sans-serif; fill: var(--lit); paint-order: stroke;
126
+ stroke: var(--bg); stroke-width: 4px; stroke-linejoin: round; animation: qdbd-fade .2s ease-out; }
127
+ @keyframes qdbd-fade { from { opacity: 0; } }
128
+ .hit path { fill: none; stroke: transparent; stroke-width: 14; pointer-events: stroke; cursor: pointer; }
129
+ /* relationship card (a clicked line) */
130
+ .rcard { position: absolute; z-index: 6; display: none; min-width: 240px; max-width: min(460px, calc(100% - 16px)); padding: 10px 12px;
131
+ background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--shadow); font-size: 12px;
132
+ color: var(--ink); transform-origin: top left; }
133
+ .rcard.open { display: block; animation: qdbd-cardin .16s ease-out; }
134
+ @keyframes qdbd-cardin { from { opacity: 0; transform: scale(.96) translateY(-4px); } }
135
+ .rcard .rt { font-weight: 600; margin-bottom: 2px; word-break: break-word; }
136
+ .rcard .rt b { color: var(--lit); font-weight: 600; padding: 0 6px; white-space: nowrap; }
137
+ .rcard .rk { color: var(--muted); margin-bottom: 8px; }
138
+ .rcard pre { margin: 0 0 8px; padding: 7px 8px; background: var(--bg); border: 1px solid var(--line); border-radius: 7px;
139
+ font: 11px ui-monospace, Menlo, Consolas, monospace; white-space: pre-wrap; user-select: text; }
140
+ .rcard .acts a { color: var(--blue); cursor: pointer; margin-right: 12px; }
141
+ @media (prefers-reduced-motion: reduce) { .fx .flow, .fx .lbl, .rcard.open { animation: none; } }
107
142
  .t { cursor: grab; }
108
143
  #canvas.panning .t { cursor: grabbing; }
109
144
 
@@ -176,8 +211,10 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
176
211
  /* details drawer (right) */
177
212
  .details { position: absolute; top: 64px; right: 16px; bottom: 16px; width: 380px; background: var(--panel);
178
213
  border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--shadow); overflow: auto;
179
- display: none; z-index: 3; padding: 16px 18px; }
180
- .details.open { display: block; }
214
+ z-index: 3; padding: 16px 18px; visibility: hidden; opacity: 0; transform: translateX(16px); pointer-events: none;
215
+ transition: opacity .2s, transform .2s, visibility 0s .2s; }
216
+ .details.open { visibility: visible; opacity: 1; transform: none; pointer-events: auto; transition: opacity .2s, transform .2s; }
217
+ @media (prefers-reduced-motion: reduce) { .details, .details.open { transition: none; } }
181
218
  .has-details .minimap { right: 412px; }
182
219
  .details .close { position: absolute; top: 10px; right: 10px; width: 28px; height: 28px; border: 0; border-radius: 6px;
183
220
  background: transparent; display: flex; align-items: center; justify-content: center; color: var(--muted); cursor: pointer; }
@@ -16,6 +16,8 @@
16
16
  shown: {},
17
17
  regions: !!data.model.regions,
18
18
  path: null, // {to: id, steps: [...]} from the selected table
19
+ edge: null, // the clicked (locked) relationship line: "a|ac|b|bc", like the .e data attributes
20
+ hover: null, // {t: table id} or {e: line key} under the mouse: lights lines without dimming anything
19
21
  };
20
22
  var svg = document.getElementById("canvas");
21
23
  var vp = document.getElementById("vp");
@@ -29,7 +31,12 @@
29
31
 
30
32
  /* ------------------------------------------------------------- render */
31
33
  function render() {
32
- vp.innerHTML = Q.renderInner(S.vm, S.pos, S.mode, { docMarks: true, regions: S.regions });
34
+ vp.innerHTML = Q.renderInner(S.vm, S.pos, S.mode, { docMarks: true, regions: S.regions }) +
35
+ '<g class="fx"></g><g class="hit"></g>';
36
+ vp.appendChild(vp.querySelector(".tables")); // lit overlay and line hit targets sit under the tables
37
+ relByKey = {};
38
+ S.vm.rels.forEach(function (r, i) { relByKey[[r.a.t, r.a.c, r.b.t, r.b.c].join("|")] = i; });
39
+ buildHits();
33
40
  applyFocus();
34
41
  drawMinimap();
35
42
  }
@@ -37,12 +44,121 @@
37
44
  function renderEdges() {
38
45
  var g = vp.querySelector(".edges");
39
46
  if (g) g.innerHTML = Q.renderEdges(S.vm, S.pos, S.mode);
47
+ buildHits();
40
48
  applyFocus();
41
49
  }
42
50
 
51
+ /* ------------------------------------------------------------- lit relationships
52
+ * A lit line gets the --lit colour, flowing dashes and cardinality labels (drawn in .fx), and its rows are
53
+ * highlighted. Lines light up under a hovered table or line, for the selected table, and for the clicked line. */
54
+ var relByKey = {};
55
+ function edgeKey(e) {
56
+ return [e.getAttribute("data-a"), e.getAttribute("data-ac"), e.getAttribute("data-b"), e.getAttribute("data-bc")].join("|");
57
+ }
58
+ /* Wide transparent copies of every line, so a 1px curve can be hovered and clicked. */
59
+ function buildHits() {
60
+ var hit = vp.querySelector(".hit");
61
+ if (!hit) return;
62
+ hit.innerHTML = [].map.call(vp.querySelectorAll(".e"), function (e) {
63
+ return '<path data-k="' + Q.esc(edgeKey(e)) + '" d="' + e.querySelector(".ep").getAttribute("d") + '"/>';
64
+ }).join("");
65
+ }
66
+ function light() {
67
+ var fx = vp.querySelector(".fx");
68
+ if (!fx) return;
69
+ if (S.edge && relByKey[S.edge] === undefined) S.edge = null; // the clicked line was filtered away or edited out
70
+ if (!S.edge) closeCard();
71
+ vp.querySelectorAll(".lit, .t.on").forEach(function (el) { el.classList.remove("lit", "on"); });
72
+ svg.classList.toggle("efocus", !!S.edge);
73
+ var focused = svg.classList.contains("focus"), h = S.hover || {}, geo = null, parts = [];
74
+ vp.querySelectorAll(".e").forEach(function (e) {
75
+ var k = edgeKey(e), a = e.getAttribute("data-a"), b = e.getAttribute("data-b"), d = e.querySelector(".ep").getAttribute("d");
76
+ var dk = ' data-k="' + Q.esc(k) + '"';
77
+ if (e.classList.contains("ph")) { parts.push('<path class="flow amber"' + dk + ' d="' + d + '"/>'); return; }
78
+ if (!(k === S.edge || h.e === k || h.t === a || h.t === b || e.classList.contains("hl") ||
79
+ (focused && !S.path && !e.classList.contains("dim")))) return;
80
+ var r = S.vm.rels[relByKey[k]];
81
+ if (!r) return;
82
+ e.classList.add("lit");
83
+ [[r.a.t, r.a.c], [r.b.t, r.b.c]].forEach(function (end) {
84
+ var t = vp.querySelector('.t[data-id="' + cssEsc(end[0]) + '"]');
85
+ if (!t) return;
86
+ if (k === S.edge) t.classList.add("on");
87
+ var row = t.querySelector('.r[data-c="' + cssEsc(end[1]) + '"]');
88
+ if (row) row.classList.add("lit");
89
+ });
90
+ parts.push('<path class="flow"' + dk + ' d="' + d + '"/>');
91
+ (Q.edgeLabels(S.vm, S.pos, S.mode, r, geo = geo || Q.geometry(S.vm, S.mode)) || []).forEach(function (l) {
92
+ parts.push('<text class="lbl"' + dk + ' x="' + l.x + '" y="' + l.y + '" text-anchor="' + l.anchor + '">' + Q.esc(l.text) + "</text>");
93
+ });
94
+ });
95
+ fx.innerHTML = parts.join("");
96
+ }
97
+ var lightRaf = null;
98
+ function scheduleLight() {
99
+ if (!lightRaf) lightRaf = requestAnimationFrame(function () { lightRaf = null; light(); });
100
+ }
101
+ vp.addEventListener("mouseover", function (ev) {
102
+ var hp = ev.target.closest && ev.target.closest(".hit path"), t = ev.target.closest && ev.target.closest(".t");
103
+ var next = hp ? { e: hp.getAttribute("data-k") } : t ? { t: t.getAttribute("data-id") } : null;
104
+ if (JSON.stringify(next) !== JSON.stringify(S.hover)) { S.hover = next; scheduleLight(); }
105
+ });
106
+ /* A click on a line locks it: everything else dims. A second click on it, Esc or a click elsewhere clears it. */
107
+ function lockEdge(k, x, y) {
108
+ select(null);
109
+ S.edge = k;
110
+ light();
111
+ openCard(k, x, y);
112
+ }
113
+
114
+ /* The relationship card: both ends with their cardinality, a plain-English sentence and the JOIN. */
115
+ var rcard = document.getElementById("rcard"), cardInfo = null;
116
+ function openCard(k, x, y) {
117
+ var r = S.vm.rels[relByKey[k]], c = Q.relCard(S.vm, relByKey[k]);
118
+ cardInfo = { sql: c.sql, tables: [r.a.t, r.b.t] };
119
+ rcard.innerHTML = '<div class="rt">' + Q.esc(c.from) + "<b>" + Q.esc(c.cardFrom) + " &mdash; " + Q.esc(c.cardTo) + "</b>" + Q.esc(c.to) +
120
+ '</div><div class="rk">' + Q.esc(c.sentence) + "</div><pre>" + Q.esc(c.sql) +
121
+ '</pre><div class="acts"><a id="rcopy">Copy JOIN</a><a id="rboth">Show both tables</a></div>';
122
+ rcard.classList.remove("open");
123
+ void rcard.offsetWidth; // restart the open animation
124
+ rcard.classList.add("open");
125
+ var st = document.getElementById("stage").getBoundingClientRect();
126
+ rcard.style.left = Math.max(8, Math.min(x - st.left + 12, st.width - rcard.offsetWidth - 8)) + "px";
127
+ rcard.style.top = Math.max(8, Math.min(y - st.top + 12, st.height - rcard.offsetHeight - 8)) + "px";
128
+ }
129
+ function closeCard() { rcard.classList.remove("open"); }
130
+ rcard.addEventListener("click", function (ev) {
131
+ if (ev.target.id === "rcopy" && cardInfo) copyText(cardInfo.sql, "JOIN copied", rcard.querySelector("pre"));
132
+ else if (ev.target.id === "rboth" && cardInfo) {
133
+ var names = cardInfo.tables.filter(function (t, i, all) { return all.indexOf(t) === i; }).map(function (t) { return byId[t].name; });
134
+ closeCard();
135
+ applyFilter(names.map(function (n) { return n.replace(/[\s,+!]/g, "?"); }).join(" "));
136
+ }
137
+ });
138
+ document.addEventListener("pointerdown", function (ev) {
139
+ if (!rcard.contains(ev.target) && !(ev.target.closest && ev.target.closest(".hit path"))) closeCard();
140
+ });
141
+
142
+ /* The clipboard is blocked in some frames (the VS Code preview): select the text instead and say how to copy it. */
143
+ function copyText(text, done, pre) {
144
+ function fallback() {
145
+ var sel = window.getSelection(), range = document.createRange();
146
+ range.selectNodeContents(pre);
147
+ sel.removeAllRanges();
148
+ sel.addRange(range);
149
+ showToast("Press Ctrl/Cmd+C");
150
+ }
151
+ if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(text).then(function () { showToast(done); }, fallback);
152
+ else fallback();
153
+ }
154
+
155
+ // Zoomed out on a big diagram, table text is unreadable but repainting it made every zoom step take ~150 ms
156
+ // at 1000 tables, so it is hidden there. Smaller diagrams always show it.
157
+ var LOD_TABLES = 300, LOD_ZOOM = 0.3;
43
158
  function applyView() {
44
159
  var v = S.view;
45
160
  vp.setAttribute("transform", "translate(" + v.x + "," + v.y + ") scale(" + v.k + ")");
161
+ svg.classList.toggle("lod", v.k < LOD_ZOOM && S.vm.tables.length > LOD_TABLES);
46
162
  zoomval.textContent = Math.round(v.k * 100) + "%";
47
163
  drawViewportRect();
48
164
  scheduleHash();
@@ -53,7 +169,28 @@
53
169
  return [r.width || 800, r.height || 600];
54
170
  }
55
171
 
56
- function fit(only) {
172
+ /* Zoom, fit and jump glide instead of snapping. The next pointer press or wheel stops a glide. Instant with
173
+ * prefers-reduced-motion, on load and while a shared link is applied. */
174
+ var glide = null, glideTarget = null, reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
175
+ function glideTo(v, ms) {
176
+ cancelGlide();
177
+ if (!ms || reduceMotion.matches || applyingHash) { S.view = v; applyView(); return; }
178
+ var from = { x: S.view.x, y: S.view.y, k: S.view.k }, t0 = performance.now();
179
+ glideTarget = v;
180
+ (function step(now) {
181
+ var f = Math.min(1, (now - t0) / ms), e = 1 - Math.pow(1 - f, 3);
182
+ S.view = { x: from.x + (v.x - from.x) * e, y: from.y + (v.y - from.y) * e, k: from.k + (v.k - from.k) * e };
183
+ applyView();
184
+ glide = f < 1 ? requestAnimationFrame(step) : null;
185
+ if (!glide) glideTarget = null;
186
+ })(t0);
187
+ }
188
+ function cancelGlide() {
189
+ if (glide) cancelAnimationFrame(glide);
190
+ glide = glideTarget = null;
191
+ }
192
+
193
+ function fit(only, animate) {
57
194
  var b = Q.bounds(only ? Q.subset(S.vm, only) : S.vm, S.pos, S.mode), sz = stageSize(), pad = 40;
58
195
  if (S.regions && !only) b = [b[0] - 30, b[1] - 60, b[2] + 30, b[3] + 30]; // room for region borders and labels
59
196
  if (only) sz = [sz[0] - (details.classList.contains("open") ? 400 : 0), sz[1]]; // keep clear of the details panel
@@ -61,24 +198,21 @@
61
198
  var top = 70, bottom = sz[0] < 900 ? 70 : pad, availH = sz[1] - top - bottom;
62
199
  var k = Math.min((sz[0] - 2 * pad) / (b[2] - b[0]), availH / (b[3] - b[1]), 1.5);
63
200
  k = Math.max(k, 0.03); // same floor as manual zoom, so wide diagrams still fit a phone screen
64
- S.view = { k: k, x: (sz[0] - (b[2] - b[0]) * k) / 2 - b[0] * k, y: top + (availH - (b[3] - b[1]) * k) / 2 - b[1] * k };
65
- applyView();
201
+ glideTo({ k: k, x: (sz[0] - (b[2] - b[0]) * k) / 2 - b[0] * k, y: top + (availH - (b[3] - b[1]) * k) / 2 - b[1] * k }, animate ? 350 : 0);
66
202
  }
67
203
 
68
- function zoomAt(factor, cx, cy) {
69
- var v = S.view, k = Math.min(4, Math.max(0.03, v.k * factor));
70
- S.view = { k: k, x: cx - (cx - v.x) * (k / v.k), y: cy - (cy - v.y) * (k / v.k) };
71
- applyView();
204
+ function zoomAt(factor, cx, cy, ms) {
205
+ var v = glideTarget || S.view, k = Math.min(4, Math.max(0.03, v.k * factor)); // quick clicks compound
206
+ glideTo({ k: k, x: cx - (cx - v.x) * (k / v.k), y: cy - (cy - v.y) * (k / v.k) }, ms);
72
207
  }
73
208
 
74
- function centerOn(id) {
209
+ function centerOn(id, animate) {
75
210
  var t = byId[id], p = S.pos[id];
76
211
  if (!t || !p) return;
77
212
  var g = Q.geometry(S.vm, S.mode)[id], sz = stageSize();
78
213
  if (!g) return;
79
214
  var k = Math.max(S.view.k, 0.9);
80
- S.view = { k: k, x: sz[0] / 2 - (p[0] + g.w / 2) * k, y: sz[1] / 2 - (p[1] + g.h / 2) * k };
81
- applyView();
215
+ glideTo({ k: k, x: sz[0] / 2 - (p[0] + g.w / 2) * k, y: sz[1] / 2 - (p[1] + g.h / 2) * k }, animate ? 350 : 0);
82
216
  var el = vp.querySelector('.t[data-id="' + cssEsc(id) + '"]');
83
217
  if (el) {
84
218
  el.classList.add("flash");
@@ -100,6 +234,10 @@
100
234
  }
101
235
 
102
236
  function applyFocus() {
237
+ applyFocusClasses();
238
+ light();
239
+ }
240
+ function applyFocusClasses() {
103
241
  vp.querySelectorAll(".ph").forEach(function (el) { el.classList.remove("ph"); });
104
242
  if (S.path && S.focus && S.path.steps) {
105
243
  var on = {}, edges = [];
@@ -157,7 +295,7 @@
157
295
  if (t) t.classList.add("hl");
158
296
  });
159
297
  });
160
- vp.addEventListener("mouseleave", function () { clearHover(); hideTip(); });
298
+ vp.addEventListener("mouseleave", function () { clearHover(); hideTip(); S.hover = null; scheduleLight(); });
161
299
 
162
300
  /* ------------------------------------------------------------- doc-comment tooltips */
163
301
  var tip = document.getElementById("tip");
@@ -205,17 +343,21 @@
205
343
  /* ------------------------------------------------------------- pointer: pan, click, drag */
206
344
  var drag = null;
207
345
  svg.addEventListener("pointerdown", function (ev) {
346
+ cancelGlide();
208
347
  if (ev.button !== 0) return;
209
348
  var tEl = ev.target.closest && ev.target.closest(".t");
210
- drag = { sx: ev.clientX, sy: ev.clientY, moved: false, table: tEl ? tEl.getAttribute("data-id") : null, el: tEl };
349
+ var hp = !tEl && ev.target.closest && ev.target.closest(".hit path");
350
+ drag = { sx: ev.clientX, sy: ev.clientY, moved: false, table: tEl ? tEl.getAttribute("data-id") : null, el: tEl,
351
+ edge: hp ? hp.getAttribute("data-k") : null };
211
352
  if (drag.table) { // tables are draggable everywhere; only `qdbd serve` saves the new position
212
353
  drag.start = S.pos[drag.table].slice();
354
+ dragPrep(drag);
213
355
  tEl.parentNode.appendChild(tEl); // bring the grabbed table to the front
214
356
  } else {
215
357
  drag.view = { x: S.view.x, y: S.view.y };
216
358
  }
217
359
  svg.classList.add("panning");
218
- svg.setPointerCapture(ev.pointerId);
360
+ try { svg.setPointerCapture(ev.pointerId); } catch (e) { /* synthetic events (the qdbd.dev tour) have no pointer to capture */ }
219
361
  });
220
362
  var raf = null;
221
363
  svg.addEventListener("pointermove", function (ev) {
@@ -227,19 +369,20 @@
227
369
  var p = [Math.round(drag.start[0] + dx / S.view.k), Math.round(drag.start[1] + dy / S.view.k)];
228
370
  S.pos[drag.table] = p;
229
371
  drag.el.setAttribute("transform", "translate(" + p[0] + "," + p[1] + ")");
230
- if (!raf) raf = requestAnimationFrame(function () { raf = null; renderEdges(); });
372
+ if (!raf) raf = requestAnimationFrame(function () { raf = null; if (drag) dragEdges(drag); });
231
373
  } else if (drag.view) {
232
374
  S.view.x = drag.view.x + dx;
233
375
  S.view.y = drag.view.y + dy;
234
376
  applyView();
235
377
  }
236
378
  });
237
- svg.addEventListener("pointerup", function () {
379
+ svg.addEventListener("pointerup", function (ev) {
238
380
  if (!drag) return;
239
381
  svg.classList.remove("panning");
240
382
  var d = drag;
241
383
  drag = null;
242
384
  if (!d.moved) {
385
+ if (d.edge) { if (d.edge === S.edge) select(null); else lockEdge(d.edge, ev.clientX, ev.clientY); return; }
243
386
  // Clicking the selected table toggles it off, unless it was selected quietly: then show its details.
244
387
  if (d.table && d.table === S.focus && !details.classList.contains("open")) showDetails(d.table);
245
388
  else select(d.table && S.focus !== d.table ? d.table : null);
@@ -247,14 +390,45 @@
247
390
  }
248
391
  if (d.start) {
249
392
  if (S.regions) render(); // regions are sized from table positions
393
+ else renderEdges();
250
394
  drawMinimap();
251
395
  var name = byId[d.table].name, body = {};
252
396
  body[name] = S.pos[d.table];
253
397
  if (window.qdbdServe) window.qdbdServe.saveLayout(body);
254
398
  }
255
399
  });
400
+ /* While a table is dragged only its own lines are redrawn (a full redraw per frame was slow at 200+ tables). */
401
+ function dragPrep(d) {
402
+ var on = function (k) { var p = k.split("|"); return p[0] === d.table || p[2] === d.table; };
403
+ d.geo = Q.geometry(S.vm, S.mode);
404
+ d.rels = S.vm.rels.filter(function (r) { return r.a.t === d.table || r.b.t === d.table; });
405
+ d.els = {};
406
+ d.hits = {};
407
+ vp.querySelectorAll(".edges .e").forEach(function (e) { var k = edgeKey(e); if (on(k)) d.els[k] = e; });
408
+ vp.querySelectorAll(".hit path").forEach(function (h) { var k = h.getAttribute("data-k"); if (on(k)) d.hits[k] = h; });
409
+ }
410
+ function dragEdges(d) {
411
+ var tmp = document.createElementNS("http://www.w3.org/2000/svg", "g"), fx = vp.querySelector(".fx");
412
+ tmp.innerHTML = Q.renderEdges({ style: S.vm.style, tables: S.vm.tables, rels: d.rels }, S.pos, S.mode, d.geo);
413
+ [].slice.call(tmp.children).forEach(function (e) {
414
+ var k = edgeKey(e), old = d.els[k], path = e.querySelector(".ep").getAttribute("d");
415
+ if (!old) return;
416
+ e.setAttribute("class", old.getAttribute("class")); // keep dim / lit / ph
417
+ old.replaceWith(e);
418
+ d.els[k] = e;
419
+ if (d.hits[k]) d.hits[k].setAttribute("d", path);
420
+ var items = fx ? fx.querySelectorAll('[data-k="' + cssEsc(k) + '"]') : [], labels = null, li = 0;
421
+ [].forEach.call(items, function (it) {
422
+ if (it.tagName === "path") { it.setAttribute("d", path); return; }
423
+ labels = labels || Q.edgeLabels(S.vm, S.pos, S.mode, S.vm.rels[relByKey[k]], d.geo) || [];
424
+ var l = labels[li++];
425
+ if (l) { it.setAttribute("x", l.x); it.setAttribute("y", l.y); it.setAttribute("text-anchor", l.anchor); }
426
+ });
427
+ });
428
+ }
256
429
  svg.addEventListener("wheel", function (ev) {
257
430
  ev.preventDefault();
431
+ cancelGlide();
258
432
  var r = svg.getBoundingClientRect();
259
433
  var factor = Math.exp(-ev.deltaY * (ev.ctrlKey ? 0.01 : 0.0015));
260
434
  zoomAt(factor, ev.clientX - r.left, ev.clientY - r.top);
@@ -265,9 +439,9 @@
265
439
 
266
440
  /* ------------------------------------------------------------- toolbar */
267
441
  function center() { var s = stageSize(); return [s[0] / 2, s[1] / 2]; }
268
- document.getElementById("zin").onclick = function () { var c = center(); zoomAt(1.25, c[0], c[1]); };
269
- document.getElementById("zout").onclick = function () { var c = center(); zoomAt(0.8, c[0], c[1]); };
270
- document.getElementById("zfit").onclick = function () { fit(); };
442
+ document.getElementById("zin").onclick = function () { var c = center(); zoomAt(1.25, c[0], c[1], 200); };
443
+ document.getElementById("zout").onclick = function () { var c = center(); zoomAt(0.8, c[0], c[1], 200); };
444
+ document.getElementById("zfit").onclick = function () { fit(null, true); };
271
445
  document.querySelectorAll("#modes button").forEach(function (b) {
272
446
  b.onclick = function () {
273
447
  setMode(b.getAttribute("data-mode"));
@@ -321,13 +495,14 @@
321
495
  /* opts.center: pan to it. opts.quiet: don't open the details panel (only refresh it if it's already open). */
322
496
  function select(id, opts) {
323
497
  if ((id || null) !== S.focus) S.path = null;
498
+ S.edge = null; // a table (or nothing) replaces the clicked line
324
499
  S.focus = id && byId[id] ? id : null;
325
500
  scheduleHash();
326
501
  applyFocus();
327
502
  if (S.focus && (!(opts && opts.quiet) || details.classList.contains("open"))) showDetails(S.focus);
328
503
  else closeDetails();
329
504
  markTree();
330
- if (S.focus && opts && opts.center) centerOn(S.focus);
505
+ if (S.focus && opts && opts.center) centerOn(S.focus, true);
331
506
  }
332
507
 
333
508
  /* ------------------------------------------------------------- explorer: search + table tree */
@@ -461,7 +636,7 @@
461
636
  drawTree();
462
637
  if (S.focus) showDetails(S.focus);
463
638
  else closeDetails();
464
- if (!keepView) fit();
639
+ if (!keepView) fit(null, true);
465
640
  scheduleHash();
466
641
  }
467
642
  finput.addEventListener("input", function () {
@@ -586,7 +761,7 @@
586
761
  S.path = { to: target, steps: Q.findPath(S.vm, S.focus, target) };
587
762
  showDetails(S.focus);
588
763
  applyFocus();
589
- if (S.path.steps && !applyingHash) fit([S.focus].concat(S.path.steps.map(function (st) { return st.to.t; })));
764
+ if (S.path.steps && !applyingHash) fit([S.focus].concat(S.path.steps.map(function (st) { return st.to.t; })), true);
590
765
  scheduleHash();
591
766
  return true;
592
767
  }
@@ -602,9 +777,7 @@
602
777
  // Selector syntax treats spaces, commas, + and a leading ! specially; match those characters with "?".
603
778
  applyFilter(names.map(function (n) { return n.replace(/[\s,+!]/g, "?"); }).join(" "));
604
779
  } else if (ev.target.id === "pathsql" && S.path && S.path.steps) {
605
- var sql = Q.pathSql(S.vm, S.focus, S.path.steps);
606
- if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(sql).then(function () { showToast("SQL copied"); });
607
- else window.prompt("Copy SQL:", sql);
780
+ copyText(Q.pathSql(S.vm, S.focus, S.path.steps), "SQL copied", dbody.querySelector("#pathres pre"));
608
781
  }
609
782
  });
610
783
 
@@ -645,6 +818,7 @@
645
818
  r.setAttribute("height", sz[1] / v.k);
646
819
  }
647
820
  function miniPan(ev) {
821
+ cancelGlide();
648
822
  var pt = mini.createSVGPoint();
649
823
  pt.x = ev.clientX;
650
824
  pt.y = ev.clientY;
@@ -654,7 +828,11 @@
654
828
  applyView();
655
829
  }
656
830
  var miniDrag = false;
657
- mini.addEventListener("pointerdown", function (ev) { miniDrag = true; mini.setPointerCapture(ev.pointerId); miniPan(ev); });
831
+ mini.addEventListener("pointerdown", function (ev) {
832
+ miniDrag = true;
833
+ try { mini.setPointerCapture(ev.pointerId); } catch (e) { /* synthetic event */ }
834
+ miniPan(ev);
835
+ });
658
836
  mini.addEventListener("pointermove", function (ev) { if (miniDrag) miniPan(ev); });
659
837
  mini.addEventListener("pointerup", function () { miniDrag = false; });
660
838
 
@@ -806,7 +984,7 @@
806
984
  try {
807
985
  if (p.has("m")) setMode(p.get("m"));
808
986
  if (p.has("r")) setRegions(p.get("r") === "1");
809
- applyFilter(p.get("f") || "", true);
987
+ if ((p.get("f") || "") !== S.filter) applyFilter(p.get("f") || "", true); // else already rendered
810
988
  var name = p.get("t"), id = null;
811
989
  if (name) S.model.tables.forEach(function (t) { if (t.name === name) id = t.id; });
812
990
  select(id && S.shown[id] ? id : null);
quickdbd_cli/cli.py CHANGED
@@ -216,7 +216,11 @@ def cmd_export(args) -> int:
216
216
  if fmt == "svg":
217
217
  out.write_text(svg, encoding="utf-8")
218
218
  else:
219
- out.write_bytes(export.svg_to_png(svg, args.scale))
219
+ scale = export.png_scale(svg, args.scale)
220
+ if scale < args.scale:
221
+ note = f"--scale lowered to {scale:.2f} to keep the PNG within {export.MAX_PNG_SIDE} px"
222
+ print(f"note: {note}", file=sys.stderr)
223
+ out.write_bytes(export.svg_to_png(svg, scale))
220
224
  print(f"Wrote {out} ({len(only) if only else len(model['tables'])} tables)")
221
225
  return 0
222
226
 
@@ -532,7 +536,7 @@ def build_parser() -> argparse.ArgumentParser:
532
536
  action="append",
533
537
  metavar="SELECTOR",
534
538
  help="dbt-style: name glob, +name (and what it references), name+ (and what references it), "
535
- "N+name+N (hops), group:/file:/col: methods, a,b intersects (repeatable)",
539
+ "N+name+N (hops), group:/file:/col:/tag: methods, a,b intersects (repeatable)",
536
540
  )
537
541
  sp.add_argument("--exclude", action="append", metavar="SELECTOR", help="drop tables matching (repeatable)")
538
542
 
quickdbd_cli/export.py CHANGED
@@ -3,12 +3,16 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import base64
6
+ import re
6
7
  from functools import cache
7
8
  from importlib import resources
8
9
 
9
10
  from . import layout
10
11
  from .jsrt import runtime
11
12
 
13
+ # A 200-table diagram at scale 2 is 17506x15274 px: 15 s to encode, and too big for many image viewers.
14
+ MAX_PNG_SIDE = 16384
15
+
12
16
 
13
17
  def _asset_bytes(name: str) -> bytes:
14
18
  return resources.files("quickdbd_cli").joinpath(f"assets/{name}").read_bytes()
@@ -85,6 +89,17 @@ def focus_positions(model: dict, positions: dict, ids: list[str]) -> dict:
85
89
  return {k: [v[0], v[1]] for k, v in pos.items()}
86
90
 
87
91
 
92
+ def svg_size(svg: str) -> tuple[float, float]:
93
+ w = re.search(r'\bwidth="([\d.]+)', svg)
94
+ h = re.search(r'\bheight="([\d.]+)', svg)
95
+ return (float(w.group(1)) if w else 1000.0, float(h.group(1)) if h else 1000.0)
96
+
97
+
98
+ def png_scale(svg: str, scale: float) -> float:
99
+ """``scale``, lowered so the PNG's longest side stays within MAX_PNG_SIDE pixels."""
100
+ return min(scale, MAX_PNG_SIDE / max(svg_size(svg)))
101
+
102
+
88
103
  def svg_to_png(svg: str, scale: float = 2.0) -> bytes:
89
104
  import resvg_py
90
105
 
quickdbd_cli/html.py CHANGED
@@ -90,6 +90,7 @@ BODY = """
90
90
  <svg id="mini" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet"></svg>
91
91
  </div>
92
92
  <div class="tip" id="tip" role="tooltip"></div>
93
+ <div class="rcard" id="rcard" role="dialog" aria-label="Relationship"></div>
93
94
  <div class="toast" id="toast" role="status"></div>
94
95
  <div class="diffbar" id="diffbar"></div>
95
96
  <div class="hint">{hint}</div>
quickdbd_cli/layout.py CHANGED
@@ -32,6 +32,10 @@ OVERLAP_OPTIONS = {
32
32
  "elk.spacing.nodeNode": "40",
33
33
  "elk.randomSeed": "1",
34
34
  }
35
+ # Overlap removal takes most of a big layout (25 of 37 s at 1000 tables). Above BIG_LAYOUT tables its
36
+ # iterations are capped, and the few tables it leaves overlapping are moved by `_separate`.
37
+ BIG_LAYOUT = 300
38
+ BIG_OVERLAP_OPTIONS = {**OVERLAP_OPTIONS, "org.eclipse.elk.overlapRemoval.maxIterations": "8"}
35
39
 
36
40
  MARGIN = 30
37
41
  Positions = dict[str, tuple[int, int]]
@@ -64,9 +68,10 @@ def elk_layout(tables: list[dict], rels: list[dict], timeout_sec: float | None =
64
68
  }
65
69
  first = rt.call_async("qdbdElkLayout", graph, timeout_sec=timeout_sec)
66
70
  first = _landscape(tables, {k: (v[0], v[1]) for k, v in first.items()})
71
+ big = len(tables) > BIG_LAYOUT
67
72
  graph = {
68
73
  "id": "root",
69
- "layoutOptions": OVERLAP_OPTIONS,
74
+ "layoutOptions": BIG_OVERLAP_OPTIONS if big else OVERLAP_OPTIONS,
70
75
  "children": [
71
76
  {"id": t["id"], "width": t["w"], "height": t["h"], "x": first[t["id"]][0], "y": first[t["id"]][1]}
72
77
  for t in tables
@@ -74,6 +79,8 @@ def elk_layout(tables: list[dict], rels: list[dict], timeout_sec: float | None =
74
79
  "edges": [],
75
80
  }
76
81
  raw = rt.call_async("qdbdElkLayout", graph, timeout_sec=timeout_sec)
82
+ if big:
83
+ raw = _separate(tables, raw)
77
84
  min_x = min(v[0] for v in raw.values())
78
85
  min_y = min(v[1] for v in raw.values())
79
86
  return {k: (int(v[0] - min_x), int(v[1] - min_y)) for k, v in raw.items()}
@@ -155,6 +162,18 @@ def _free_spot(cx: float, cy: float, w: float, h: float, rects, step: int = 30)
155
162
  return int(round(x0)), int(round(y0))
156
163
 
157
164
 
165
+ def _separate(tables: list[dict], pos: dict) -> dict:
166
+ """Move each table that overlaps an earlier one to the nearest free spot."""
167
+ out, rects = {}, []
168
+ for t in tables:
169
+ x, y = pos[t["id"]]
170
+ if _overlaps(x, y, t["w"], t["h"], rects):
171
+ x, y = _free_spot(x + t["w"] / 2, y + t["h"] / 2, t["w"], t["h"], rects)
172
+ out[t["id"]] = (x, y)
173
+ rects.append((x, y, t["w"], t["h"]))
174
+ return out
175
+
176
+
158
177
  def place_missing(tables: list[dict], rels: list[dict], positions: Positions) -> Positions:
159
178
  """Positions for every table. Existing positions never move.
160
179
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: quickdbd-cli
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Compile QuickDBD-syntax schema files into interactive HTML ER diagrams, with no table limit.
5
5
  Project-URL: Homepage, https://qdbd.dev/
6
6
  Project-URL: Documentation, https://qdbd.dev/guide/
@@ -43,7 +43,7 @@ Description-Content-Type: text/markdown
43
43
  </picture>
44
44
  </p>
45
45
 
46
- <p align="center"><b>Interactive ER diagrams from QuickDBD text or a dbt project, readable at 200+ tables</b></p>
46
+ <p align="center"><b>Interactive ER diagrams from QuickDBD text, DBML or a dbt project, readable at 200+ tables</b></p>
47
47
 
48
48
  <p align="center">
49
49
  <a href="https://pypi.org/project/quickdbd-cli/"><img src="https://img.shields.io/pypi/v/quickdbd-cli?label=PyPI" alt="PyPI version"></a>
@@ -89,7 +89,7 @@ qdbd is not affiliated with QuickDBD or dbdiagram.io. It reads the same text syn
89
89
  - **QuickDBD-compatible**: the parser mirrors QuickDBD's own, error messages included. A file that passes `qdbd check --strict` pastes into QuickDBD unchanged.
90
90
  - **dbt import**: `qdbd import dbt` reads `manifest.json`. Primary keys come from `unique` + `not_null` tests or constraints, relationships from `relationships` tests, and `--infer-fks` fills the gaps from column names.
91
91
  - **Readable at 200+ tables**: automatic layout, search, dbt-style selectors (`+orders+1`, `group:marts`, `tag:fact`), shaded regions per file, collapsed views and a minimap.
92
- - **Review changes in pull requests**: `qdbd diff` reports what a branch changed, the GitHub Action posts it as a PR comment, and `--diff-base` colours the changes in the diagram.
92
+ - **Review changes in pull requests**: `qdbd diff` reports what a branch changed, the GitHub Action posts it as a PR comment, and `--diff-base` colours the changes in the diagram. To propose a column change, tag the column `# @tag: added` or `# @tag: removed` and it's drawn green or red ([column tags](https://qdbd.dev/tags/#columns)).
93
93
  - **VS Code extension**: highlighting, errors as you type, outline, F12 across files, and the diagram beside the editor, following your cursor, or in your browser on localhost. Export, share, compare with git, convert to and from DBML, and dbt import from the sidebar or the Command Palette, with no terminal: it installs the CLI itself. Get it from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=MarkPhamm.quickdbd) or, for Cursor, Windsurf and VSCodium, [Open VSX](https://open-vsx.org/extension/MarkPhamm/quickdbd) ([docs](https://qdbd.dev/vscode/)).
94
94
  - **AI agents**: `qdbd mcp` gives Claude, Copilot and Cursor your schema as tools: tables and columns, join paths as SQL (composite keys included), checks for their edits and diffs against `main` ([docs](https://qdbd.dev/mcp/)).
95
95
  - **One schema, many outputs**: an HTML page, PNG, SVG, editable draw.io, or a shareable link. Installs with pip: no Node, Graphviz or browser needed.
@@ -186,7 +186,7 @@ jobs:
186
186
  permissions: { contents: read, pull-requests: write }
187
187
  steps:
188
188
  - uses: actions/checkout@v4
189
- - uses: MarkPhamm/quickdbd-cli@v0.1.11
189
+ - uses: MarkPhamm/quickdbd-cli@v0.3.4
190
190
  with:
191
191
  path: erd/ # schema file(s) or folder(s)
192
192
  output: site/index.html
@@ -198,7 +198,7 @@ dbt projects, GitHub Pages, pre-commit and GitLab CI are covered in [CI and Auto
198
198
 
199
199
  ```text
200
200
  dbt project ──qdbd import dbt──┐
201
- ├──▶ schema/*.txt (QuickDBD syntax, in git)
201
+ ├──▶ schema/*.txt, *.dbml (QuickDBD syntax or DBML, in git)
202
202
  hand-written / pasted ─────────┘ │
203
203
  ├──▶ qdbd check validation, QuickDBD's error messages ──▶ CI gate
204
204
  ├──▶ qdbd serve live editor, drag to arrange ──▶ schema.layout.json
@@ -240,7 +240,7 @@ qdbd build schema/ -o erd.html
240
240
 
241
241
  | | qdbd | QuickDBD |
242
242
  |---|---|---|
243
- | Text syntax | QuickDBD's | QuickDBD's |
243
+ | Text syntax | QuickDBD's or DBML | QuickDBD's |
244
244
  | Tables per diagram | no limit | 10 on the free plan |
245
245
  | Where it runs | your terminal and CI; the result is a static HTML file | the QuickDBD web app |
246
246
  | Where the schema lives | `.txt` / `.dbml` files in your git repo | the web app |
@@ -1,15 +1,15 @@
1
- quickdbd_cli/__init__.py,sha256=vNiWJ14r_cw5t_7UDqDQIVZvladKFGyHH2avsLpN7Vg,22
1
+ quickdbd_cli/__init__.py,sha256=oYLGMpySamd16KLiaBTfRyrAS7_oyp-TOEHmzmeumwg,22
2
2
  quickdbd_cli/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
3
- quickdbd_cli/agent.py,sha256=pZzKc9DMqN6pX1ZzXgcb11ZqX91wuJZW1f6xO2uGc2M,37421
4
- quickdbd_cli/cli.py,sha256=GJnOpXVwEpJHUQkagGrMc27x5vt1N6Ybj1-tX-TLPzE,29288
3
+ quickdbd_cli/agent.py,sha256=LEuReTkwWDlRLs-GfQQMpMosHDFWbin0EAXRo73j-dA,37202
4
+ quickdbd_cli/cli.py,sha256=h9pTnGnprYOhyuEpjdd32QyAqR-skJRTBvoipIyCZS0,29522
5
5
  quickdbd_cli/convert.py,sha256=1AYFm4YmEav_wZp7xCAcL7TsD3qC17Y1I5g6V_fyAU0,16682
6
6
  quickdbd_cli/dbml.py,sha256=tHJOtMm1PHx4-yqs6rncPqdDfbwPcW8F54j_kKlWkx0,59413
7
7
  quickdbd_cli/dbt.py,sha256=AMHzERmc8WfL1tBLX4bJdPVghIHrqrfx8nMDbZMrueY,23100
8
8
  quickdbd_cli/diff.py,sha256=UBiCE3dk_cN-xSRZFQoLiIIFh_FAVZUpLwsdtHG9eBo,9986
9
- quickdbd_cli/export.py,sha256=pjWe3oloaxVkV2b96GllQR4faB9OMtbNuTscxwjQhsk,2983
10
- quickdbd_cli/html.py,sha256=fiMsBPkBiCpxz-8x4_SFaeejdnHCAAK0IVlrimbpS5I,7950
9
+ quickdbd_cli/export.py,sha256=XbSvWqFmhQHPcBtPHPit1SmhJ6qXrPyGlHYt7FpIMyo,3539
10
+ quickdbd_cli/html.py,sha256=pn4Jlcce5nniH7NtGoZjzrTH_dTw4qIhio4bWXGupVw,8031
11
11
  quickdbd_cli/jsrt.py,sha256=_sRkmUbgHDTwK272jeTNUwojfjUKAkv4UeD2E2u3H1A,3464
12
- quickdbd_cli/layout.py,sha256=ajzJCJQ8ofnYj8dhz8aWq-LV2tmW5BHwJQ2LRoeSWaM,9998
12
+ quickdbd_cli/layout.py,sha256=2wrryJ-SSJbMlOdxmtCTzC2_zkTtvRVs-OuOBS1bGEQ,10856
13
13
  quickdbd_cli/mcp.py,sha256=ibUg1a50BBco-hZABdxTexGZi_gclFCdcBDalTvFar8,12350
14
14
  quickdbd_cli/measure.py,sha256=Nyk47FiJokPvTStKz23mgXkO0HnXKWOts90kp1gCjng,5969
15
15
  quickdbd_cli/model.py,sha256=lQcj89gDg2y4v_xhJX0_PWoWKGx0rg2emREVL_LA6uQ,3352
@@ -28,15 +28,15 @@ quickdbd_cli/assets/editor.js,sha256=Gare_MeAOKlc2QZpTR9nD8Ty-U6JBY3HRWAeMnDkCsU
28
28
  quickdbd_cli/assets/elk-LICENSE.md,sha256=Y36B9KG2tAeVNcSZ_KBeI4z3YFwf9bdtYNfafOlnAMk,14443
29
29
  quickdbd_cli/assets/elk.bundled.js,sha256=EiLkT5U853Rq8jgB5yNwj45vQ2uLN3pqX8dVLzSjB7M,1609707
30
30
  quickdbd_cli/assets/font-metrics.json,sha256=sXSiElxQSK8md8IEePLwh16TTdAqKt9rV140pvMlVS8,8654
31
- quickdbd_cli/assets/render.js,sha256=3Jv7On2X9MghnP-GInfkkfsBFgMC1vg6VkpYfOzgjwM,28944
32
- quickdbd_cli/assets/style.css,sha256=HDF9tWXoQ0DRhSCiSmMDaXysK0Wd0VWW4_iwKgQdLFg,16185
31
+ quickdbd_cli/assets/render.js,sha256=WzxX_sUgdz0O4GgiaupKl8J_LI4YCqF9PUxzRbwebwo,31891
32
+ quickdbd_cli/assets/style.css,sha256=ttyGKLti1sU-NMp9CBTDgaznfeU14XjWquFJr9XGsaA,18877
33
33
  quickdbd_cli/assets/syntax-reference.md,sha256=WERcsWjyrfeHQPgxQriWKBsqFJnmWTQ2oRZjKYAHhKc,5322
34
- quickdbd_cli/assets/viewer.js,sha256=T7AD6qQ3GmHN4iL8MX19GZ_kQO46hfPsJp7AsMYyRYU,39683
35
- quickdbd_cli-0.3.2.dist-info/METADATA,sha256=3Qc1utZqKyjVciyy0ZvUH-0ADh_OlH817rA9ZK9TbZw,17009
36
- quickdbd_cli-0.3.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
37
- quickdbd_cli-0.3.2.dist-info/entry_points.txt,sha256=nRhiYYug-XOY95nWUmi4oanico3Zjkfqk2OHtIE6SQ0,47
38
- quickdbd_cli-0.3.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
39
- quickdbd_cli-0.3.2.dist-info/licenses/NOTICE,sha256=pyv3BfIYiGzKLU4w-GcBe7GXoXxHqZ97Mf6Zpx_3_i0,2067
40
- quickdbd_cli-0.3.2.dist-info/licenses/src/quickdbd_cli/assets/Inter-LICENSE.txt,sha256=JiSB6ERSGzJvXs0FPlm5jIstp4yO4b27boF0MF5Uk1o,4380
41
- quickdbd_cli-0.3.2.dist-info/licenses/src/quickdbd_cli/assets/elk-LICENSE.md,sha256=Y36B9KG2tAeVNcSZ_KBeI4z3YFwf9bdtYNfafOlnAMk,14443
42
- quickdbd_cli-0.3.2.dist-info/RECORD,,
34
+ quickdbd_cli/assets/viewer.js,sha256=3rKXI80TNv9jF1dJNQa3Gpy0042yTcGfTz_8qg4h5WU,49676
35
+ quickdbd_cli-0.3.4.dist-info/METADATA,sha256=ubcoaxo_WRsI_-5TAdjNoR1G9kWcfsRTT_ibmIXdAzA,17195
36
+ quickdbd_cli-0.3.4.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
37
+ quickdbd_cli-0.3.4.dist-info/entry_points.txt,sha256=nRhiYYug-XOY95nWUmi4oanico3Zjkfqk2OHtIE6SQ0,47
38
+ quickdbd_cli-0.3.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
39
+ quickdbd_cli-0.3.4.dist-info/licenses/NOTICE,sha256=pyv3BfIYiGzKLU4w-GcBe7GXoXxHqZ97Mf6Zpx_3_i0,2067
40
+ quickdbd_cli-0.3.4.dist-info/licenses/src/quickdbd_cli/assets/Inter-LICENSE.txt,sha256=JiSB6ERSGzJvXs0FPlm5jIstp4yO4b27boF0MF5Uk1o,4380
41
+ quickdbd_cli-0.3.4.dist-info/licenses/src/quickdbd_cli/assets/elk-LICENSE.md,sha256=Y36B9KG2tAeVNcSZ_KBeI4z3YFwf9bdtYNfafOlnAMk,14443
42
+ quickdbd_cli-0.3.4.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.4
2
+ Generator: hatchling 1.33.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any