import { useAgent } from "agents/react"; import { useState, useEffect, useCallback } from "react"; import { Button, Input, InputArea, Surface, Table, Text } from "@cloudflare/kumo"; import { DemoWrapper } from "../../layout"; import { LogPanel, ConnectionStatus, CodeExplanation, HighlightedJson, type CodeSection } from "../../components"; import { useLogs, useUserId, useToast } from "../../hooks"; import type { SqlAgent } from "./sql-agent"; const codeSections: CodeSection[] = [ { title: "Query with the sql tagged template", description: "Every agent has a built-in SQLite database. Use this.sql with a tagged template literal for safe, parameterized queries — values are automatically escaped.", code: `import { Agent, callable } from "agents"; class SqlAgent extends Agent { @callable() insertRecord(key: string, value: string) { this.sql\` CREATE TABLE IF NOT EXISTS playground_data ( key TEXT PRIMARY KEY, value TEXT, created_at INTEGER DEFAULT (unixepoch()) ) \`; this.sql\` INSERT OR REPLACE INTO playground_data (key, value) VALUES (\${key}, \${value}) \`; } }` }, { title: "Read data back", description: "this.sql returns results as an array directly — no need to spread a cursor. For lower-level access, this.ctx.storage.sql.exec() returns a raw iterable cursor you can stream through.", code: ` @callable() getRecords() { // this.sql returns T[] directly return this.sql\` SELECT * FROM playground_data ORDER BY created_at DESC \`; } @callable() listTables() { return this.sql\` SELECT name, type FROM sqlite_master WHERE type IN ('table', 'index') ORDER BY type, name \`; }` } ]; export function SqlDemo() { const userId = useUserId(); const { logs, addLog, clearLogs } = useLogs(); const { toast } = useToast(); const [tables, setTables] = useState>( [] ); const [selectedTable, setSelectedTable] = useState(null); const [schema, setSchema] = useState([]); const [query, setQuery] = useState("SELECT * FROM cf_agents_state"); const [queryResult, setQueryResult] = useState(null); const [newKey, setNewKey] = useState(""); const [newValue, setNewValue] = useState(""); const [records, setRecords] = useState([]); const agent = useAgent({ agent: "sql-agent", name: `sql-demo-${userId}`, onOpen: () => { addLog("info", "connected"); loadTables(); loadRecords(); }, onClose: () => addLog("info", "disconnected"), onError: () => addLog("error", "error", "Connection error") }); const loadTables = useCallback(async () => { try { const result = (await agent.call("listTables")) as Array<{ name: string; type: string; }>; setTables(result); } catch { // Ignore } }, [agent]); const loadRecords = useCallback(async () => { try { const result = (await agent.call("getRecords")) as unknown[]; setRecords(result); } catch { // Ignore } }, [agent]); useEffect(() => { if (agent.readyState === WebSocket.OPEN) { loadTables(); loadRecords(); } }, [agent.readyState, loadTables, loadRecords]); const handleSelectTable = async (tableName: string) => { setSelectedTable(tableName); addLog("out", "getTableSchema", tableName); try { const result = (await agent.call("getTableSchema", [ tableName ])) as unknown[]; addLog("in", "schema", result); setSchema(result); setQuery(`SELECT * FROM ${tableName} LIMIT 10`); } catch (e) { addLog("error", "error", e instanceof Error ? e.message : String(e)); } }; const handleExecuteQuery = async () => { addLog("out", "executeQuery", query); setQueryResult(null); try { const result = (await agent.call("executeQuery", [query])) as unknown[]; addLog("in", "query_result", `${result.length} rows`); setQueryResult(result); toast(result.length + " rows returned", "success"); } catch (e) { addLog("error", "error", e instanceof Error ? e.message : String(e)); } }; const handleInsertRecord = async () => { if (!newKey.trim() || !newValue.trim()) return; addLog("out", "insertRecord", { key: newKey, value: newValue }); try { await agent.call("insertRecord", [newKey, newValue]); addLog("in", "inserted"); toast("Record inserted", "success"); setNewKey(""); setNewValue(""); loadRecords(); loadTables(); } catch (e) { addLog("error", "error", e instanceof Error ? e.message : String(e)); } }; return ( Every agent instance has its own embedded SQLite database, backed by Durable Object storage. Use{" "} this.sql {" "} as a tagged template literal for safe, parameterized queries — values are automatically escaped. Explore the tables below, run custom queries, or insert your own data. } statusIndicator={ } >
{/* Controls */}
{/* Tables */}
Tables
{tables.length === 0 ? (

Loading...

) : (
{tables .filter((t) => t.type === "table") .map((table) => ( ))}
)}
{/* Schema */} {selectedTable && schema.length > 0 && (
Schema: {selectedTable}
Column Type Nullable {schema.map((col: unknown, i) => { const c = col as { name: string; type: string; notnull: number; }; return ( {c.name} {c.type} {c.notnull ? "No" : "Yes"} ); })}
)} {/* Query */}
Execute Query
setQuery(e.target.value)} className="w-full h-24 font-mono" placeholder="SELECT * FROM ..." />

Only SELECT queries are allowed in the playground

{/* Query Result */} {queryResult && (
Results ({queryResult.length} rows)
)} {/* Insert Record */}
Custom Data
) => setNewKey(e.target.value) } className="flex-1" placeholder="Key" /> ) => setNewValue(e.target.value) } className="flex-1" placeholder="Value" />
{records.length > 0 && (
{records.map((r: unknown, i) => { const rec = r as { key: string; value: string }; return (
{rec.key} {rec.value}
); })}
)}
{/* Logs */}
); }