Skip to content

Commit eece936

Browse files
feat: Enhance caching mechanism with session-aware queries and improved logging
1 parent 3c35e0f commit eece936

File tree

6 files changed

+256
-15
lines changed

6 files changed

+256
-15
lines changed

src/main/cache/CacheManager.ts

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,32 @@ export class CacheManager {
2727
async getCachedResult(
2828
query: string,
2929
connectionId: string,
30-
database?: string
30+
database?: string,
31+
sessionId?: string
3132
): Promise<QueryResult | null> {
3233
if (!this.enabled || !QueryNormalizer.shouldCache(query)) {
3334
return null
3435
}
3536

36-
const cacheKey = QueryNormalizer.generateCacheKey(query, connectionId, database)
37-
return await this.queryCache.get(cacheKey)
37+
const cacheKey = QueryNormalizer.generateCacheKey(query, connectionId, database, sessionId)
38+
const isSessionIndependent = QueryNormalizer.isSessionIndependentQuery(query)
39+
40+
const result = await this.queryCache.get(cacheKey)
41+
42+
// Enhanced logging for cache strategy monitoring
43+
if (result) {
44+
console.log(
45+
`[CACHE HIT] ${isSessionIndependent ? 'Session-Independent' : 'Session-Dependent'} query cache hit`,
46+
{
47+
queryType: QueryNormalizer.normalize(query).queryType,
48+
isSessionIndependent,
49+
sessionId: isSessionIndependent ? 'N/A' : sessionId,
50+
cacheKey: cacheKey.substring(0, 16) + '...'
51+
}
52+
)
53+
}
54+
55+
return result
3856
}
3957

4058
/**
@@ -44,7 +62,8 @@ export class CacheManager {
4462
query: string,
4563
result: QueryResult,
4664
connectionId: string,
47-
database?: string
65+
database?: string,
66+
sessionId?: string
4867
): Promise<void> {
4968
if (!this.enabled || !QueryNormalizer.shouldCache(query)) {
5069
return
@@ -56,7 +75,8 @@ export class CacheManager {
5675
}
5776

5877
const normalizedQuery = QueryNormalizer.normalize(query)
59-
const cacheKey = QueryNormalizer.generateCacheKey(query, connectionId, database)
78+
const cacheKey = QueryNormalizer.generateCacheKey(query, connectionId, database, sessionId)
79+
const isSessionIndependent = QueryNormalizer.isSessionIndependentQuery(query)
6080

6181
const metadata: CacheMetadata = {
6282
connectionId,
@@ -65,10 +85,24 @@ export class CacheManager {
6585
hitCount: 0,
6686
lastAccessed: Date.now(),
6787
tables: normalizedQuery.tables,
68-
queryType: normalizedQuery.queryType
88+
queryType: normalizedQuery.queryType,
89+
sessionId: isSessionIndependent ? undefined : sessionId,
90+
isSessionIndependent
6991
}
7092

7193
await this.queryCache.set(cacheKey, result, metadata)
94+
95+
// Enhanced logging for cache strategy monitoring
96+
console.log(
97+
`[CACHE STORE] ${isSessionIndependent ? 'Session-Independent' : 'Session-Dependent'} query cached`,
98+
{
99+
queryType: normalizedQuery.queryType,
100+
isSessionIndependent,
101+
sessionId: isSessionIndependent ? 'N/A' : sessionId,
102+
tables: normalizedQuery.tables,
103+
cacheKey: cacheKey.substring(0, 16) + '...'
104+
}
105+
)
72106
}
73107

74108
/**
@@ -163,14 +197,21 @@ export class CacheManager {
163197
/**
164198
* Get cache entry details for debugging
165199
*/
166-
getCacheEntryInfo(query: string, connectionId: string, database?: string): any {
167-
const cacheKey = QueryNormalizer.generateCacheKey(query, connectionId, database)
200+
getCacheEntryInfo(
201+
query: string,
202+
connectionId: string,
203+
database?: string,
204+
sessionId?: string
205+
): any {
206+
const cacheKey = QueryNormalizer.generateCacheKey(query, connectionId, database, sessionId)
168207
const normalizedQuery = QueryNormalizer.normalize(query)
208+
const isSessionIndependent = QueryNormalizer.isSessionIndependentQuery(query)
169209

170210
return {
171211
cacheKey,
172212
normalizedQuery,
173213
shouldCache: QueryNormalizer.shouldCache(query),
214+
isSessionIndependent,
174215
isEnabled: this.enabled
175216
}
176217
}

src/main/cache/QueryNormalizer.ts

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,96 @@ export class QueryNormalizer {
3434
}
3535
}
3636

37+
/**
38+
* Check if a query can be cached session-independently
39+
*/
40+
static isSessionIndependentQuery(query: string): boolean {
41+
const normalized = this.normalize(query)
42+
43+
// Only SELECT queries can be session-independent
44+
if (normalized.queryType !== 'SELECT') {
45+
return false
46+
}
47+
48+
const upperQuery = normalized.normalized
49+
50+
// Session-dependent patterns to exclude
51+
const sessionDependentPatterns = [
52+
// User or session-specific functions
53+
'USER()',
54+
'CURRENT_USER',
55+
'SESSION_USER',
56+
'CONNECTION_ID()',
57+
'LAST_INSERT_ID()',
58+
59+
// Temporary tables
60+
'#',
61+
'TEMPORARY',
62+
63+
// Variables
64+
'@',
65+
'SET @',
66+
'SELECT @',
67+
68+
// Time-sensitive functions (already covered in shouldCache)
69+
'NOW()',
70+
'CURRENT_TIMESTAMP',
71+
'CURRENT_DATE',
72+
'CURRENT_TIME',
73+
'RAND()',
74+
'RANDOM()',
75+
'UUID()',
76+
'NEWID()'
77+
]
78+
79+
// Check for session-dependent patterns
80+
const hasSessionDependentContent = sessionDependentPatterns.some((pattern) =>
81+
upperQuery.includes(pattern)
82+
)
83+
84+
if (hasSessionDependentContent) {
85+
return false
86+
}
87+
88+
// Check for table browsing patterns (simple SELECT * with basic conditions)
89+
const isTableBrowsing =
90+
// Simple SELECT * queries
91+
(upperQuery.match(/^SELECT \* FROM/) ||
92+
// SELECT with specific columns from single table
93+
upperQuery.match(/^SELECT [^()]+FROM [^\s,;()]+$/)) &&
94+
// With basic WHERE conditions (no subqueries or complex joins)
95+
(!upperQuery.includes('(') || upperQuery.match(/WHERE [^()]+$/)) &&
96+
// No complex operations
97+
!upperQuery.includes('JOIN') &&
98+
!upperQuery.includes('UNION') &&
99+
!upperQuery.includes('CASE') &&
100+
!upperQuery.includes('EXISTS')
101+
102+
return isTableBrowsing
103+
}
104+
37105
/**
38106
* Generate a cache key from query and connection parameters
39107
*/
40-
static generateCacheKey(query: string, connectionId: string, database?: string): string {
108+
static generateCacheKey(
109+
query: string,
110+
connectionId: string,
111+
database?: string,
112+
sessionId?: string
113+
): string {
41114
const normalized = this.normalize(query)
42-
const contextData = `${connectionId}:${database || 'default'}`
115+
116+
// Check if this query can be cached session-independently
117+
const isSessionIndependent = this.isSessionIndependentQuery(query)
118+
119+
let contextData: string
120+
if (isSessionIndependent) {
121+
// For session-independent queries, exclude sessionId from cache key
122+
contextData = `${connectionId}:${database || 'default'}`
123+
} else {
124+
// For session-dependent queries, include sessionId
125+
contextData = `${connectionId}:${database || 'default'}:${sessionId || 'default'}`
126+
}
43127

44128
return crypto
45129
.createHash('sha256')

src/main/cache/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export interface CacheMetadata {
1919
lastAccessed: number
2020
tables: string[]
2121
queryType: 'SELECT' | 'DDL' | 'DML' | 'OTHER'
22+
sessionId?: string
23+
isSessionIndependent?: boolean
2224
}
2325

2426
export interface CacheConfig {

src/main/database/base.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,12 @@ export abstract class BaseDatabaseManager implements DatabaseManagerInterface {
113113
const database = connectionInfo?.database
114114

115115
// Try to get cached result first
116-
const cachedResult = await this.cacheManager.getCachedResult(sql, connectionId, database)
116+
const cachedResult = await this.cacheManager.getCachedResult(
117+
sql,
118+
connectionId,
119+
database,
120+
sessionId
121+
)
117122
if (cachedResult) {
118123
console.log('Cache hit for query:', sql.substring(0, 100) + '...')
119124
return cachedResult
@@ -126,7 +131,7 @@ export abstract class BaseDatabaseManager implements DatabaseManagerInterface {
126131

127132
// Cache the result if successful
128133
if (result.success) {
129-
await this.cacheManager.cacheResult(sql, result, connectionId, database)
134+
await this.cacheManager.cacheResult(sql, result, connectionId, database, sessionId)
130135
}
131136

132137
// Handle cache invalidation for DDL/DML queries

src/main/database/clickhouse.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
TableQueryOptions,
1111
TableFilter
1212
} from './interface'
13+
import { logger } from '../utils/logger'
1314

1415
interface ClickHouseConfig {
1516
host: string
@@ -166,6 +167,9 @@ class ClickHouseManager extends BaseDatabaseManager {
166167
}
167168

168169
async executeQuery(connectionId: string, sql: string, sessionId?: string): Promise<QueryResult> {
170+
const startTime = Date.now()
171+
const timestamp = new Date().toISOString()
172+
169173
try {
170174
const connection = this.connections.get(connectionId)
171175
if (!connection || !connection.isConnected) {
@@ -175,6 +179,12 @@ class ClickHouseManager extends BaseDatabaseManager {
175179
// Validate read-only queries
176180
const validation = this.validateReadOnlyQuery(connectionId, sql)
177181
if (!validation.valid) {
182+
logger.warn(`[${timestamp}] ClickHouse read-only query validation failed`, {
183+
connectionId,
184+
sql: sql.slice(0, 100) + (sql.length > 100 ? '...' : ''),
185+
error: validation.error,
186+
timestamp
187+
})
178188
return this.createQueryResult(false, validation.error || 'Query not allowed')
179189
}
180190

@@ -186,13 +196,41 @@ class ClickHouseManager extends BaseDatabaseManager {
186196
const isDDL = queryType === QueryType.DDL
187197
const isDML = [QueryType.INSERT, QueryType.UPDATE, QueryType.DELETE].includes(queryType)
188198

199+
logger.debug(`[${timestamp}] ClickHouse query details`, {
200+
connectionId,
201+
queryType,
202+
isDDL,
203+
isDML,
204+
sessionId,
205+
host: connection.config.host,
206+
database: connection.config.database,
207+
timestamp
208+
})
209+
189210
if (isDDL || isDML) {
211+
logger.info(`[${timestamp}] Executing ClickHouse command`, {
212+
connectionId,
213+
queryType,
214+
sessionId,
215+
sql: sql.slice(0, 200) + (sql.length > 200 ? '...' : ''),
216+
timestamp
217+
})
218+
190219
// Use command() for DDL/DML queries that don't return data
191220
await connection.client.command({
192221
query: sql,
193222
query_id: sessionId || undefined
194223
})
195224

225+
const duration = Date.now() - startTime
226+
const completionTimestamp = new Date().toISOString()
227+
logger.info(`[${completionTimestamp}] ClickHouse command completed in ${duration}ms`, {
228+
queryType,
229+
sessionId,
230+
duration,
231+
timestamp: completionTimestamp
232+
})
233+
196234
return this.createQueryResult(
197235
true,
198236
'Command executed successfully',
@@ -202,6 +240,14 @@ class ClickHouseManager extends BaseDatabaseManager {
202240
isDML ? 1 : 0 // For DML, we don't get affected rows from ClickHouse easily
203241
)
204242
} else {
243+
logger.info(`[${timestamp}] Executing ClickHouse query`, {
244+
connectionId,
245+
queryType,
246+
sessionId,
247+
sql: sql.slice(0, 200) + (sql.length > 200 ? '...' : ''),
248+
timestamp
249+
})
250+
205251
// Use query() for SELECT and data-returning queries
206252
const abortController = new AbortController()
207253

@@ -234,6 +280,16 @@ class ClickHouseManager extends BaseDatabaseManager {
234280
this.activeQueries.delete(sessionId)
235281
}
236282

283+
const duration = Date.now() - startTime
284+
const completionTimestamp = new Date().toISOString()
285+
logger.info(`[${completionTimestamp}] ClickHouse query completed in ${duration}ms`, {
286+
queryType,
287+
rowCount: data.length,
288+
sessionId,
289+
duration,
290+
timestamp: completionTimestamp
291+
})
292+
237293
return this.createQueryResult(
238294
true,
239295
`Query executed successfully. Returned ${data.length} rows.`,
@@ -248,10 +304,27 @@ class ClickHouseManager extends BaseDatabaseManager {
248304
this.activeQueries.delete(sessionId)
249305
}
250306

251-
console.error('ClickHouse query error:', error)
307+
const duration = Date.now() - startTime
308+
const errorTimestamp = new Date().toISOString()
309+
310+
logger.error(`[${errorTimestamp}] ClickHouse query error after ${duration}ms`, {
311+
error: error instanceof Error ? error.message : 'Unknown error',
312+
connectionId,
313+
sessionId,
314+
duration,
315+
sql: sql.slice(0, 200) + (sql.length > 200 ? '...' : ''),
316+
timestamp: errorTimestamp
317+
})
252318

253319
// Check if it was cancelled
254320
if (error instanceof Error && error.name === 'AbortError') {
321+
logger.info(`[${errorTimestamp}] ClickHouse query was cancelled`, {
322+
connectionId,
323+
sessionId,
324+
duration,
325+
timestamp: errorTimestamp
326+
})
327+
255328
return this.createQueryResult(
256329
false,
257330
'Query was cancelled',

0 commit comments

Comments
 (0)