Skip to content

Commit 9942518

Browse files
authored
TRQL/Query improvements (#2870)
Summary - Improve query experience and safety across ClickHouse and TSQL. Changes - Display JSON columns when in non-pretty mode (no longer show [Object Object]). - Sanitize ClickHouse errors originating from TSQL. - Remove tenant details from errors. - Add AI-assisted error-fixing for queries. - Improve code quality and readability. - Provide autocomplete support for enum values. - Enforce limits on ClickHouse queries (10s query limit). - Add org-level and global concurrency limits. - Warn and train AI to avoid SELECT *; when used, only return core columns and show info. - If AI suggests no time range, default to past 7 days. - Format the default query for readability. - Add an admin-only EXPLAIN button. - Prevent impersonation queries from being saved to history.
1 parent a3c3876 commit 9942518

File tree

21 files changed

+1558
-365
lines changed

21 files changed

+1558
-365
lines changed

apps/webapp/app/components/code/AIQueryInput.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,16 @@ interface AIQueryInputProps {
3232
onQueryGenerated: (query: string) => void;
3333
/** Set this to a prompt to auto-populate and immediately submit */
3434
autoSubmitPrompt?: string;
35+
/** Change this to force re-submission even if prompt is the same */
36+
autoSubmitKey?: number;
3537
/** Get the current query in the editor (used for edit mode) */
3638
getCurrentQuery?: () => string;
3739
}
3840

3941
export function AIQueryInput({
4042
onQueryGenerated,
4143
autoSubmitPrompt,
44+
autoSubmitKey,
4245
getCurrentQuery,
4346
}: AIQueryInputProps) {
4447
const [prompt, setPrompt] = useState("");
@@ -50,7 +53,7 @@ export function AIQueryInput({
5053
const [lastResult, setLastResult] = useState<"success" | "error" | null>(null);
5154
const textareaRef = useRef<HTMLTextAreaElement>(null);
5255
const abortControllerRef = useRef<AbortController | null>(null);
53-
const lastAutoSubmitRef = useRef<string | null>(null);
56+
const lastAutoSubmitRef = useRef<{ prompt: string; key?: number } | null>(null);
5457

5558
const organization = useOrganization();
5659
const project = useProject();
@@ -197,19 +200,22 @@ export function AIQueryInput({
197200
[prompt, submitQuery]
198201
);
199202

200-
// Auto-submit when autoSubmitPrompt changes
203+
// Auto-submit when autoSubmitPrompt or autoSubmitKey changes
201204
useEffect(() => {
202-
if (
203-
autoSubmitPrompt &&
204-
autoSubmitPrompt.trim() &&
205-
autoSubmitPrompt !== lastAutoSubmitRef.current &&
206-
!isLoading
207-
) {
208-
lastAutoSubmitRef.current = autoSubmitPrompt;
205+
if (!autoSubmitPrompt || !autoSubmitPrompt.trim() || isLoading) {
206+
return;
207+
}
208+
209+
const last = lastAutoSubmitRef.current;
210+
const isDifferent =
211+
last === null || autoSubmitPrompt !== last.prompt || autoSubmitKey !== last.key;
212+
213+
if (isDifferent) {
214+
lastAutoSubmitRef.current = { prompt: autoSubmitPrompt, key: autoSubmitKey };
209215
setPrompt(autoSubmitPrompt);
210216
submitQuery(autoSubmitPrompt);
211217
}
212-
}, [autoSubmitPrompt, isLoading, submitQuery]);
218+
}, [autoSubmitPrompt, autoSubmitKey, isLoading, submitQuery]);
213219

214220
// Cleanup on unmount
215221
useEffect(() => {

apps/webapp/app/components/code/TSQLEditor.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { sql, StandardSQL } from "@codemirror/lang-sql";
2-
import { autocompletion } from "@codemirror/autocomplete";
2+
import { autocompletion, startCompletion } from "@codemirror/autocomplete";
33
import { linter, lintGutter } from "@codemirror/lint";
4+
import { EditorView } from "@codemirror/view";
45
import type { ViewUpdate } from "@codemirror/view";
56
import { CheckIcon, ClipboardIcon, SparklesIcon, TrashIcon } from "@heroicons/react/20/solid";
67
import {
@@ -103,6 +104,23 @@ export function TSQLEditor(opts: TSQLEditorProps) {
103104
maxRenderedOptions: 50,
104105
})
105106
);
107+
108+
// Trigger autocomplete when ' is typed in value context
109+
// CodeMirror's activateOnTyping only triggers on alphanumeric characters,
110+
// so we manually trigger for quotes after comparison operators
111+
exts.push(
112+
EditorView.domEventHandlers({
113+
keyup: (event, view) => {
114+
// Trigger on quote key (both ' and shift+' on some keyboards)
115+
if (event.key === "'" || event.key === '"' || event.code === "Quote") {
116+
setTimeout(() => {
117+
startCompletion(view);
118+
}, 50);
119+
}
120+
return false;
121+
},
122+
})
123+
);
106124
}
107125

108126
// Add TSQL linter

apps/webapp/app/components/code/TSQLResultsTable.tsx

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ function CellValue({
134134
}) {
135135
// Plain text mode - render everything as monospace text with truncation
136136
if (!prettyFormatting) {
137+
if (column.type === "JSON") {
138+
return <JSONCellValue value={value} />;
139+
}
140+
137141
const plainValue = value === null ? "NULL" : String(value);
138142
const isTruncated = plainValue.length > MAX_STRING_DISPLAY_LENGTH;
139143

@@ -277,24 +281,7 @@ function CellValue({
277281

278282
// JSON type
279283
if (type === "JSON") {
280-
const jsonString = JSON.stringify(value);
281-
const isTruncated = jsonString.length > MAX_STRING_DISPLAY_LENGTH;
282-
283-
if (isTruncated) {
284-
return (
285-
<SimpleTooltip
286-
content={
287-
<pre className="max-w-sm whitespace-pre-wrap break-all font-mono text-xs">
288-
{jsonString}
289-
</pre>
290-
}
291-
button={
292-
<span className="font-mono text-xs text-text-dimmed">{truncateString(jsonString)}</span>
293-
}
294-
/>
295-
);
296-
}
297-
return <span className="font-mono text-xs text-text-dimmed">{jsonString}</span>;
284+
return <JSONCellValue value={value} />;
298285
}
299286

300287
// Array types
@@ -382,6 +369,28 @@ function EnvironmentCellValue({ value }: { value: string }) {
382369
return <EnvironmentLabel environment={environment} />;
383370
}
384371

372+
function JSONCellValue({ value }: { value: any }) {
373+
const jsonString = JSON.stringify(value);
374+
const isTruncated = jsonString.length > MAX_STRING_DISPLAY_LENGTH;
375+
376+
if (isTruncated) {
377+
return (
378+
<SimpleTooltip
379+
content={
380+
<pre className="max-w-sm whitespace-pre-wrap break-all font-mono text-xs">
381+
{jsonString}
382+
</pre>
383+
}
384+
button={
385+
<span className="font-mono text-xs text-text-dimmed">{truncateString(jsonString)}</span>
386+
}
387+
/>
388+
);
389+
}
390+
391+
return <span className="font-mono text-xs text-text-dimmed">{jsonString}</span>;
392+
}
393+
385394
/**
386395
* Check if a column should be right-aligned (numeric columns, duration, cost)
387396
*/

apps/webapp/app/components/code/tsql/tsqlCompletion.test.ts

Lines changed: 0 additions & 172 deletions
This file was deleted.

0 commit comments

Comments
 (0)