Spaces:
Running on Zero
Running on Zero
Add schema visualizer (table cards + PK/FK badges + relationships); guard prefetch to Space only
Browse files
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
|
| 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 |
-
#
|
| 265 |
-
uploaded_db.change
|
| 266 |
-
|
|
|
|
| 267 |
|
| 268 |
|
| 269 |
-
# Run at import so the Space downloads weights during startup, not on first query.
|
| 270 |
ensure_sample_db()
|
| 271 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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→" + 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"]))} → '
|
| 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()
|