branch:
SqlDemo.tsx
11768 bytesRaw
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<Env> {
@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<Array<{ name: string; type: string }>>(
[]
);
const [selectedTable, setSelectedTable] = useState<string | null>(null);
const [schema, setSchema] = useState<unknown[]>([]);
const [query, setQuery] = useState("SELECT * FROM cf_agents_state");
const [queryResult, setQueryResult] = useState<unknown[] | null>(null);
const [newKey, setNewKey] = useState("");
const [newValue, setNewValue] = useState("");
const [records, setRecords] = useState<unknown[]>([]);
const agent = useAgent<SqlAgent, {}>({
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 (
<DemoWrapper
title="SQL Queries"
description={
<>
Every agent instance has its own embedded SQLite database, backed by
Durable Object storage. Use{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
this.sql
</code>{" "}
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={
<ConnectionStatus
status={
agent.readyState === WebSocket.OPEN ? "connected" : "connecting"
}
/>
}
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Controls */}
<div className="space-y-6">
{/* Tables */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="flex items-center justify-between mb-4">
<Text variant="heading3">Tables</Text>
<Button variant="ghost" size="xs" onClick={loadTables}>
Refresh
</Button>
</div>
{tables.length === 0 ? (
<p className="text-sm text-kumo-inactive">Loading...</p>
) : (
<div className="space-y-1">
{tables
.filter((t) => t.type === "table")
.map((table) => (
<button
type="button"
key={table.name}
onClick={() => handleSelectTable(table.name)}
className={`w-full text-left py-1.5 px-2 rounded text-sm transition-colors ${
selectedTable === table.name
? "bg-kumo-contrast text-kumo-inverse"
: "hover:bg-kumo-tint text-kumo-default"
}`}
>
{table.name}
</button>
))}
</div>
)}
</Surface>
{/* Schema */}
{selectedTable && schema.length > 0 && (
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">Schema: {selectedTable}</Text>
</div>
<div className="overflow-x-auto">
<Table>
<Table.Header>
<Table.Row>
<Table.Head>Column</Table.Head>
<Table.Head>Type</Table.Head>
<Table.Head>Nullable</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{schema.map((col: unknown, i) => {
const c = col as {
name: string;
type: string;
notnull: number;
};
return (
<Table.Row key={i}>
<Table.Cell className="font-mono">
{c.name}
</Table.Cell>
<Table.Cell>{c.type}</Table.Cell>
<Table.Cell>{c.notnull ? "No" : "Yes"}</Table.Cell>
</Table.Row>
);
})}
</Table.Body>
</Table>
</div>
</Surface>
)}
{/* Query */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">Execute Query</Text>
</div>
<InputArea
aria-label="SQL query"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full h-24 font-mono"
placeholder="SELECT * FROM ..."
/>
<Button
variant="primary"
onClick={handleExecuteQuery}
className="mt-2 w-full"
>
Execute
</Button>
<p className="text-xs text-kumo-subtle mt-2">
Only SELECT queries are allowed in the playground
</p>
</Surface>
{/* Query Result */}
{queryResult && (
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">
Results ({queryResult.length} rows)
</Text>
</div>
<div className="max-h-60 overflow-y-auto">
<HighlightedJson data={queryResult} />
</div>
</Surface>
)}
{/* Insert Record */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">Custom Data</Text>
</div>
<div className="flex gap-2 mb-3">
<Input
aria-label="Record key"
type="text"
value={newKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setNewKey(e.target.value)
}
className="flex-1"
placeholder="Key"
/>
<Input
aria-label="Record value"
type="text"
value={newValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setNewValue(e.target.value)
}
className="flex-1"
placeholder="Value"
/>
<Button variant="primary" onClick={handleInsertRecord}>
Insert
</Button>
</div>
{records.length > 0 && (
<div className="space-y-1">
{records.map((r: unknown, i) => {
const rec = r as { key: string; value: string };
return (
<div
key={i}
className="flex justify-between py-1 px-2 bg-kumo-elevated rounded text-sm"
>
<span className="font-mono text-kumo-default">
{rec.key}
</span>
<span className="text-kumo-subtle">{rec.value}</span>
</div>
);
})}
</div>
)}
</Surface>
</div>
{/* Logs */}
<div className="space-y-6">
<LogPanel logs={logs} onClear={clearLogs} maxHeight="400px" />
</div>
</div>
<CodeExplanation sections={codeSections} />
</DemoWrapper>
);
}