Ashsinha1 commited on
Commit
fd3cae3
·
verified ·
1 Parent(s): 0ed7ab9

Add schemagate demo app

Browse files
Files changed (3) hide show
  1. README.md +33 -5
  2. app.py +106 -0
  3. requirements.txt +1 -0
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
- title: Schemagate
3
- emoji: 🐨
4
- colorFrom: yellow
5
- colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.26.0
8
  python_version: '3.12'
@@ -10,6 +10,34 @@ app_file: app.py
10
  pinned: false
11
  license: apache-2.0
12
  short_description: Identity-scoped schema selection for text-to-SQL
 
 
 
 
 
 
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: schemagate
3
+ emoji: 🔒
4
+ colorFrom: green
5
+ colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.26.0
8
  python_version: '3.12'
 
10
  pinned: false
11
  license: apache-2.0
12
  short_description: Identity-scoped schema selection for text-to-SQL
13
+ tags:
14
+ - text-to-sql
15
+ - nl2sql
16
+ - rag
17
+ - sql
18
+ - security
19
  ---
20
 
21
+ # schemagate
22
+
23
+ Picks the handful of tables a text-to-SQL model needs, and filters them by who is
24
+ asking — **before** the model sees anything.
25
+
26
+ Schema selection runs before the query runs, so it runs before row-level security
27
+ can act. A selector that is not identity-aware hands the model a table the caller
28
+ cannot read: the SQL is correct, RLS filters every row, and the user is told
29
+ *no records found*. A wrong answer in a confident tone, not an access-denied message.
30
+
31
+ This Space runs the real selector over a bundled 42-object demo schema.
32
+ No database, no API key, no model call — BM25 plus a hashed embedder, offline.
33
+ `hr_compensation` is restricted to the `payroll` role: change the roles box and
34
+ watch it appear and disappear from the prompt.
35
+
36
+ ```
37
+ pip install schemagate
38
+ schemagate demo "which customers owe us money"
39
+ ```
40
+
41
+ - Source: https://github.com/ashishsinha1602/schemagate
42
+ - PyPI: https://pypi.org/project/schemagate/
43
+ - Apache-2.0. Certified live on Oracle Autonomous Database 26ai and PostgreSQL 16.
app.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """schemagate on Hugging Face Spaces.
2
+
3
+ Two questions this page answers, without a database and without a model:
4
+ 1. What does the selector actually send to the LLM?
5
+ 2. What happens to a restricted table when the caller lacks the role?
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import gradio as gr
10
+
11
+ from schemagate import Catalog, Principal
12
+ from schemagate.demo_schema import HINTS, create_demo_db
13
+
14
+ CAT = Catalog().bootstrap(create_demo_db())
15
+ for _table, _text in HINTS.items():
16
+ CAT.hint(_table, _text)
17
+ CAT.restrict("hr_compensation", ["payroll"])
18
+
19
+ FULL_DDL = "\n\n".join(d.render_ddl() for d in CAT.objects())
20
+ FULL_TOKENS = max(1, len(FULL_DDL) // 4) # ~4 chars per token
21
+ OBJECTS = len(CAT)
22
+
23
+ EXAMPLES = [
24
+ ["which customers owe us money", "demo:analyst", ""],
25
+ ["salary and pay grade by employee", "okta:jdoe", ""],
26
+ ["salary and pay grade by employee", "okta:hr", "payroll"],
27
+ ["things we are running out of", "demo:analyst", ""],
28
+ ["total revenue by month last year", "demo:analyst", ""],
29
+ ]
30
+
31
+
32
+ def run(question: str, subject: str, roles: str, top_k: int):
33
+ question = (question or "").strip()
34
+ if not question:
35
+ return "Ask something.", "", ""
36
+
37
+ role_set = {r.strip() for r in (roles or "").split(",") if r.strip()}
38
+ principal = Principal(subject or "demo:analyst", roles=role_set)
39
+ sel = CAT.select(question, top_k=int(top_k), principal=principal)
40
+
41
+ fragment = sel.prompt_fragment()
42
+ tokens = max(1, len(fragment) // 4)
43
+ cut = 100 - round(tokens / FULL_TOKENS * 100)
44
+
45
+ picked = "\n".join(f"- `{n}`" for n in sel.table_names) or "_nothing matched_"
46
+ restricted = any(n.split(".")[-1] == "hr_compensation" for n in sel.table_names)
47
+ note = (
48
+ "`hr_compensation` **is** in the selection — this caller holds `payroll`."
49
+ if restricted
50
+ else "`hr_compensation` is **absent**: not ranked low, not present. Its name "
51
+ "does not appear anywhere in the text below."
52
+ )
53
+
54
+ summary = (
55
+ f"**{len(sel.table_names)} of {OBJECTS} objects** for "
56
+ f"`{principal.subject}` roles={sorted(role_set) or '—'}\n\n"
57
+ f"{picked}\n\n{note}\n\n"
58
+ f"**{tokens} tokens** in the prompt fragment, against **{FULL_TOKENS}** "
59
+ f"for the whole schema — a {cut}% cut. The selector never called a model "
60
+ f"to decide this."
61
+ )
62
+ return summary, fragment, sel.explain()
63
+
64
+
65
+ with gr.Blocks(title="schemagate", theme=gr.themes.Soft()) as demo:
66
+ gr.Markdown(
67
+ "# schemagate\n"
68
+ "### Identity-scoped schema selection for text-to-SQL\n\n"
69
+ "Schema selection happens **before** the query runs — so it happens before "
70
+ "row-level security can act. A selector that is not identity-aware hands the "
71
+ "model a table the caller cannot read; the SQL is correct, RLS filters every "
72
+ "row, and the user is told *no records found*. That is a wrong answer in a "
73
+ "confident tone, not an access-denied message.\n\n"
74
+ f"This page runs the real selector over a bundled {OBJECTS}-object schema. "
75
+ "No database, no API key, no model call — BM25 plus a hashed embedder, offline. "
76
+ "`hr_compensation` is restricted to the `payroll` role: change the roles box "
77
+ "and watch it appear and disappear."
78
+ )
79
+ with gr.Row():
80
+ with gr.Column(scale=3):
81
+ question = gr.Textbox(label="Question", value="salary and pay grade by employee")
82
+ with gr.Column(scale=2):
83
+ subject = gr.Textbox(label="Caller", value="okta:jdoe")
84
+ roles = gr.Textbox(label="Roles (comma-separated)", placeholder="payroll")
85
+ top_k = gr.Slider(1, 12, value=6, step=1, label="Tables to select")
86
+ go = gr.Button("Select", variant="primary")
87
+
88
+ out = gr.Markdown()
89
+ with gr.Accordion("Prompt fragment — exactly what the model would receive", open=True):
90
+ fragment = gr.Code(label="", language="sql")
91
+ with gr.Accordion("Why these tables", open=False):
92
+ explain = gr.Code(label="")
93
+
94
+ gr.Examples(EXAMPLES, inputs=[question, subject, roles])
95
+ gr.Markdown(
96
+ "`pip install schemagate` · "
97
+ "[GitHub](https://github.com/ashishsinha1602/schemagate) · "
98
+ "[PyPI](https://pypi.org/project/schemagate/) · Apache-2.0\n\n"
99
+ "Certified live on Oracle Autonomous Database 26ai and PostgreSQL 16."
100
+ )
101
+
102
+ go.click(run, [question, subject, roles, top_k], [out, fragment, explain])
103
+ question.submit(run, [question, subject, roles, top_k], [out, fragment, explain])
104
+
105
+ if __name__ == "__main__":
106
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ schemagate>=0.1.8