@@ -7,11 +7,20 @@ import { QueryTabs } from '../QueryTabs/QueryTabs'
77import { TableView } from '../TableView/TableView'
88import { AIAssistant } from '../AIAssistant'
99import { SqlEditor } from './SqlEditor'
10+ import { ExclamationTriangleIcon , MagicWandIcon , CodeIcon , PlayIcon } from '@radix-ui/react-icons'
1011
1112import { exportToCSV , exportToJSON } from '../../utils/exportData'
1213import { Tab , QueryTab , TableTab , QueryExecutionResult } from '../../types/tabs'
14+ import {
15+ isSelectQuery ,
16+ hasLimitClause ,
17+ addLimitToQuery ,
18+ mightReturnLargeResultSet
19+ } from '../../utils/queryParser'
1320import './QueryWorkspace.css'
1421
22+ const DEFAULT_LIMIT = 100
23+
1524interface QueryWorkspaceProps {
1625 connectionId : string
1726 onOpenTableTab ?: ( database : string , tableName : string ) => void
@@ -33,7 +42,9 @@ export function QueryWorkspace({ connectionId, onOpenTableTab }: QueryWorkspaceP
3342 const [ selectedText , setSelectedText ] = useState ( '' )
3443 const [ showAIPanel , setShowAIPanel ] = useState ( false )
3544 const editorRef = useRef < any > ( null )
36- const executeQueryRef = useRef < ( ) => void > ( )
45+ const executeQueryRef = useRef < ( ) => void > ( ( ) => { } )
46+ const [ showLimitWarning , setShowLimitWarning ] = useState ( false )
47+ const [ queryLimitOverride , setQueryLimitOverride ] = useState ( false )
3748
3849 const activeTab = tabs . find ( ( tab ) => tab . id === activeTabId )
3950 const activeResult = activeTab ? results [ activeTab . id ] : null
@@ -96,6 +107,8 @@ export function QueryWorkspace({ connectionId, onOpenTableTab }: QueryWorkspaceP
96107
97108 const handleSelectTab = useCallback ( ( tabId : string ) => {
98109 setActiveTabId ( tabId )
110+ setShowLimitWarning ( false )
111+ setQueryLimitOverride ( false )
99112 } , [ ] )
100113
101114 const handleUpdateTabTitle = useCallback (
@@ -132,7 +145,7 @@ export function QueryWorkspace({ connectionId, onOpenTableTab }: QueryWorkspaceP
132145 } , [ openTableTab , onOpenTableTab ] )
133146
134147 const executeQuery = useCallback (
135- async ( queryToExecute : string ) => {
148+ async ( queryToExecute : string , forceUnlimited = false ) => {
136149 if ( ! activeTab || activeTab . type !== 'query' ) return
137150
138151 if ( ! queryToExecute . trim ( ) ) return
@@ -141,7 +154,22 @@ export function QueryWorkspace({ connectionId, onOpenTableTab }: QueryWorkspaceP
141154 setIsExecuting ( true )
142155 const startTime = Date . now ( )
143156
144- const queryResult = await window . api . database . query ( connectionId , queryToExecute . trim ( ) )
157+ let finalQuery = queryToExecute . trim ( )
158+
159+ // Check if we should add a limit
160+ if (
161+ isSelectQuery ( finalQuery ) &&
162+ ! hasLimitClause ( finalQuery ) &&
163+ ! forceUnlimited &&
164+ ! queryLimitOverride
165+ ) {
166+ if ( mightReturnLargeResultSet ( finalQuery ) ) {
167+ setShowLimitWarning ( true )
168+ finalQuery = addLimitToQuery ( finalQuery , DEFAULT_LIMIT )
169+ }
170+ }
171+
172+ const queryResult = await window . api . database . query ( connectionId , finalQuery )
145173 const executionTime = Date . now ( ) - startTime
146174
147175 const result : QueryExecutionResult = {
@@ -164,7 +192,7 @@ export function QueryWorkspace({ connectionId, onOpenTableTab }: QueryWorkspaceP
164192 setIsExecuting ( false )
165193 }
166194 } ,
167- [ activeTab , results , connectionId ]
195+ [ activeTab , results , connectionId , queryLimitOverride ]
168196 )
169197
170198 const handleExecuteQuery = useCallback ( async ( ) => {
@@ -309,19 +337,21 @@ export function QueryWorkspace({ connectionId, onOpenTableTab }: QueryWorkspaceP
309337 < Flex gap = "2" align = "center" >
310338 < Button
311339 size = "1"
312- variant = { showAIPanel ? 'solid' : 'ghost ' }
340+ variant = { showAIPanel ? 'solid' : 'soft ' }
313341 onClick = { ( ) => setShowAIPanel ( ! showAIPanel ) }
314- style = { { minWidth : '60px' } }
315342 >
316- ✨ AI
343+ < MagicWandIcon />
344+ AI
317345 </ Button >
318- < Button size = "1" variant = "ghost" onClick = { formatQuery } >
346+ < Button size = "1" variant = "soft" onClick = { formatQuery } >
347+ < CodeIcon />
319348 Format
320349 </ Button >
321350 < Button
351+ size = "1"
352+ variant = "solid"
322353 onClick = { handleExecuteQuery }
323354 disabled = { isExecuting || ( ! activeTab . query && ! selectedText . trim ( ) ) }
324- size = "1"
325355 >
326356 { isExecuting ? (
327357 < >
@@ -330,8 +360,9 @@ export function QueryWorkspace({ connectionId, onOpenTableTab }: QueryWorkspaceP
330360 </ >
331361 ) : (
332362 < >
363+ < PlayIcon />
333364 Run
334- < Text size = "1" color = "gray" ml = "1" >
365+ < Text size = "1" ml = "1" style = { { opacity : 0.7 } } >
335366 ⌘↵
336367 </ Text >
337368 </ >
@@ -346,12 +377,15 @@ export function QueryWorkspace({ connectionId, onOpenTableTab }: QueryWorkspaceP
346377 < SqlEditor
347378 connectionId = { connectionId }
348379 value = { activeTab . query }
349- onChange = { ( value ) =>
380+ onChange = { ( value ) => {
350381 handleUpdateTabContent ( activeTab . id , {
351382 query : value || '' ,
352383 isDirty : true
353384 } )
354- }
385+ // Reset limit override when query changes
386+ setQueryLimitOverride ( false )
387+ setShowLimitWarning ( false )
388+ } }
355389 onMount = { handleEditorDidMount }
356390 onSelectionChange = { setSelectedText }
357391 height = "100%"
@@ -391,6 +425,9 @@ export function QueryWorkspace({ connectionId, onOpenTableTab }: QueryWorkspaceP
391425 < >
392426 < Badge size = "1" variant = "soft" >
393427 { activeResult . rowCount || activeResult . data . length } rows
428+ { activeResult . data . length === 1000 &&
429+ ! hasLimitClause ( editorRef . current ?. getValue ( ) || '' ) &&
430+ ' (limited)' }
394431 </ Badge >
395432 { activeResult . executionTime && (
396433 < Badge size = "1" variant = "soft" color = "gray" >
@@ -421,6 +458,45 @@ export function QueryWorkspace({ connectionId, onOpenTableTab }: QueryWorkspaceP
421458 ) }
422459 </ Flex >
423460
461+ { /* Limit warning */ }
462+ { showLimitWarning && activeResult ?. success && (
463+ < Box
464+ p = "2"
465+ style = { {
466+ backgroundColor : 'var(--amber-3)' ,
467+ borderBottom : '1px solid var(--amber-6)'
468+ } }
469+ >
470+ < Flex align = "center" justify = "between" >
471+ < Flex align = "center" gap = "2" >
472+ < ExclamationTriangleIcon color = "var(--amber-11)" />
473+ < Text size = "1" color = "amber" weight = "medium" >
474+ Query limited to { DEFAULT_LIMIT } rows for safety
475+ </ Text >
476+ < Text size = "1" color = "gray" >
477+ Remove LIMIT to see all results
478+ </ Text >
479+ </ Flex >
480+ < Flex gap = "2" align = "center" >
481+ < Button
482+ size = "1"
483+ variant = "soft"
484+ onClick = { ( ) => {
485+ setShowLimitWarning ( false )
486+ setQueryLimitOverride ( true )
487+ handleExecuteQuery ( )
488+ } }
489+ >
490+ Run without limit
491+ </ Button >
492+ < Button size = "1" variant = "ghost" onClick = { ( ) => setShowLimitWarning ( false ) } >
493+ Dismiss
494+ </ Button >
495+ </ Flex >
496+ </ Flex >
497+ </ Box >
498+ ) }
499+
424500 < Box className = "results-content" style = { { flex : 1 } } >
425501 { activeResult ? (
426502 activeResult . success ? (
0 commit comments