Skip to content

Commit db65ac1

Browse files
Merge pull request #66 from DataPupOrg/refactor/database-agnostic-filtering
refactor: move SQL query building from UI to database adapters
2 parents 7bbeb5b + a69ddae commit db65ac1

File tree

9 files changed

+280
-28
lines changed

9 files changed

+280
-28
lines changed

src/main/database/base.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import {
1111
DatabaseCapabilities,
1212
TransactionHandle,
1313
BulkOperation,
14-
BulkOperationResult
14+
BulkOperationResult,
15+
TableQueryOptions,
16+
TableFilter
1517
} from './interface'
1618

1719
export abstract class BaseDatabaseManager implements DatabaseManagerInterface {
@@ -108,6 +110,81 @@ export abstract class BaseDatabaseManager implements DatabaseManagerInterface {
108110
}
109111
}
110112

113+
async queryTable(
114+
connectionId: string,
115+
options: TableQueryOptions,
116+
sessionId?: string
117+
): Promise<QueryResult> {
118+
// Default implementation - build a basic SQL query
119+
// This can be overridden by specific database implementations
120+
const { database, table, filters, orderBy, limit, offset } = options
121+
122+
// Escape identifiers (basic implementation - should be overridden)
123+
const qualifiedTable = database ? `"${database}"."${table}"` : `"${table}"`
124+
125+
let sql = `SELECT * FROM ${qualifiedTable}`
126+
127+
// Add WHERE clause if filters exist
128+
if (filters && filters.length > 0) {
129+
const whereClauses = filters.map((filter) => this.buildWhereClause(filter)).filter(Boolean)
130+
if (whereClauses.length > 0) {
131+
sql += ` WHERE ${whereClauses.join(' AND ')}`
132+
}
133+
}
134+
135+
// Add ORDER BY clause
136+
if (orderBy && orderBy.length > 0) {
137+
const orderClauses = orderBy.map((o) => `"${o.column}" ${o.direction.toUpperCase()}`)
138+
sql += ` ORDER BY ${orderClauses.join(', ')}`
139+
}
140+
141+
// Add LIMIT and OFFSET
142+
if (limit) {
143+
sql += ` LIMIT ${limit}`
144+
}
145+
if (offset) {
146+
sql += ` OFFSET ${offset}`
147+
}
148+
149+
return this.query(connectionId, sql, sessionId)
150+
}
151+
152+
protected buildWhereClause(filter: TableFilter): string {
153+
const { column, operator, value } = filter
154+
155+
// Handle NULL operators
156+
if (operator === 'IS NULL' || operator === 'IS NOT NULL') {
157+
return `"${column}" ${operator}`
158+
}
159+
160+
// Handle IN and NOT IN operators
161+
if ((operator === 'IN' || operator === 'NOT IN') && Array.isArray(value)) {
162+
const values = value.map((v) => this.escapeValue(v)).join(', ')
163+
return `"${column}" ${operator} (${values})`
164+
}
165+
166+
// Handle LIKE operators
167+
if (operator === 'LIKE' || operator === 'NOT LIKE') {
168+
return `"${column}" ${operator} ${this.escapeValue(`%${value}%`)}`
169+
}
170+
171+
// Handle other operators
172+
if (value !== undefined && value !== null) {
173+
return `"${column}" ${operator} ${this.escapeValue(value)}`
174+
}
175+
176+
return ''
177+
}
178+
179+
protected escapeValue(value: any): string {
180+
if (value === null || value === undefined) return 'NULL'
181+
if (typeof value === 'string') return `'${value.replace(/'/g, "''")}'`
182+
if (typeof value === 'number') return value.toString()
183+
if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE'
184+
if (value instanceof Date) return `'${value.toISOString()}'`
185+
return `'${String(value).replace(/'/g, "''")}'`
186+
}
187+
111188
async insertRow(
112189
connectionId: string,
113190
table: string,

src/main/database/clickhouse.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import {
66
QueryType,
77
DatabaseCapabilities,
88
TableSchema,
9-
ColumnSchema
9+
ColumnSchema,
10+
TableQueryOptions,
11+
TableFilter
1012
} from './interface'
1113

1214
interface ClickHouseConfig {
@@ -279,6 +281,82 @@ class ClickHouseManager extends BaseDatabaseManager {
279281
}
280282
}
281283

284+
async queryTable(
285+
connectionId: string,
286+
options: TableQueryOptions,
287+
sessionId?: string
288+
): Promise<QueryResult> {
289+
// ClickHouse-specific implementation
290+
const { database, table, filters, orderBy, limit, offset } = options
291+
292+
// ClickHouse uses backticks for identifiers
293+
const qualifiedTable = database ? `\`${database}\`.\`${table}\`` : `\`${table}\``
294+
295+
let sql = `SELECT * FROM ${qualifiedTable}`
296+
297+
// Add WHERE clause if filters exist
298+
if (filters && filters.length > 0) {
299+
const whereClauses = filters
300+
.map((filter) => this.buildClickHouseWhereClause(filter))
301+
.filter(Boolean)
302+
if (whereClauses.length > 0) {
303+
sql += ` WHERE ${whereClauses.join(' AND ')}`
304+
}
305+
}
306+
307+
// Add ORDER BY clause
308+
if (orderBy && orderBy.length > 0) {
309+
const orderClauses = orderBy.map((o) => `\`${o.column}\` ${o.direction.toUpperCase()}`)
310+
sql += ` ORDER BY ${orderClauses.join(', ')}`
311+
}
312+
313+
// Add LIMIT and OFFSET
314+
if (limit) {
315+
sql += ` LIMIT ${limit}`
316+
}
317+
if (offset) {
318+
sql += ` OFFSET ${offset}`
319+
}
320+
321+
return this.query(connectionId, sql, sessionId)
322+
}
323+
324+
private buildClickHouseWhereClause(filter: TableFilter): string {
325+
const { column, operator, value } = filter
326+
327+
// Handle NULL operators
328+
if (operator === 'IS NULL' || operator === 'IS NOT NULL') {
329+
return `\`${column}\` ${operator}`
330+
}
331+
332+
// Handle IN and NOT IN operators
333+
if ((operator === 'IN' || operator === 'NOT IN') && Array.isArray(value)) {
334+
const values = value.map((v) => this.escapeClickHouseValue(v)).join(', ')
335+
return `\`${column}\` ${operator} (${values})`
336+
}
337+
338+
// Handle LIKE operators - ClickHouse is case-sensitive by default
339+
if (operator === 'LIKE' || operator === 'NOT LIKE') {
340+
return `\`${column}\` ${operator} ${this.escapeClickHouseValue(`%${value}%`)}`
341+
}
342+
343+
// Handle other operators
344+
if (value !== undefined && value !== null) {
345+
return `\`${column}\` ${operator} ${this.escapeClickHouseValue(value)}`
346+
}
347+
348+
return ''
349+
}
350+
351+
private escapeClickHouseValue(value: any): string {
352+
if (value === null || value === undefined) return 'NULL'
353+
if (typeof value === 'string') return `'${value.replace(/'/g, "\\'")}'`
354+
if (typeof value === 'number') return value.toString()
355+
if (typeof value === 'boolean') return value ? '1' : '0'
356+
if (value instanceof Date) return `'${value.toISOString()}'`
357+
return `'${String(value).replace(/'/g, "\\'")}'`
358+
}
359+
282360
async getDatabases(
283361
connectionId: string
284362
): Promise<{ success: boolean; databases?: string[]; message: string }> {

src/main/database/interface.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,33 @@ export interface BulkOperationResult {
9898
data?: any[] // Updated rows after operations
9999
}
100100

101+
export interface TableFilter {
102+
column: string
103+
operator:
104+
| '='
105+
| '!='
106+
| '>'
107+
| '<'
108+
| '>='
109+
| '<='
110+
| 'LIKE'
111+
| 'NOT LIKE'
112+
| 'IN'
113+
| 'NOT IN'
114+
| 'IS NULL'
115+
| 'IS NOT NULL'
116+
value?: string | string[] | number | number[]
117+
}
118+
119+
export interface TableQueryOptions {
120+
database: string
121+
table: string
122+
filters?: TableFilter[]
123+
orderBy?: Array<{ column: string; direction: 'asc' | 'desc' }>
124+
limit?: number
125+
offset?: number
126+
}
127+
101128
export interface DatabaseManagerInterface {
102129
// Connection management
103130
connect(config: DatabaseConfig, connectionId: string): Promise<ConnectionResult>
@@ -109,6 +136,13 @@ export interface DatabaseManagerInterface {
109136
query(connectionId: string, sql: string, sessionId?: string): Promise<QueryResult>
110137
cancelQuery(connectionId: string, queryId: string): Promise<{ success: boolean; message: string }>
111138

139+
// Table query with filters
140+
queryTable(
141+
connectionId: string,
142+
options: TableQueryOptions,
143+
sessionId?: string
144+
): Promise<QueryResult>
145+
112146
// CRUD operations
113147
insertRow(
114148
connectionId: string,

src/main/database/manager.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
DatabaseCapabilities,
1111
TransactionHandle,
1212
BulkOperation,
13-
BulkOperationResult
13+
BulkOperationResult,
14+
TableQueryOptions
1415
} from './interface'
1516
import { DatabaseManagerFactory } from './factory'
1617

@@ -187,6 +188,31 @@ class DatabaseManager {
187188
}
188189
}
189190

191+
async queryTable(
192+
connectionId: string,
193+
options: TableQueryOptions,
194+
sessionId?: string
195+
): Promise<QueryResult> {
196+
try {
197+
if (!this.activeConnection || this.activeConnection.id !== connectionId) {
198+
return {
199+
success: false,
200+
message: 'Connection not found. Please connect first.',
201+
error: 'No active connection'
202+
}
203+
}
204+
205+
return await this.activeConnection.manager.queryTable(connectionId, options, sessionId)
206+
} catch (error) {
207+
console.error('Table query error:', error)
208+
return {
209+
success: false,
210+
message: 'Table query execution failed',
211+
error: error instanceof Error ? error.message : 'Unknown error'
212+
}
213+
}
214+
}
215+
190216
async getDatabases(
191217
connectionId: string
192218
): Promise<{ success: boolean; databases?: string[]; message: string }> {

src/main/index.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { join } from 'path'
33
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
44
import { SecureStorage, DatabaseConnection } from './secureStorage'
55
import { DatabaseManager } from './database/manager'
6-
import { DatabaseConfig } from './database/interface'
6+
import { DatabaseConfig, TableQueryOptions } from './database/interface'
77
import { LangChainAgent } from './llm/langchainAgent'
88
import * as fs from 'fs'
99

@@ -189,6 +189,23 @@ ipcMain.handle('db:query', async (_, connectionId: string, query: string, sessio
189189
}
190190
})
191191

192+
ipcMain.handle(
193+
'db:queryTable',
194+
async (_, connectionId: string, options: TableQueryOptions, sessionId?: string) => {
195+
try {
196+
const result = await databaseManager.queryTable(connectionId, options, sessionId)
197+
return result
198+
} catch (error) {
199+
console.error('Table query execution error:', error)
200+
return {
201+
success: false,
202+
message: 'Table query execution failed',
203+
error: error instanceof Error ? error.message : 'Unknown error'
204+
}
205+
}
206+
}
207+
)
208+
192209
ipcMain.handle('db:cancelQuery', async (_, connectionId: string, queryId: string) => {
193210
try {
194211
const result = await databaseManager.cancelQuery(connectionId, queryId)

src/preload/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const api = {
99
disconnect: (connectionId?: string) => ipcRenderer.invoke('db:disconnect', connectionId),
1010
query: (connectionId: string, sql: string, sessionId?: string) =>
1111
ipcRenderer.invoke('db:query', connectionId, sql, sessionId),
12+
queryTable: (connectionId: string, options: any, sessionId?: string) =>
13+
ipcRenderer.invoke('db:queryTable', connectionId, options, sessionId),
1214
cancelQuery: (connectionId: string, queryId: string) =>
1315
ipcRenderer.invoke('db:cancelQuery', connectionId, queryId),
1416
getDatabases: (connectionId: string) => ipcRenderer.invoke('db:getDatabases', connectionId),

src/renderer/components/TableView/TableView.tsx

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -175,43 +175,32 @@ export function TableView({ connectionId, database, tableName, onFiltersChange }
175175
}
176176
}
177177

178-
const buildQuery = () => {
179-
let query = `SELECT * FROM ${database}.${tableName}`
180-
181-
const validFilters = filters.filter(
178+
const getValidFilters = () => {
179+
return filters.filter(
182180
(f) =>
183181
f.column &&
184182
f.operator &&
185183
(f.value || f.operator === 'IS NULL' || f.operator === 'IS NOT NULL')
186184
)
187-
188-
if (validFilters.length > 0) {
189-
const whereClauses = validFilters.map((filter) => {
190-
if (filter.operator === 'IS NULL' || filter.operator === 'IS NOT NULL') {
191-
return `${filter.column} ${filter.operator}`
192-
} else if (filter.operator === 'LIKE' || filter.operator === 'NOT LIKE') {
193-
return `${filter.column} ${filter.operator} '%${filter.value}%'`
194-
} else {
195-
// Check if value is numeric
196-
const isNumeric = !isNaN(Number(filter.value))
197-
return `${filter.column} ${filter.operator} ${isNumeric ? filter.value : `'${filter.value}'`}`
198-
}
199-
})
200-
query += ` WHERE ${whereClauses.join(' AND ')}`
201-
}
202-
203-
query += ' LIMIT 100'
204-
return query
205185
}
206186

207187
const executeQuery = async () => {
208188
try {
209189
setIsLoading(true)
210190
const startTime = Date.now()
211-
const query = buildQuery()
191+
const validFilters = getValidFilters()
212192

213193
const sessionId = uuidv4()
214-
const queryResult = await window.api.database.query(connectionId, query, sessionId)
194+
const queryResult = await window.api.database.queryTable(
195+
connectionId,
196+
{
197+
database,
198+
table: tableName,
199+
filters: validFilters,
200+
limit: 100
201+
},
202+
sessionId
203+
)
215204
const executionTime = Date.now() - startTime
216205

217206
setResult({

0 commit comments

Comments
 (0)