Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 25 additions & 54 deletions src/main/database/clickhouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
TableQueryOptions,
TableFilter
} from './interface'
import { PaginationQueryBuilder, DatabaseAdapter } from '../utils/pagination'

interface ClickHouseConfig {
host: string
Expand All @@ -30,9 +31,10 @@ interface ClickHouseConnection {
lastUsed: Date
}

class ClickHouseManager extends BaseDatabaseManager {
class ClickHouseManager extends BaseDatabaseManager implements DatabaseAdapter {
protected connections: Map<string, ClickHouseConnection> = new Map()
private activeQueries: Map<string, AbortController> = new Map() // Track active queries by queryId
private paginationBuilder = new PaginationQueryBuilder(this)

async connect(config: DatabaseConfig, connectionId: string): Promise<ConnectionResult> {
try {
Expand Down Expand Up @@ -281,66 +283,35 @@ class ClickHouseManager extends BaseDatabaseManager {
}
}

// DatabaseAdapter interface methods
escapeIdentifier(identifier: string): string {
return `\`${identifier}\``
}

escapeValue = this.escapeClickHouseValue.bind(this)

buildWhereClause = this.buildClickHouseWhereClause.bind(this)

getCountExpression(): string {
return 'count()'
}

async queryTable(
connectionId: string,
options: TableQueryOptions,
sessionId?: string
): Promise<QueryResult> {
// ClickHouse-specific implementation
const { database, table, filters, orderBy, limit, offset } = options

const { database, table } = options

// ClickHouse uses backticks for identifiers
const qualifiedTable = database ? `\`${database}\`.\`${table}\`` : `\`${table}\``

let baseQuery = `FROM ${qualifiedTable}`

// Add WHERE clause if filters exist
if (filters && filters.length > 0) {
const whereClauses = filters
.map((filter) => this.buildClickHouseWhereClause(filter))
.filter(Boolean)
if (whereClauses.length > 0) {
baseQuery += ` WHERE ${whereClauses.join(' AND ')}`
}
}

// Build the main SELECT query
let sql = `SELECT * ${baseQuery}`

// Add ORDER BY clause
if (orderBy && orderBy.length > 0) {
const orderClauses = orderBy.map((o) => `\`${o.column}\` ${o.direction.toUpperCase()}`)
sql += ` ORDER BY ${orderClauses.join(', ')}`
}

// Add LIMIT and OFFSET
if (limit) {
sql += ` LIMIT ${limit}`
}
if (offset) {
sql += ` OFFSET ${offset}`
}

// Execute the main query
const result = await this.query(connectionId, sql, sessionId)

// If successful and we have pagination, get the total count
if (result.success && (limit || offset)) {
try {
const countSql = `SELECT count() as total ${baseQuery}`
const countResult = await this.query(connectionId, countSql)

if (countResult.success && countResult.data && countResult.data[0]) {
result.totalRows = Number(countResult.data[0].total)
result.hasMore = offset + (result.data?.length || 0) < result.totalRows
}
} catch (error) {
// If count fails, continue without it
console.warn('Failed to get total count:', error)
}
}

return result

return this.paginationBuilder.buildPaginatedQuery(
connectionId,
options,
qualifiedTable,
sessionId
)
}

private buildClickHouseWhereClause(filter: TableFilter): string {
Expand Down
81 changes: 18 additions & 63 deletions src/main/database/postgresql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
UpdateResult,
DeleteResult
} from './interface'
import { PaginationQueryBuilder, DatabaseAdapter } from '../utils/pagination'

interface PostgreSQLConfig {
host: string
Expand All @@ -33,8 +34,9 @@ interface PostgreSQLConnection {
lastUsed: Date
}

class PostgreSQLManager extends BaseDatabaseManager {
class PostgreSQLManager extends BaseDatabaseManager implements DatabaseAdapter {
protected connections: Map<string, PostgreSQLConnection> = new Map()
private paginationBuilder = new PaginationQueryBuilder(this)

async connect(config: DatabaseConfig, connectionId: string): Promise<ConnectionResult> {
try {
Expand Down Expand Up @@ -362,73 +364,26 @@ class PostgreSQLManager extends BaseDatabaseManager {
options: TableQueryOptions,
sessionId?: string
): Promise<QueryResult> {
console.log('PostgreSQL queryTable called with options:', options)
const { database, table, filters, orderBy, limit, offset } = options

// In PostgreSQL, ignore the 'database' parameter and always use 'public' schema
// The database is already selected in the connection, tables are in schemas
const { table } = options

// PostgreSQL uses double quotes and public schema
const schema = 'public'
console.log('PostgreSQL queryTable - database param:', database, 'using schema:', schema, 'table:', table)
const qualifiedTable = `${this.escapeIdentifier(schema)}.${this.escapeIdentifier(table)}`

let sql = `SELECT * FROM ${qualifiedTable}`

// Add WHERE clause if filters exist
if (filters && filters.length > 0) {
const whereClauses = filters.map((filter) => this.buildWhereClause(filter)).filter(Boolean)
if (whereClauses.length > 0) {
sql += ` WHERE ${whereClauses.join(' AND ')}`
}
}

// Add ORDER BY clause
if (orderBy && orderBy.length > 0) {
const orderClauses = orderBy.map((o) => `${this.escapeIdentifier(o.column)} ${o.direction.toUpperCase()}`)
sql += ` ORDER BY ${orderClauses.join(', ')}`
}

// Add LIMIT and OFFSET
if (limit) {
sql += ` LIMIT ${limit}`
}
if (offset) {
sql += ` OFFSET ${offset}`
}

console.log('PostgreSQL queryTable SQL:', sql)

// Execute the main query
const result = await this.query(connectionId, sql, sessionId)

// If successful and we have pagination, get the total count
if (result.success && (limit || offset)) {
try {
// Build count query without LIMIT/OFFSET
let countSql = `SELECT COUNT(*) as total FROM ${qualifiedTable}`

// Add WHERE clause if filters exist (same as main query)
if (filters && filters.length > 0) {
const whereClauses = filters.map((filter) => this.buildWhereClause(filter)).filter(Boolean)
if (whereClauses.length > 0) {
countSql += ` WHERE ${whereClauses.join(' AND ')}`
}
}

const countResult = await this.query(connectionId, countSql)

if (countResult.success && countResult.data && countResult.data[0]) {
result.totalRows = Number(countResult.data[0].total)
result.hasMore = (offset || 0) + (result.data?.length || 0) < result.totalRows
}
} catch (error) {
// If count fails, continue without it
console.warn('Failed to get total count:', error)
}
}

return result
return this.paginationBuilder.buildPaginatedQuery(
connectionId,
options,
qualifiedTable,
sessionId
)
}

// DatabaseAdapter interface methods (already implemented above):
// - escapeIdentifier() and escapeValue() methods
// - buildWhereClause() method below
// - query() method inherited from BaseDatabaseManager
// PostgreSQL uses COUNT(*) by default, so no getCountExpression() needed

protected buildWhereClause(filter: TableFilter): string {
const { column, operator, value } = filter
const escapedColumn = this.escapeIdentifier(column)
Expand Down
75 changes: 75 additions & 0 deletions src/main/utils/pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { TableQueryOptions, TableFilter, QueryResult } from '../database/interface'

export interface DatabaseAdapter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this? If yes, can we add this near BaseDatabaseManager as interface IBaseDatabaseManager?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, we need it - it defines the contract for the pagination utility to work with different database implementations. I would keep it here rather than near BaseDatabaseManager since it's pagination-specific.

escapeIdentifier(identifier: string): string
escapeValue(value: any): string
buildWhereClause(filter: TableFilter): string
query(connectionId: string, sql: string, sessionId?: string): Promise<QueryResult>
getCountExpression?(): string // Optional method for database-specific count syntax
}

export class PaginationQueryBuilder {
constructor(private adapter: DatabaseAdapter) {}

async buildPaginatedQuery(
connectionId: string,
options: TableQueryOptions,
qualifiedTable: string,
sessionId?: string
): Promise<QueryResult> {
const { filters, orderBy, limit, offset } = options

let baseQuery = `FROM ${qualifiedTable}`

// Add WHERE clause if filters exist
if (filters && filters.length > 0) {
const whereClauses = filters
.map((filter: TableFilter) => this.adapter.buildWhereClause(filter))
.filter(Boolean)
if (whereClauses.length > 0) {
baseQuery += ` WHERE ${whereClauses.join(' AND ')}`
}
}

// Build the main SELECT query
let sql = `SELECT * ${baseQuery}`

// Add ORDER BY clause
if (orderBy && orderBy.length > 0) {
const orderClauses = orderBy.map((o: { column: string; direction: 'asc' | 'desc' }) =>
`${this.adapter.escapeIdentifier(o.column)} ${o.direction.toUpperCase()}`
)
sql += ` ORDER BY ${orderClauses.join(', ')}`
}

// Add LIMIT and OFFSET
if (limit) {
sql += ` LIMIT ${limit}`
}
if (offset) {
sql += ` OFFSET ${offset}`
}

// Execute the main query
const result = await this.adapter.query(connectionId, sql, sessionId)

// If successful and we have pagination, get the total count
if (result.success && (limit || offset)) {
try {
const countExpression = this.adapter.getCountExpression?.() || 'COUNT(*)'
const countSql = `SELECT ${countExpression} as total ${baseQuery}`
const countResult = await this.adapter.query(connectionId, countSql)

if (countResult.success && countResult.data && countResult.data[0]) {
result.totalRows = Number(countResult.data[0].total)
result.hasMore = (offset || 0) + (result.data?.length || 0) < result.totalRows
}
} catch (error) {
// If count fails, continue without it
console.warn('Failed to get total count:', error)
}
}

return result
}
}