jk200201 commited on
Commit
28459d0
·
verified ·
1 Parent(s): bd643c9

Add schema visualizer (table cards + PK/FK badges + relationships); guard prefetch to Space only

Browse files
Files changed (1) hide show
  1. app.py +96 -7
app.py CHANGED
@@ -8,6 +8,7 @@ read-only against the chosen database.
8
  """
9
  from __future__ import annotations
10
 
 
11
  import sqlite3
12
  import traceback
13
  import tempfile
@@ -131,7 +132,7 @@ def resolve_db(uploaded_db: str | None) -> tuple[str | None, str | None]:
131
 
132
 
133
  def show_schema(uploaded_db: str | None) -> str:
134
- """Preview the schema of whichever database is currently selected."""
135
  db_path, err = resolve_db(uploaded_db)
136
  if err:
137
  return f"-- {err} --"
@@ -141,6 +142,87 @@ def show_schema(uploaded_db: str | None) -> str:
141
  return f"-- could not read schema: {exc} --"
142
 
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  def prefetch_weights() -> None:
145
  """Download weights to disk at startup (CPU, no GPU timer).
146
 
@@ -218,6 +300,9 @@ with gr.Blocks(title="LocalSQL") as demo:
218
  gr.Markdown(INTRO)
219
  gr.Markdown(NOTE)
220
 
 
 
 
221
  with gr.Row():
222
  with gr.Column(scale=1):
223
  uploaded_db = gr.File(
@@ -253,7 +338,7 @@ with gr.Blocks(title="LocalSQL") as demo:
253
  status = gr.Textbox(label="Status")
254
  with gr.Accordion("Model reasoning (chain of thought)", open=False):
255
  reasoning = gr.Markdown()
256
- with gr.Accordion("Schema", open=False):
257
  schema = gr.Code(language="sql")
258
 
259
  submit.click(
@@ -261,14 +346,18 @@ with gr.Blocks(title="LocalSQL") as demo:
261
  inputs=[question, evidence, run_sql, uploaded_db],
262
  outputs=[sql, rows, status, schema, reasoning],
263
  )
264
- # Preview the schema of whichever database is selected.
265
- uploaded_db.change(show_schema, inputs=[uploaded_db], outputs=[schema])
266
- demo.load(show_schema, inputs=[uploaded_db], outputs=[schema])
 
267
 
268
 
269
- # Run at import so the Space downloads weights during startup, not on first query.
270
  ensure_sample_db()
271
- prefetch_weights()
 
 
 
 
272
 
273
  if __name__ == "__main__":
274
  demo.launch()
 
8
  """
9
  from __future__ import annotations
10
 
11
+ import html
12
  import sqlite3
13
  import traceback
14
  import tempfile
 
132
 
133
 
134
  def show_schema(uploaded_db: str | None) -> str:
135
+ """Preview the raw DDL of whichever database is currently selected."""
136
  db_path, err = resolve_db(uploaded_db)
137
  if err:
138
  return f"-- {err} --"
 
142
  return f"-- could not read schema: {exc} --"
143
 
144
 
145
+ def _badge(text: str, color: str) -> str:
146
+ return (
147
+ f'<span style="background:{color}22;color:{color};border-radius:4px;'
148
+ f'padding:0 5px;font-size:11px;margin-left:6px">{text}</span>'
149
+ )
150
+
151
+
152
+ def schema_map_html(uploaded_db: str | None) -> str:
153
+ """Render the selected database as a grid of table cards (columns + PK/FK
154
+ badges) plus a relationships line, so users can see what they can ask about.
155
+ """
156
+ db_path, err = resolve_db(uploaded_db)
157
+ if err:
158
+ return f"<p>{html.escape(err)}</p>"
159
+ try:
160
+ con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
161
+ con.row_factory = sqlite3.Row
162
+ tables = [
163
+ r[0] for r in con.execute(
164
+ "SELECT name FROM sqlite_master WHERE type='table' "
165
+ "AND name NOT LIKE 'sqlite_%' ORDER BY name"
166
+ )
167
+ ]
168
+ if not tables:
169
+ con.close()
170
+ return "<p>No tables found in this database.</p>"
171
+
172
+ cards, rels = [], []
173
+ for t in tables:
174
+ cols = con.execute(f'PRAGMA table_info("{t}")').fetchall()
175
+ fks = con.execute(f'PRAGMA foreign_key_list("{t}")').fetchall()
176
+ try:
177
+ n = con.execute(f'SELECT COUNT(*) FROM "{t}"').fetchone()[0]
178
+ except Exception: # noqa: BLE001
179
+ n = "?"
180
+ fk_from = {f["from"]: (f["table"], f["to"]) for f in fks}
181
+
182
+ col_rows = []
183
+ for c in cols:
184
+ name = html.escape(str(c["name"]))
185
+ ctype = html.escape(str(c["type"] or ""))
186
+ if c["pk"]:
187
+ badge = _badge("PK", "#3b82f6")
188
+ elif c["name"] in fk_from:
189
+ badge = _badge("FK&rarr;" + html.escape(fk_from[c["name"]][0]), "#10b981")
190
+ else:
191
+ badge = ""
192
+ col_rows.append(
193
+ '<div style="display:flex;justify-content:space-between;gap:10px;padding:1px 0">'
194
+ f'<span>{name}{badge}</span>'
195
+ f'<span style="opacity:.5;font-size:11px">{ctype}</span></div>'
196
+ )
197
+ for f in fks:
198
+ rels.append(
199
+ f'{html.escape(t)}.{html.escape(str(f["from"]))} &rarr; '
200
+ f'{html.escape(str(f["table"]))}.{html.escape(str(f["to"]))}'
201
+ )
202
+
203
+ cards.append(
204
+ '<div style="border:1px solid rgba(128,128,128,.35);border-radius:10px;'
205
+ 'padding:10px 12px;min-width:190px;flex:0 0 auto">'
206
+ f'<div style="font-weight:600;border-bottom:1px solid rgba(128,128,128,.25);'
207
+ f'padding-bottom:5px;margin-bottom:5px">{html.escape(t)} '
208
+ f'<span style="opacity:.5;font-weight:400;font-size:12px">{n} rows</span></div>'
209
+ + "".join(col_rows) + "</div>"
210
+ )
211
+ con.close()
212
+
213
+ grid = (
214
+ '<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:flex-start">'
215
+ + "".join(cards) + "</div>"
216
+ )
217
+ rel_html = (
218
+ '<div style="margin-top:10px;opacity:.8;font-size:13px"><b>Relationships:</b> '
219
+ + "; ".join(rels) + "</div>"
220
+ ) if rels else ""
221
+ return grid + rel_html
222
+ except Exception as exc: # noqa: BLE001
223
+ return f"<p>Could not read database: {html.escape(str(exc))}</p>"
224
+
225
+
226
  def prefetch_weights() -> None:
227
  """Download weights to disk at startup (CPU, no GPU timer).
228
 
 
300
  gr.Markdown(INTRO)
301
  gr.Markdown(NOTE)
302
 
303
+ with gr.Accordion("What's in this database? (tables, columns, keys)", open=True):
304
+ schema_map = gr.HTML()
305
+
306
  with gr.Row():
307
  with gr.Column(scale=1):
308
  uploaded_db = gr.File(
 
338
  status = gr.Textbox(label="Status")
339
  with gr.Accordion("Model reasoning (chain of thought)", open=False):
340
  reasoning = gr.Markdown()
341
+ with gr.Accordion("Schema (raw SQL)", open=False):
342
  schema = gr.Code(language="sql")
343
 
344
  submit.click(
 
346
  inputs=[question, evidence, run_sql, uploaded_db],
347
  outputs=[sql, rows, status, schema, reasoning],
348
  )
349
+ # Refresh the schema map + raw DDL for whichever database is selected.
350
+ for trigger in (uploaded_db.change, demo.load):
351
+ trigger(schema_map_html, inputs=[uploaded_db], outputs=[schema_map])
352
+ trigger(show_schema, inputs=[uploaded_db], outputs=[schema])
353
 
354
 
 
355
  ensure_sample_db()
356
+ # On HF Spaces (SPACE_ID is set) prefetch weights at startup so the 15 GB pull is
357
+ # outside the GPU window. Skipped locally so importing the module stays cheap.
358
+ import os # noqa: E402
359
+ if os.getenv("SPACE_ID"):
360
+ prefetch_weights()
361
 
362
  if __name__ == "__main__":
363
  demo.launch()