Skip to content

Commit 1d9a481

Browse files
committed
Save the titles
1 parent 478f180 commit 1d9a481

File tree

3 files changed

+68
-33
lines changed

3 files changed

+68
-33
lines changed

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/QueryHistoryPopover.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,14 @@ const SQL_KEYWORDS = [
3636
];
3737

3838
function highlightSQL(query: string): React.ReactNode[] {
39-
// Normalize whitespace for display (let CSS line-clamp handle truncation)
40-
const normalized = query.replace(/\s+/g, " ").slice(0, 200);
41-
const suffix = "";
39+
// Normalize: collapse multiple spaces/tabs to single space, but preserve newlines
40+
// Then trim each line and limit total length
41+
const normalized = query
42+
.split("\n")
43+
.map((line) => line.replace(/[ \t]+/g, " ").trim())
44+
.filter((line) => line.length > 0)
45+
.join("\n")
46+
.slice(0, 500);
4247

4348
// Create a regex pattern that matches keywords as whole words (case insensitive)
4449
const keywordPattern = new RegExp(
@@ -69,10 +74,6 @@ function highlightSQL(query: string): React.ReactNode[] {
6974
parts.push(normalized.slice(lastIndex));
7075
}
7176

72-
if (suffix) {
73-
parts.push(suffix);
74-
}
75-
7677
return parts;
7778
}
7879

@@ -118,10 +119,21 @@ export function QueryHistoryPopover({
118119
}}
119120
className="flex w-full items-center gap-2 rounded-sm px-2 py-2 outline-none transition-colors focus-custom hover:bg-charcoal-900"
120121
>
121-
<div className="flex flex-1 flex-col items-start overflow-hidden">
122-
<p className="line-clamp-2 w-full break-words text-left font-mono text-xs text-[#9b99ff]">
123-
{highlightSQL(item.query)}
124-
</p>
122+
<div className="flex flex-1 flex-col items-start gap-0.5 overflow-hidden">
123+
{item.title ? (
124+
<>
125+
<p className="w-full truncate text-left text-sm font-medium text-text-bright">
126+
{item.title}
127+
</p>
128+
<p className="line-clamp-4 w-full whitespace-pre-wrap text-left font-mono text-xs text-text-dimmed">
129+
{highlightSQL(item.query)}
130+
</p>
131+
</>
132+
) : (
133+
<p className="line-clamp-4 w-full whitespace-pre-wrap text-left font-mono text-xs text-[#9b99ff]">
134+
{highlightSQL(item.query)}
135+
</p>
136+
)}
125137
<div className="flex items-center gap-1.5 text-xs text-text-dimmed">
126138
<span className="capitalize">{item.scope}</span>
127139
{valueLabel && <span>· {valueLabel}</span>}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
289289
}
290290

291291
try {
292-
const [error, result] = await executeQuery({
292+
const [error, result, queryId] = await executeQuery({
293293
name: "query-page",
294294
query,
295295
schema: z.record(z.any()),
@@ -327,6 +327,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
327327
hiddenColumns: null,
328328
explainOutput: null,
329329
generatedSql: null,
330+
queryId: null,
330331
},
331332
{ status: 400 }
332333
);
@@ -340,6 +341,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
340341
hiddenColumns: result.hiddenColumns ?? null,
341342
explainOutput: result.explainOutput ?? null,
342343
generatedSql: result.generatedSql ?? null,
344+
queryId,
343345
});
344346
} catch (err) {
345347
const errorMessage = err instanceof Error ? err.message : "Unknown error executing query";
@@ -352,6 +354,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
352354
hiddenColumns: null,
353355
explainOutput: null,
354356
generatedSql: null,
357+
queryId: null,
355358
},
356359
{ status: 500 }
357360
);
@@ -571,14 +574,15 @@ export default function Page() {
571574
if (
572575
results?.rows &&
573576
!results.error &&
577+
results.queryId &&
574578
shouldGenerateTitle &&
575579
!historyTitle &&
576580
titleFetcher.state === "idle"
577581
) {
578582
const currentQuery = editorRef.current?.getQuery();
579583
if (currentQuery) {
580584
titleFetcher.submit(
581-
{ query: currentQuery },
585+
{ query: currentQuery, queryId: results.queryId },
582586
{
583587
method: "POST",
584588
action: `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/query/ai-title`,

apps/webapp/app/services/queryService.server.ts

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,21 @@ export type ExecuteQueryOptions<TOut extends z.ZodSchema> = Omit<
8989
customOrgConcurrencyLimit?: number;
9090
};
9191

92+
/**
93+
* Extended result type that includes the optional queryId when saved to history
94+
*/
95+
export type ExecuteQueryResult<T> =
96+
| [error: Error, result: null, queryId: null]
97+
| [error: null, result: T, queryId: string | null];
98+
9299
/**
93100
* Execute a TSQL query against ClickHouse with tenant isolation
94101
* Handles building tenant options, field mappings, and optionally saves to history
102+
* Returns [error, result, queryId] where queryId is the CustomerQuery ID if saved to history
95103
*/
96104
export async function executeQuery<TOut extends z.ZodSchema>(
97105
options: ExecuteQueryOptions<TOut>
98-
): Promise<TSQLQueryResult<z.output<TOut>>> {
106+
): Promise<ExecuteQueryResult<Exclude<TSQLQueryResult<z.output<TOut>>[1], null>>> {
99107
const {
100108
scope,
101109
organizationId,
@@ -112,20 +120,20 @@ export async function executeQuery<TOut extends z.ZodSchema>(
112120
const orgLimit = customOrgConcurrencyLimit ?? DEFAULT_ORG_CONCURRENCY_LIMIT;
113121

114122
// Acquire concurrency slot
115-
const acquireResult = await queryConcurrencyLimiter.acquire({
116-
key: organizationId,
117-
requestId,
118-
keyLimit: orgLimit,
119-
globalLimit: GLOBAL_CONCURRENCY_LIMIT,
120-
});
121-
122-
if (!acquireResult.success) {
123-
const errorMessage =
124-
acquireResult.reason === "key_limit"
125-
? `You've exceeded your query concurrency of ${orgLimit} for this organization. Please try again later.`
126-
: "We're experiencing a lot of queries at the moment. Please try again later.";
127-
return [new QueryError(errorMessage, { query: options.query }), null];
128-
}
123+
const acquireResult = await queryConcurrencyLimiter.acquire({
124+
key: organizationId,
125+
requestId,
126+
keyLimit: orgLimit,
127+
globalLimit: GLOBAL_CONCURRENCY_LIMIT,
128+
});
129+
130+
if (!acquireResult.success) {
131+
const errorMessage =
132+
acquireResult.reason === "key_limit"
133+
? `You've exceeded your query concurrency of ${orgLimit} for this organization. Please try again later.`
134+
: "We're experiencing a lot of queries at the moment. Please try again later.";
135+
return [new QueryError(errorMessage, { query: options.query }), null, null];
136+
}
129137

130138
try {
131139
// Build tenant IDs based on scope
@@ -172,9 +180,16 @@ export async function executeQuery<TOut extends z.ZodSchema>(
172180
},
173181
});
174182

183+
// If query failed, return early with no queryId
184+
if (result[0] !== null) {
185+
return [result[0], null, null];
186+
}
187+
188+
let queryId: string | null = null;
189+
175190
// If query succeeded and history options provided, save to history
176191
// Skip history for EXPLAIN queries (admin debugging) and when explicitly skipped (e.g., impersonating)
177-
if (result[0] === null && history && !history.skip && !baseOptions.explain) {
192+
if (history && !history.skip && !baseOptions.explain) {
178193
// Check if this query is the same as the last one saved (avoid duplicate history entries)
179194
const lastQuery = await prisma.customerQuery.findFirst({
180195
where: {
@@ -183,7 +198,7 @@ export async function executeQuery<TOut extends z.ZodSchema>(
183198
userId: history.userId ?? null,
184199
},
185200
orderBy: { createdAt: "desc" },
186-
select: { query: true, scope: true, filterPeriod: true, filterFrom: true, filterTo: true },
201+
select: { id: true, query: true, scope: true, filterPeriod: true, filterFrom: true, filterTo: true },
187202
});
188203

189204
const timeFilter = history.timeFilter;
@@ -195,8 +210,11 @@ export async function executeQuery<TOut extends z.ZodSchema>(
195210
lastQuery.filterFrom?.getTime() === (timeFilter?.from?.getTime() ?? undefined) &&
196211
lastQuery.filterTo?.getTime() === (timeFilter?.to?.getTime() ?? undefined);
197212

198-
if (!isDuplicate) {
199-
await prisma.customerQuery.create({
213+
if (isDuplicate && lastQuery) {
214+
// Return the existing query's ID for duplicate queries
215+
queryId = lastQuery.id;
216+
} else {
217+
const created = await prisma.customerQuery.create({
200218
data: {
201219
query: options.query,
202220
scope: scopeToEnum[scope],
@@ -211,10 +229,11 @@ export async function executeQuery<TOut extends z.ZodSchema>(
211229
filterTo: history.timeFilter?.to ?? null,
212230
},
213231
});
232+
queryId = created.id;
214233
}
215234
}
216235

217-
return result;
236+
return [null, result[1], queryId];
218237
} finally {
219238
// Always release the concurrency slot
220239
await queryConcurrencyLimiter.release({

0 commit comments

Comments
 (0)