From 68f958a5f0848b48df355f31a67ab12e17a15b0b Mon Sep 17 00:00:00 2001 From: sathvik-palley Date: Thu, 14 Aug 2025 20:17:51 -0500 Subject: [PATCH] Add shared pagination utility --- src/main/database/clickhouse.ts | 79 ++++++++++---------------------- src/main/database/postgresql.ts | 81 ++++++++------------------------- src/main/utils/pagination.ts | 75 ++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 117 deletions(-) create mode 100644 src/main/utils/pagination.ts diff --git a/src/main/database/clickhouse.ts b/src/main/database/clickhouse.ts index 209ad22..99d08ce 100644 --- a/src/main/database/clickhouse.ts +++ b/src/main/database/clickhouse.ts @@ -10,6 +10,7 @@ import { TableQueryOptions, TableFilter } from './interface' +import { PaginationQueryBuilder, DatabaseAdapter } from '../utils/pagination' interface ClickHouseConfig { host: string @@ -30,9 +31,10 @@ interface ClickHouseConnection { lastUsed: Date } -class ClickHouseManager extends BaseDatabaseManager { +class ClickHouseManager extends BaseDatabaseManager implements DatabaseAdapter { protected connections: Map = new Map() private activeQueries: Map = new Map() // Track active queries by queryId + private paginationBuilder = new PaginationQueryBuilder(this) async connect(config: DatabaseConfig, connectionId: string): Promise { try { @@ -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 { - // 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 { diff --git a/src/main/database/postgresql.ts b/src/main/database/postgresql.ts index 82fc2a6..d451615 100644 --- a/src/main/database/postgresql.ts +++ b/src/main/database/postgresql.ts @@ -13,6 +13,7 @@ import { UpdateResult, DeleteResult } from './interface' +import { PaginationQueryBuilder, DatabaseAdapter } from '../utils/pagination' interface PostgreSQLConfig { host: string @@ -33,8 +34,9 @@ interface PostgreSQLConnection { lastUsed: Date } -class PostgreSQLManager extends BaseDatabaseManager { +class PostgreSQLManager extends BaseDatabaseManager implements DatabaseAdapter { protected connections: Map = new Map() + private paginationBuilder = new PaginationQueryBuilder(this) async connect(config: DatabaseConfig, connectionId: string): Promise { try { @@ -362,73 +364,26 @@ class PostgreSQLManager extends BaseDatabaseManager { options: TableQueryOptions, sessionId?: string ): Promise { - 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) diff --git a/src/main/utils/pagination.ts b/src/main/utils/pagination.ts new file mode 100644 index 0000000..3ff8d83 --- /dev/null +++ b/src/main/utils/pagination.ts @@ -0,0 +1,75 @@ +import { TableQueryOptions, TableFilter, QueryResult } from '../database/interface' + +export interface DatabaseAdapter { + escapeIdentifier(identifier: string): string + escapeValue(value: any): string + buildWhereClause(filter: TableFilter): string + query(connectionId: string, sql: string, sessionId?: string): Promise + 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 { + 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 + } +} \ No newline at end of file