Skip to content
5 changes: 5 additions & 0 deletions .changeset/ninety-snakes-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/diagnostics-app': patch
---

Added quick-select template query buttons for common PowerSync internal tables (ps_oplog, ps_crud, ps_buckets, ps_untyped) with tooltips and docs links to both SQL consoles
4 changes: 2 additions & 2 deletions tools/diagnostics-app/src/app/views/inspector-sql-console.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { NavigationPage } from '@/components/navigation/NavigationPage';
import { SQLConsoleCore } from '@/app/views/shared/sql-console-core';
import { SQLConsoleCore, POWERSYNC_TEMPLATE_QUERIES, CLIENT_ARCH_DOCS_URL } from '@/app/views/shared/sql-console-core';
import { useInspectorDatabase } from '@/library/inspector/InspectorContext';

const DEFAULT_QUERY = `SELECT name, type FROM sqlite_master ORDER BY type, name`;
Expand All @@ -17,7 +17,7 @@ export default function InspectorSQLConsolePage() {

return (
<NavigationPage title="SQL Console">
<SQLConsoleCore executeQuery={executeQuery} defaultQuery={DEFAULT_QUERY} historySource="inspector" />
<SQLConsoleCore executeQuery={executeQuery} defaultQuery={DEFAULT_QUERY} historySource="inspector" templateQueries={POWERSYNC_TEMPLATE_QUERIES} templateDocsUrl={CLIENT_ARCH_DOCS_URL} />
</NavigationPage>
);
}
16 changes: 15 additions & 1 deletion tools/diagnostics-app/src/app/views/shared/query-history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,34 @@ interface QueryHistoryEntry {
executed_at: string;
}

export interface QueryHistoryHandle {
setQuery: (query: string) => void;
}

export interface QueryHistoryDropdownProps {
source: string;
defaultQuery?: string;
ready?: boolean;
error?: string | null;
onQueryChanged: (params: { query: string }) => void;
onReady?: (handle: QueryHistoryHandle) => void;
}

/**
* Inner component that renders the query input, execute button, and history dropdown.
* Must be rendered inside a PowerSyncContext provider for useQuery to work.
*/
function QueryHistoryInput({ source, defaultQuery = '', ready = true, error, onQueryChanged }: QueryHistoryDropdownProps) {
function QueryHistoryInput({ source, defaultQuery = '', ready = true, error, onQueryChanged, onReady }: QueryHistoryDropdownProps) {
const inputRef = React.useRef<HTMLInputElement>(null);

React.useEffect(() => {
onReady?.({
setQuery: (query: string) => {
if (inputRef.current) inputRef.current.value = query;
}
});
}, [onReady]);

const inputWrapperRef = React.useRef<HTMLDivElement>(null);
const [showHistory, setShowHistory] = React.useState(false);
const [dropdownStyle, setDropdownStyle] = React.useState<React.CSSProperties>({});
Expand Down
128 changes: 126 additions & 2 deletions tools/diagnostics-app/src/app/views/shared/sql-console-core.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,54 @@
import React from 'react';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner';
import { DataTable, DataTableColumn } from '@/components/ui/data-table';
import { QueryHistoryDropdown } from './query-history';
import { Card, CardContent } from '@/components/ui/card';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { QueryHistoryDropdown, QueryHistoryHandle } from './query-history';

// ---------------------------------------------------------------------------
// SQLConsoleCore - the reusable SQL console UI
// ---------------------------------------------------------------------------

const MAX_RESULT_ROWS = 10_000;
const MAX_DISCOVERED_VIEWS = 30;

export interface TemplateQuery {
label: string;
query: string;
tooltip: string;
}

export const CLIENT_ARCH_DOCS_URL =
'https://docs.powersync.com/architecture/client-architecture#client-side-schema-and-sqlite-database-structure';

export const POWERSYNC_TEMPLATE_QUERIES: TemplateQuery[] = [
{
label: 'ps_untyped',
query: 'SELECT * FROM ps_untyped',
tooltip:
'Synced data not matching any table in the client-side schema. Rows migrate to ps_data__<table> once the table is added to the schema.'
},
{
label: 'ps_oplog',
query: 'SELECT * FROM ps_oplog',
tooltip:
'Operation log received from the PowerSync Service, grouped per bucket. Useful for debugging sync state and inspecting individual operations.'
},
{
label: 'ps_crud',
query: 'SELECT * FROM ps_crud',
tooltip:
'Pending local changes waiting to be uploaded. If rows are stuck here, the upload queue may be blocked by a failing operation.'
},
{
label: 'ps_buckets',
query: 'SELECT * FROM ps_buckets',
tooltip:
'Metadata for each sync bucket including last applied op and checkpoint. Helpful for verifying which buckets are actively syncing.'
}
];

export interface SQLConsoleCoreProps {
/** Execute a query and return the results as an array of row objects */
Expand All @@ -19,14 +59,27 @@ export interface SQLConsoleCoreProps {
historySource?: string;
/** Whether the target database is ready for queries. Auto-execution is deferred until true. Defaults to true. */
ready?: boolean;
/** Predefined queries shown as quick-select buttons. These don't save to history. */
templateQueries?: TemplateQuery[];
/** URL shown below the Quick Queries heading as a "Learn more" link */
templateDocsUrl?: string;
}

export function SQLConsoleCore({ executeQuery, defaultQuery = '', historySource = 'powersync', ready = true }: SQLConsoleCoreProps) {
export function SQLConsoleCore({ executeQuery, defaultQuery = '', historySource = 'powersync', ready = true, templateQueries, templateDocsUrl }: SQLConsoleCoreProps) {
const historyHandleRef = React.useRef<QueryHistoryHandle | null>(null);
const [results, setResults] = React.useState<Record<string, any>[] | null>(null);
const [totalRowCount, setTotalRowCount] = React.useState(0);
const [isLoading, setIsLoading] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const [autoLimited, setAutoLimited] = React.useState(false);
const [discoveredTables, setDiscoveredTables] = React.useState<string[]>([]);

React.useEffect(() => {
if (!ready) return;
executeQuery(`SELECT name FROM sqlite_master WHERE type='view' AND name NOT LIKE 'ps_%' ORDER BY name`)
.then((rows) => setDiscoveredTables(rows.map((r) => r.name as string)))
.catch(() => {});
}, [ready, executeQuery]);

const runQuery = React.useCallback(
async (sql: string) => {
Expand Down Expand Up @@ -87,6 +140,76 @@ export function SQLConsoleCore({ executeQuery, defaultQuery = '', historySource

return (
<div className="min-w-0 max-w-full p-5">
{templateQueries && templateQueries.length > 0 && (
<div className="mb-4 space-y-3">
<Label className="mb-1 block">Quick Queries</Label>
<Card>
<CardContent className="p-3 space-y-2">
<p className="text-xs text-muted-foreground">
PowerSync internal tables.{' '}
{templateDocsUrl && (
<a
href={templateDocsUrl}
target="_blank"
rel="noopener noreferrer"
className="hover:text-foreground underline">
Learn more about client architecture
</a>
)}
</p>
<div className="flex flex-wrap gap-2">
<TooltipProvider delayDuration={200}>
{templateQueries.map((tq) => (
<Tooltip key={tq.label}>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={() => {
historyHandleRef.current?.setQuery(tq.query);
runQuery(tq.query);
}}>
{tq.label}
</Button>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p>{tq.tooltip}</p>
</TooltipContent>
</Tooltip>
))}
</TooltipProvider>
</div>
</CardContent>
</Card>
{discoveredTables.length > 0 && (
<Card>
<CardContent className="p-3 space-y-2">
<p className="text-xs text-muted-foreground">
Views discovered from the database schema.
{discoveredTables.length > MAX_DISCOVERED_VIEWS && ` Showing first ${MAX_DISCOVERED_VIEWS} of ${discoveredTables.length}.`}
</p>
<div className="flex flex-wrap gap-2">
{discoveredTables.slice(0, MAX_DISCOVERED_VIEWS).map((name) => {
const query = `SELECT * FROM ${name}`;
return (
<Button
key={name}
variant="outline"
size="sm"
onClick={() => {
historyHandleRef.current?.setQuery(query);
runQuery(query);
}}>
{name}
</Button>
);
})}
</div>
</CardContent>
</Card>
)}
</div>
)}
<div className="flex flex-wrap items-end gap-2.5 mb-4">
<div className="min-w-0 flex-1 basis-0 space-y-1.5 relative">
<Label htmlFor="query-input">Query</Label>
Expand All @@ -96,6 +219,7 @@ export function SQLConsoleCore({ executeQuery, defaultQuery = '', historySource
ready={ready}
error={error}
onQueryChanged={handleQueryChanged}
onReady={(handle) => { historyHandleRef.current = handle; }}
/>
{error && <p className="text-sm text-destructive">{error}</p>}
</div>
Expand Down
4 changes: 2 additions & 2 deletions tools/diagnostics-app/src/app/views/sql-console.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { NavigationPage } from '@/components/navigation/NavigationPage';
import { SQLConsoleCore } from '@/app/views/shared/sql-console-core';
import { SQLConsoleCore, POWERSYNC_TEMPLATE_QUERIES, CLIENT_ARCH_DOCS_URL } from '@/app/views/shared/sql-console-core';
import { db, useSchemaReady } from '@/library/powersync/ConnectionManager';

const DEFAULT_QUERY = `SELECT name FROM ps_buckets`;
Expand All @@ -14,7 +14,7 @@ export default function SQLConsolePage() {

return (
<NavigationPage title="SQL Console">
<SQLConsoleCore executeQuery={executeQuery} defaultQuery={DEFAULT_QUERY} ready={schemaReady} />
<SQLConsoleCore executeQuery={executeQuery} defaultQuery={DEFAULT_QUERY} ready={schemaReady} templateQueries={POWERSYNC_TEMPLATE_QUERIES} templateDocsUrl={CLIENT_ARCH_DOCS_URL} />
</NavigationPage>
);
}