Spaces:
Sleeping
Sleeping
File size: 16,803 Bytes
40eb9bf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 | #!/usr/bin/env python3
"""
pg_plan_cache Agent β Hugging Face Spaces (Gradio UI)
Provides a web interface for managing and exploring the pg_plan_cache
PostgreSQL extension. Works in two modes:
Demo mode β query normalization, hashing, architecture docs (no backend needed)
Live mode β full cache stats, Redis inspection, diagnostics (needs PG + Redis)
"""
import os
import gradio as gr
from normalizer import normalize_query, compute_query_hash
# ---------------------------------------------------------------------------
# Optional live DB connection (only when credentials are provided)
# ---------------------------------------------------------------------------
db = None
def _connect_db():
"""Try to connect using env vars / HF Secrets. Returns DatabaseManager or None."""
global db
if db is not None:
return db
pg_host = os.getenv("PG_HOST")
redis_host = os.getenv("REDIS_HOST")
if not pg_host and not redis_host:
return None
try:
from config import load_config
from db import DatabaseManager
config = load_config()
db = DatabaseManager(config)
db.pg_query("SELECT 1")
return db
except Exception:
return None
def _is_live():
return _connect_db() is not None
# ---------------------------------------------------------------------------
# Tab 1: Query Normalizer & Hasher (always works β no backend needed)
# ---------------------------------------------------------------------------
def normalize_tab(query: str):
"""Normalize a SQL query and compute its SHA-256 cache key."""
if not query.strip():
return "", "", ""
normalized = normalize_query(query)
qhash = compute_query_hash(normalized)
# Check cache status if live
status = "N/A (demo mode β no Redis connected)"
conn = _connect_db()
if conn:
try:
cached = conn.redis_get(f"plan:{qhash}")
status = "CACHED" if cached else "NOT CACHED"
except Exception as e:
status = f"Error: {e}"
return normalized, qhash, status
# ---------------------------------------------------------------------------
# Tab 2: Cache Statistics (needs live PG)
# ---------------------------------------------------------------------------
def stats_tab():
conn = _connect_db()
if not conn:
return "Not connected to PostgreSQL.\nSet PG_HOST, PG_USER, etc. in Space Secrets to enable."
try:
import tools
return tools.get_cache_stats(conn)
except Exception as e:
return f"Error: {e}"
# ---------------------------------------------------------------------------
# Tab 3: Cache Efficiency Analysis (needs live PG + Redis)
# ---------------------------------------------------------------------------
def analyze_tab():
conn = _connect_db()
if not conn:
return "Not connected.\nSet PG and Redis credentials in Space Secrets to enable."
try:
import tools
return tools.analyze_cache_efficiency(conn)
except Exception as e:
return f"Error: {e}"
# ---------------------------------------------------------------------------
# Tab 4: Full Diagnostics (needs live PG + Redis)
# ---------------------------------------------------------------------------
def diagnose_tab():
conn = _connect_db()
if not conn:
return "Not connected.\nSet PG and Redis credentials in Space Secrets to enable."
try:
import tools
return tools.diagnose(conn)
except Exception as e:
return f"Error: {e}"
# ---------------------------------------------------------------------------
# Tab 5: Redis Health (needs Redis)
# ---------------------------------------------------------------------------
def redis_health_tab():
conn = _connect_db()
if not conn:
return "Not connected.\nSet REDIS_HOST, REDIS_PORT, etc. in Space Secrets to enable."
try:
import tools
return tools.check_redis_health(conn)
except Exception as e:
return f"Error: {e}"
# ---------------------------------------------------------------------------
# Tab 6: List Cached Plans (needs Redis)
# ---------------------------------------------------------------------------
def list_plans_tab(limit: int):
conn = _connect_db()
if not conn:
return "Not connected.\nSet PG and Redis credentials in Space Secrets to enable."
try:
import tools
return tools.list_cached_plans(conn, int(limit))
except Exception as e:
return f"Error: {e}"
# ---------------------------------------------------------------------------
# Tab 7: Inspect Plan (needs Redis)
# ---------------------------------------------------------------------------
def inspect_plan_tab(query_hash: str):
conn = _connect_db()
if not conn:
return "Not connected.\nSet Redis credentials in Space Secrets to enable."
if not query_hash.strip():
return "Enter a query hash."
try:
import tools
return tools.get_cached_plan_detail(conn, query_hash.strip())
except Exception as e:
return f"Error: {e}"
# ---------------------------------------------------------------------------
# Tab 8: Table Dependencies (needs Redis)
# ---------------------------------------------------------------------------
def deps_tab(table_name: str, query_hash: str):
conn = _connect_db()
if not conn:
return "Not connected.\nSet Redis credentials in Space Secrets to enable."
try:
import tools
return tools.get_table_dependencies(
conn,
table_name.strip() or None,
query_hash.strip() or None,
)
except Exception as e:
return f"Error: {e}"
# ---------------------------------------------------------------------------
# Tab 9: Extension Config (needs PG)
# ---------------------------------------------------------------------------
def config_tab():
conn = _connect_db()
if not conn:
return "Not connected.\nSet PG credentials in Space Secrets to enable."
try:
import tools
return tools.get_extension_config(conn)
except Exception as e:
return f"Error: {e}"
# ---------------------------------------------------------------------------
# Tab 10: Invalidate (needs PG)
# ---------------------------------------------------------------------------
def invalidate_tab(table_name: str):
conn = _connect_db()
if not conn:
return "Not connected.\nSet PG credentials in Space Secrets to enable."
try:
import tools
if table_name.strip():
return tools.invalidate_table_plans(conn, table_name.strip())
else:
return tools.invalidate_all_plans(conn)
except Exception as e:
return f"Error: {e}"
# ---------------------------------------------------------------------------
# Tab 11: Run SQL (needs PG)
# ---------------------------------------------------------------------------
def sql_tab(sql: str):
conn = _connect_db()
if not conn:
return "Not connected.\nSet PG credentials in Space Secrets to enable."
if not sql.strip():
return "Enter a SELECT query."
try:
import tools
return tools.run_sql_query(conn, sql.strip())
except Exception as e:
return f"Error: {e}"
# ---------------------------------------------------------------------------
# Architecture reference (always works)
# ---------------------------------------------------------------------------
ARCHITECTURE_MD = """
## pg_plan_cache β Architecture
### How It Works
```
Query arrives at PostgreSQL
β
βΌ
βββββββββββββββββββββββββββ
β planner_hook β β pgpc_planner_hook()
β intercepts planning β
ββββββββββ¬βββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββ
β normalize_query() β Replace literals with $N
β compute_query_hash() β SHA-256 of normalized query
ββββββββββ¬βββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββ ββββββββββββββββ
β Redis GET plan:<hash> ββββββΆβ Cache HIT? β
ββββββββββ¬βββββββββββββββββ ββββββββ¬ββββββββ
β β
Cache MISS Return cached
β PlannedStmt
βΌ
βββββββββββββββββββββββββββ
β standard_planner() β Run PostgreSQL planner
ββββββββββ¬βββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββ
β serialize + Redis SET β Store plan with TTL
β extract_table_names() β Track dependencies
β redis_set_dependencies()β Bidirectional mapping
βββββββββββββββββββββββββββ
```
### Key Components
| Component | File | Purpose |
|-----------|------|---------|
| Planner Hook | `pg_plan_cache.c` | Intercepts queries, checks cache, stores plans |
| Query Normalizer | `query_normalize.c` | Literals β `$N`, collapse whitespace, lowercase |
| Redis Client | `redis_client.c` | Connection pool, GET/SET/DEL, dependency tracking |
| Schema Invalidation | `pg_plan_cache.c` | Relcache callback bumps version on DDL |
### Redis Key Patterns
| Pattern | Purpose |
|---------|---------|
| `plan:<sha256>` | Serialized PlannedStmt + metadata |
| `deps:table:<schema.table>` | SET of query hashes depending on this table |
| `qdeps:<sha256>` | SET of table names this query depends on |
### GUC Parameters
| Parameter | Default | Restart? |
|-----------|---------|----------|
| `pg_plan_cache.redis_host` | 127.0.0.1 | No (SIGHUP) |
| `pg_plan_cache.redis_port` | 6379 | No (SIGHUP) |
| `pg_plan_cache.ttl` | 3600 | No (SIGHUP) |
| `pg_plan_cache.enabled` | true | No (SUSET) |
| `pg_plan_cache.redis_timeout_ms` | 500 | No (SIGHUP) |
| `pg_plan_cache.redis_pool_size` | 4 | Yes (POSTMASTER) |
### Serialization Format
```
schema_version|total_cost|unix_timestamp|nodeToString(PlannedStmt)
```
Plans with a stale `schema_version` are discarded on read.
### Normalization Examples
| Input | Normalized |
|-------|-----------|
| `SELECT * FROM users WHERE id = 42` | `select * from users where id = $1` |
| `SELECT name FROM t WHERE x = 'hello'` | `select name from t where x = $1` |
| `SELECT /* comment */ a FROM b` | `select a from b` |
"""
# ---------------------------------------------------------------------------
# Build Gradio app
# ---------------------------------------------------------------------------
def build_app():
with gr.Blocks(
title="pg_plan_cache Agent",
theme=gr.themes.Soft(),
) as app:
gr.Markdown("# pg_plan_cache Agent")
gr.Markdown(
"Manage, monitor, and diagnose the **pg_plan_cache** PostgreSQL extension. "
"The **Normalizer** and **Architecture** tabs work without any backend. "
"All other tabs require PostgreSQL and Redis credentials set as environment variables."
)
live_status = "Connected" if _is_live() else "Demo mode (no backend)"
gr.Markdown(f"**Status:** {live_status}")
with gr.Tab("Normalizer"):
gr.Markdown("Normalize a SQL query and compute its SHA-256 cache key.")
query_input = gr.Textbox(
label="SQL Query",
placeholder="SELECT * FROM users WHERE id = 42 AND name = 'Alice'",
lines=3,
)
norm_btn = gr.Button("Normalize & Hash", variant="primary")
norm_output = gr.Textbox(label="Normalized Query", lines=2)
hash_output = gr.Textbox(label="Cache Key (SHA-256)")
status_output = gr.Textbox(label="Cache Status")
norm_btn.click(
fn=normalize_tab,
inputs=[query_input],
outputs=[norm_output, hash_output, status_output],
)
with gr.Tab("Cache Stats"):
gr.Markdown("Live cache hit/miss/error statistics from `pg_plan_cache_stats()`.")
stats_btn = gr.Button("Refresh Stats", variant="primary")
stats_output = gr.Textbox(label="Statistics", lines=12)
stats_btn.click(fn=stats_tab, outputs=[stats_output])
with gr.Tab("Analyze"):
gr.Markdown("Cache efficiency analysis with tuning recommendations.")
analyze_btn = gr.Button("Run Analysis", variant="primary")
analyze_output = gr.Textbox(label="Analysis", lines=20)
analyze_btn.click(fn=analyze_tab, outputs=[analyze_output])
with gr.Tab("Diagnose"):
gr.Markdown("Full diagnostic: PostgreSQL, Redis, stats, and configuration.")
diag_btn = gr.Button("Run Diagnostics", variant="primary")
diag_output = gr.Textbox(label="Diagnostic Report", lines=25)
diag_btn.click(fn=diagnose_tab, outputs=[diag_output])
with gr.Tab("Redis Health"):
gr.Markdown("Redis connectivity, memory, and keyspace info.")
redis_btn = gr.Button("Check Redis", variant="primary")
redis_output = gr.Textbox(label="Redis Health", lines=12)
redis_btn.click(fn=redis_health_tab, outputs=[redis_output])
with gr.Tab("Cached Plans"):
gr.Markdown("List cached plan keys stored in Redis.")
plan_limit = gr.Slider(minimum=5, maximum=200, value=50, step=5, label="Limit")
plans_btn = gr.Button("List Plans", variant="primary")
plans_output = gr.Textbox(label="Plans", lines=15)
plans_btn.click(fn=list_plans_tab, inputs=[plan_limit], outputs=[plans_output])
with gr.Tab("Inspect Plan"):
gr.Markdown("View details of a specific cached plan by hash.")
hash_input = gr.Textbox(label="Query Hash (64-char hex)", placeholder="abc123...")
inspect_btn = gr.Button("Inspect", variant="primary")
inspect_output = gr.Textbox(label="Plan Details", lines=12)
inspect_btn.click(fn=inspect_plan_tab, inputs=[hash_input], outputs=[inspect_output])
with gr.Tab("Dependencies"):
gr.Markdown("Explore bidirectional dependency mappings between queries and tables.")
dep_table = gr.Textbox(label="Table Name", placeholder="public.users")
dep_hash = gr.Textbox(label="Query Hash (optional)", placeholder="abc123...")
deps_btn = gr.Button("Look Up", variant="primary")
deps_output = gr.Textbox(label="Dependencies", lines=12)
deps_btn.click(fn=deps_tab, inputs=[dep_table, dep_hash], outputs=[deps_output])
with gr.Tab("Configuration"):
gr.Markdown("View current GUC parameter values for pg_plan_cache.")
cfg_btn = gr.Button("Show Config", variant="primary")
cfg_output = gr.Textbox(label="Configuration", lines=8)
cfg_btn.click(fn=config_tab, outputs=[cfg_output])
with gr.Tab("Invalidate"):
gr.Markdown("Invalidate cached plans. Leave table name empty to invalidate ALL plans.")
inv_table = gr.Textbox(label="Table Name (optional)", placeholder="public.users")
inv_btn = gr.Button("Invalidate", variant="stop")
inv_output = gr.Textbox(label="Result")
inv_btn.click(fn=invalidate_tab, inputs=[inv_table], outputs=[inv_output])
with gr.Tab("SQL Query"):
gr.Markdown("Execute a read-only SELECT query against PostgreSQL.")
sql_input = gr.Textbox(
label="SQL (SELECT only)",
placeholder="SELECT * FROM pg_stat_activity LIMIT 5",
lines=3,
)
sql_btn = gr.Button("Execute", variant="primary")
sql_output = gr.Textbox(label="Results", lines=15)
sql_btn.click(fn=sql_tab, inputs=[sql_input], outputs=[sql_output])
with gr.Tab("Architecture"):
gr.Markdown(ARCHITECTURE_MD)
return app
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
app = build_app()
app.launch()
|