11import React from 'react' ;
22import { Label } from '@/components/ui/label' ;
3+ import { Button } from '@/components/ui/button' ;
34import { Spinner } from '@/components/ui/spinner' ;
45import { DataTable , DataTableColumn } from '@/components/ui/data-table' ;
5- import { QueryHistoryDropdown } from './query-history' ;
6+ import { Card , CardContent } from '@/components/ui/card' ;
7+ import { Tooltip , TooltipContent , TooltipProvider , TooltipTrigger } from '@/components/ui/tooltip' ;
8+ import { QueryHistoryDropdown , QueryHistoryHandle } from './query-history' ;
69
710// ---------------------------------------------------------------------------
811// SQLConsoleCore - the reusable SQL console UI
912// ---------------------------------------------------------------------------
1013
1114const MAX_RESULT_ROWS = 10_000 ;
15+ const MAX_DISCOVERED_VIEWS = 30 ;
16+
17+ export interface TemplateQuery {
18+ label : string ;
19+ query : string ;
20+ tooltip : string ;
21+ }
22+
23+ export const CLIENT_ARCH_DOCS_URL =
24+ 'https://docs.powersync.com/architecture/client-architecture#client-side-schema-and-sqlite-database-structure' ;
25+
26+ export const POWERSYNC_TEMPLATE_QUERIES : TemplateQuery [ ] = [
27+ {
28+ label : 'ps_untyped' ,
29+ query : 'SELECT * FROM ps_untyped' ,
30+ tooltip :
31+ '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.'
32+ } ,
33+ {
34+ label : 'ps_oplog' ,
35+ query : 'SELECT * FROM ps_oplog' ,
36+ tooltip :
37+ 'Operation log received from the PowerSync Service, grouped per bucket. Useful for debugging sync state and inspecting individual operations.'
38+ } ,
39+ {
40+ label : 'ps_crud' ,
41+ query : 'SELECT * FROM ps_crud' ,
42+ tooltip :
43+ 'Pending local changes waiting to be uploaded. If rows are stuck here, the upload queue may be blocked by a failing operation.'
44+ } ,
45+ {
46+ label : 'ps_buckets' ,
47+ query : 'SELECT * FROM ps_buckets' ,
48+ tooltip :
49+ 'Metadata for each sync bucket including last applied op and checkpoint. Helpful for verifying which buckets are actively syncing.'
50+ }
51+ ] ;
1252
1353export interface SQLConsoleCoreProps {
1454 /** Execute a query and return the results as an array of row objects */
@@ -19,14 +59,27 @@ export interface SQLConsoleCoreProps {
1959 historySource ?: string ;
2060 /** Whether the target database is ready for queries. Auto-execution is deferred until true. Defaults to true. */
2161 ready ?: boolean ;
62+ /** Predefined queries shown as quick-select buttons. These don't save to history. */
63+ templateQueries ?: TemplateQuery [ ] ;
64+ /** URL shown below the Quick Queries heading as a "Learn more" link */
65+ templateDocsUrl ?: string ;
2266}
2367
24- export function SQLConsoleCore ( { executeQuery, defaultQuery = '' , historySource = 'powersync' , ready = true } : SQLConsoleCoreProps ) {
68+ export function SQLConsoleCore ( { executeQuery, defaultQuery = '' , historySource = 'powersync' , ready = true , templateQueries, templateDocsUrl } : SQLConsoleCoreProps ) {
69+ const historyHandleRef = React . useRef < QueryHistoryHandle | null > ( null ) ;
2570 const [ results , setResults ] = React . useState < Record < string , any > [ ] | null > ( null ) ;
2671 const [ totalRowCount , setTotalRowCount ] = React . useState ( 0 ) ;
2772 const [ isLoading , setIsLoading ] = React . useState ( false ) ;
2873 const [ error , setError ] = React . useState < string | null > ( null ) ;
2974 const [ autoLimited , setAutoLimited ] = React . useState ( false ) ;
75+ const [ discoveredTables , setDiscoveredTables ] = React . useState < string [ ] > ( [ ] ) ;
76+
77+ React . useEffect ( ( ) => {
78+ if ( ! ready ) return ;
79+ executeQuery ( `SELECT name FROM sqlite_master WHERE type='view' AND name NOT LIKE 'ps_%' ORDER BY name` )
80+ . then ( ( rows ) => setDiscoveredTables ( rows . map ( ( r ) => r . name as string ) ) )
81+ . catch ( ( ) => { } ) ;
82+ } , [ ready , executeQuery ] ) ;
3083
3184 const runQuery = React . useCallback (
3285 async ( sql : string ) => {
@@ -87,6 +140,76 @@ export function SQLConsoleCore({ executeQuery, defaultQuery = '', historySource
87140
88141 return (
89142 < div className = "min-w-0 max-w-full p-5" >
143+ { templateQueries && templateQueries . length > 0 && (
144+ < div className = "mb-4 space-y-3" >
145+ < Label className = "mb-1 block" > Quick Queries</ Label >
146+ < Card >
147+ < CardContent className = "p-3 space-y-2" >
148+ < p className = "text-xs text-muted-foreground" >
149+ PowerSync internal tables.{ ' ' }
150+ { templateDocsUrl && (
151+ < a
152+ href = { templateDocsUrl }
153+ target = "_blank"
154+ rel = "noopener noreferrer"
155+ className = "hover:text-foreground underline" >
156+ Learn more about client architecture
157+ </ a >
158+ ) }
159+ </ p >
160+ < div className = "flex flex-wrap gap-2" >
161+ < TooltipProvider delayDuration = { 200 } >
162+ { templateQueries . map ( ( tq ) => (
163+ < Tooltip key = { tq . label } >
164+ < TooltipTrigger asChild >
165+ < Button
166+ variant = "outline"
167+ size = "sm"
168+ onClick = { ( ) => {
169+ historyHandleRef . current ?. setQuery ( tq . query ) ;
170+ runQuery ( tq . query ) ;
171+ } } >
172+ { tq . label }
173+ </ Button >
174+ </ TooltipTrigger >
175+ < TooltipContent className = "max-w-xs" >
176+ < p > { tq . tooltip } </ p >
177+ </ TooltipContent >
178+ </ Tooltip >
179+ ) ) }
180+ </ TooltipProvider >
181+ </ div >
182+ </ CardContent >
183+ </ Card >
184+ { discoveredTables . length > 0 && (
185+ < Card >
186+ < CardContent className = "p-3 space-y-2" >
187+ < p className = "text-xs text-muted-foreground" >
188+ Views discovered from the database schema.
189+ { discoveredTables . length > MAX_DISCOVERED_VIEWS && ` Showing first ${ MAX_DISCOVERED_VIEWS } of ${ discoveredTables . length } .` }
190+ </ p >
191+ < div className = "flex flex-wrap gap-2" >
192+ { discoveredTables . slice ( 0 , MAX_DISCOVERED_VIEWS ) . map ( ( name ) => {
193+ const query = `SELECT * FROM ${ name } ` ;
194+ return (
195+ < Button
196+ key = { name }
197+ variant = "outline"
198+ size = "sm"
199+ onClick = { ( ) => {
200+ historyHandleRef . current ?. setQuery ( query ) ;
201+ runQuery ( query ) ;
202+ } } >
203+ { name }
204+ </ Button >
205+ ) ;
206+ } ) }
207+ </ div >
208+ </ CardContent >
209+ </ Card >
210+ ) }
211+ </ div >
212+ ) }
90213 < div className = "flex flex-wrap items-end gap-2.5 mb-4" >
91214 < div className = "min-w-0 flex-1 basis-0 space-y-1.5 relative" >
92215 < Label htmlFor = "query-input" > Query</ Label >
@@ -96,6 +219,7 @@ export function SQLConsoleCore({ executeQuery, defaultQuery = '', historySource
96219 ready = { ready }
97220 error = { error }
98221 onQueryChanged = { handleQueryChanged }
222+ onReady = { ( handle ) => { historyHandleRef . current = handle ; } }
99223 />
100224 { error && < p className = "text-sm text-destructive" > { error } </ p > }
101225 </ div >
0 commit comments