Skip to content

Commit fd5ec87

Browse files
Merge pull request #68 from DataPupOrg/feature/button-alignment-fixes
Pagination & Button Alignment
2 parents 3731c96 + f070584 commit fd5ec87

File tree

10 files changed

+410
-23
lines changed

10 files changed

+410
-23
lines changed

src/main/database/clickhouse.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,18 +292,21 @@ class ClickHouseManager extends BaseDatabaseManager {
292292
// ClickHouse uses backticks for identifiers
293293
const qualifiedTable = database ? `\`${database}\`.\`${table}\`` : `\`${table}\``
294294

295-
let sql = `SELECT * FROM ${qualifiedTable}`
295+
let baseQuery = `FROM ${qualifiedTable}`
296296

297297
// Add WHERE clause if filters exist
298298
if (filters && filters.length > 0) {
299299
const whereClauses = filters
300300
.map((filter) => this.buildClickHouseWhereClause(filter))
301301
.filter(Boolean)
302302
if (whereClauses.length > 0) {
303-
sql += ` WHERE ${whereClauses.join(' AND ')}`
303+
baseQuery += ` WHERE ${whereClauses.join(' AND ')}`
304304
}
305305
}
306306

307+
// Build the main SELECT query
308+
let sql = `SELECT * ${baseQuery}`
309+
307310
// Add ORDER BY clause
308311
if (orderBy && orderBy.length > 0) {
309312
const orderClauses = orderBy.map((o) => `\`${o.column}\` ${o.direction.toUpperCase()}`)
@@ -318,7 +321,26 @@ class ClickHouseManager extends BaseDatabaseManager {
318321
sql += ` OFFSET ${offset}`
319322
}
320323

321-
return this.query(connectionId, sql, sessionId)
324+
// Execute the main query
325+
const result = await this.query(connectionId, sql, sessionId)
326+
327+
// If successful and we have pagination, get the total count
328+
if (result.success && (limit || offset)) {
329+
try {
330+
const countSql = `SELECT count() as total ${baseQuery}`
331+
const countResult = await this.query(connectionId, countSql)
332+
333+
if (countResult.success && countResult.data && countResult.data[0]) {
334+
result.totalRows = Number(countResult.data[0].total)
335+
result.hasMore = offset + (result.data?.length || 0) < result.totalRows
336+
}
337+
} catch (error) {
338+
// If count fails, continue without it
339+
console.warn('Failed to get total count:', error)
340+
}
341+
}
342+
343+
return result
322344
}
323345

324346
private buildClickHouseWhereClause(filter: TableFilter): string {

src/main/database/interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export interface QueryResult {
4545
affectedRows?: number
4646
isDDL?: boolean
4747
isDML?: boolean
48+
totalRows?: number // Total rows available (for pagination)
49+
hasMore?: boolean // Indicates if there are more rows beyond current result set
4850
}
4951

5052
export interface InsertResult extends QueryResult {

src/renderer/components/QueryWorkspace/QueryWorkspace.tsx

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,20 @@ import { QueryTabs } from '../QueryTabs/QueryTabs'
77
import { TableView } from '../TableView/TableView'
88
import { AIAssistant } from '../AIAssistant'
99
import { SqlEditor } from './SqlEditor'
10+
import { ExclamationTriangleIcon, MagicWandIcon, CodeIcon, PlayIcon } from '@radix-ui/react-icons'
1011

1112
import { exportToCSV, exportToJSON } from '../../utils/exportData'
1213
import { Tab, QueryTab, TableTab, QueryExecutionResult } from '../../types/tabs'
14+
import {
15+
isSelectQuery,
16+
hasLimitClause,
17+
addLimitToQuery,
18+
mightReturnLargeResultSet
19+
} from '../../utils/queryParser'
1320
import './QueryWorkspace.css'
1421

22+
const DEFAULT_LIMIT = 100
23+
1524
interface 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

Comments
 (0)