Skip to content

Conversation

@matt-aitken
Copy link
Member

@matt-aitken matt-aitken commented Jan 7, 2026

TRQL (pronounced Treacle like the delicious British dark sweet syrup) is the TRiggerQueryLanguage. It allows users to safely write queries on their data. The queries are safely turned into ClickHouse queries which are tenant-safe and not SQL injectable.

query-alpha-demo-100mb.mp4

This started out as a translation of HogQL by PostHog from Python to TypeScript.

Features

  • Tenant safe queries.
  • Many underlying ClickHouse features including functions and aggregations.
  • Virtual columns, which are exposed to users as real columns but are actually expressions.
  • Transformations of data types and where clauses.
  • Simple JSON path querying.
  • Limits on execution time.
  • Reporting of query statistics.

Query page

There’s a new Query page (currently behind a feature flag) where you can write TRQL queries and execute them against your environment, project or organization.

Features

  • Executing TRQL queries
  • Syntax highlighting and errors
  • Autocomplete
  • AI generation/editing of queries
  • Help and examples
  • Table with auto-inferred data types from the table schema
  • Table cell renderers for our special types like Run ids, environments, machines, tasks, queues, etc.
  • Copy/export as CSV/JSON
  • Line and bar graphs with grouping and stacking
  • History of queries

@changeset-bot
Copy link

changeset-bot bot commented Jan 7, 2026

⚠️ No Changeset found

Latest commit: 9d0ceb4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 7, 2026

Walkthrough

Adds a large TSQL feature surface and integrations: a new internal package @internal/tsql (ANTLR grammars, generated lexer/parser, AST, compiler, escapers, functions, schema/models, parser/printer, tests), ClickHouse client extensions including queryWithStats and TSQL execution helpers, DB migration and Prisma CustomerQuery model, backend AI query service and streaming route, a query execution service, many new frontend components and hooks (TSQLEditor, AIQueryInput, QueryResultsChart, ChartConfigPanel, TSQLResultsTable, CopyableTableCell, AlphaBadge, QueueName, useFeatureFlags), route pages and resources for query UIs, utilities (data export, number formatting, path builders), styles, tests, and dependency/config updates.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~240 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'TRQL and the Query page' clearly describes the main changes: introducing TRQL language support and a new Query page feature.
Description check ✅ Passed The description comprehensively explains TRQL features, the Query page functionality, and key capabilities including AI generation, syntax highlighting, autocomplete, charting, and query history.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vibe-kanban-cloud
Copy link

Review Complete

Your review story is ready!

View Story

Comment !reviewfast on this PR to re-generate the story.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

🤖 Fix all issues with AI agents
In @apps/webapp/app/components/code/AIQueryInput.tsx:
- Around line 163-164: submitQuery currently calls processStreamEvent but
processStreamEvent is not included in submitQuery's dependency array, causing
stale closures; fix by either moving the processStreamEvent function declaration
above the submitQuery definition so it can be referenced in the dependency
array, or wrap processStreamEvent in a stable ref (useRef) and reference
ref.current inside submitQuery; then add processStreamEvent (or the ref) to the
submitQuery useCallback dependency list and ensure onQueryGenerated remains
correctly referenced to avoid circular dependencies.

In @apps/webapp/app/components/code/QueryResultsChart.tsx:
- Around line 292-332: The sortData function currently always uses __rawDate
when present, so add an xAxisColumn parameter to sortData and only perform the
date comparison when sortByColumn === xAxisColumn and both a.__rawDate and
b.__rawDate exist; otherwise fall through to the numeric/string comparison.
Update calls to sortData (e.g., where transformDataForChart or the component
sorts the results) to pass the current xAxisColumn. Ensure the function
signature is updated (sortData(data, sortByColumn, sortDirection, xAxisColumn))
and the date branch becomes: if (sortByColumn === xAxisColumn && aDate && bDate)
{ ... } so sorting by other columns uses the numeric/string logic.

In @apps/webapp/app/components/code/TSQLEditor.tsx:
- Around line 208-211: The onBlur handler in TSQLEditor reads
editor.current?.textContent (DOM) which can differ from the editor's document;
instead, call into the editor view/state to get the canonical document text
(e.g., use the editor view/state on the editor ref such as
editor.current?.view.state.doc.toString() or
editor.current?.state.doc.toString()) and pass that string to the onBlur prop;
update the onBlur arrow function to guard for onBlur and editor.current then
extract the document via the editor's state rather than textContent.

In @apps/webapp/app/components/primitives/Table.tsx:
- Around line 286-293: Replace the non-semantic <span> used for the copy action
with a <button> element (keep the same className including "absolute -right-2
top-1/2 z-10 hidden -translate-y-1/2 cursor-pointer
group-hover/copyable-cell:flex"), add type="button", preserve the existing
onClick handler logic (e.stopPropagation(); e.preventDefault(); copy();) and add
an accessible label via aria-label (e.g., aria-label="Copy cell"). This ensures
the interactive element is semantic and keyboard-accessible while keeping the
same styling and behavior.

In @apps/webapp/app/components/runs/v3/TaskRunStatus.tsx:
- Around line 243-249: The reverse lookup in runStatusFromFriendlyTitle fails
because runStatusTitleFromStatus maps both PENDING_VERSION and
WAITING_FOR_DEPLOY to the same friendly string ("Pending version"), so
titlesStatusesArray can’t distinguish them; fix by choosing one of: (A) give
each status a unique friendly title in runStatusTitleFromStatus so
runStatusFromFriendlyTitle works unchanged, or (B) change
runStatusFromFriendlyTitle to use an explicit reverse map (e.g., build a
Map<string, TaskRunStatus | TaskRunStatus[]> from titlesStatusesArray) and
either return an array of matching statuses, return the first by documented
priority, or throw an explicit error on ambiguous titles (mentioning
PENDING_VERSION and WAITING_FOR_DEPLOY) to make the collision handling
deterministic and obvious.

In
@apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx:
- Around line 132-159: The switch case labeled "finish" declares const finalText
and const query which leak into other cases; wrap the entire case body in its
own block (add { ... } immediately after case "finish":) so finalText and query
are block-scoped, keep the sendEvent logic intact and ensure the break remains
inside that block; locate the "finish" case in the switch handling result events
to apply this change.

In @apps/webapp/app/services/queryService.server.ts:
- Around line 97-98: The code parses stats.byte_seconds into byteSeconds and
multiplies to compute costInCents but does not guard against parseFloat
returning NaN; update the logic in queryService.server.ts around the
byteSeconds/costInCents calculation to validate the parsed value (e.g., use
Number.isFinite or !isNaN on byteSeconds), and if invalid set a safe default
(such as 0) or handle the error path (log/warn and skip cost computation) before
multiplying with env.CENTS_PER_QUERY_BYTE_SECOND so costInCents never becomes
NaN.

In @apps/webapp/test/components/code/tsql/tsqlCompletion.test.ts:
- Around line 72-173: The test file is in the wrong location per repo
guidelines—move the test from
apps/webapp/test/components/code/tsql/tsqlCompletion.test.ts to sit beside the
implementation at apps/webapp/app/components/code/tsql/tsqlCompletion.test.ts;
update any relative imports in the test if paths change and ensure your test
runner/tsconfig includes the new folder (adjust include/glob patterns if
necessary) so createTSQLCompletion tests (the describe block for
createTSQLCompletion) keep running from the new location.

In @apps/webapp/test/components/code/tsql/tsqlLinter.test.ts:
- Around line 49-78: The test file for getTSQLError is placed in the wrong
directory; move the test to sit beside the implementation (same directory as the
tsql linter module), update any relative imports to reference getTSQLError from
its local implementation (ensure the test still imports getTSQLError correctly),
and run the test suite to confirm paths and test discovery are unchanged; keep
the test content (describe/getTSQLError cases) intact during the move.

In @internal-packages/clickhouse/src/client/client.ts:
- Around line 363-366: parsedSummary can yield elapsed_ns = 0 causing
elapsedSeconds to be 0 and byteSeconds to become Infinity; guard the computation
in the block that calculates readBytes/elapsedSeconds (variables parsedSummary,
readBytes, elapsedNs, elapsedSeconds, byteSeconds) by checking if elapsedSeconds
is <= 0 (or extremely small) and in that case set byteSeconds to 0 (or null)
before creating the stats object so you never produce "Infinity" strings for
downstream consumers.
- Line 362: The debug logging in the method is inconsistent: replace the call to
this.logger.log("parsedSummary", parsedSummary) with the same debug-level logger
used elsewhere (use this.logger.debug) so the parsedSummary output follows the
existing debug logging conventions in the method; locate the occurrence of
"parsedSummary" in client.ts and change its logging method to debug to match the
other debug entries (e.g., the lines using this.logger.debug earlier in the
function).

In @internal-packages/clickhouse/tsconfig.build.json:
- Line 19: The tsconfig override explicitly disables noImplicitAny which weakens
type safety despite strict: true; remove the "noImplicitAny": false entry (or
set it to true) from tsconfig.build.json so the compiler inherits noImplicitAny
from strict mode, then run the build/type-check and fix any resulting
implicit-any errors in the code referenced by this package.

In @internal-packages/tsql/src/grammar/parser.test.ts:
- Around line 1-4: The test file TSQL parser uses the global test helpers but
never imports them; add an import for vitest test helpers (e.g., import {
describe, it, expect } from "vitest";) to
internal-packages/tsql/src/grammar/parser.test.ts so calls to describe, it, and
expect used in the file resolve at runtime; place the import alongside the other
top-level imports near the top of the file before tests run.

In @internal-packages/tsql/src/query/ast.ts:
- Around line 185-187: IntervalType currently sets data_type to the wrong
literal ("unknown"); update the IntervalType definition so its data_type uses a
distinct literal (e.g., "interval") instead of "unknown" to mirror how
DecimalType defines its own data_type; modify the IntervalType interface (which
extends ConstantType) to declare data_type: "interval" (or the project's
established interval literal) so type discrimination works correctly across the
AST.
- Around line 654-671: The returned object from createSelectSetQueryFromQueries
is missing the required expression_type field for a SelectSetQuery; update the
returned object literal in createSelectSetQueryFromQueries to include
expression_type: "select_set_query" (alongside initial_select_query and
subsequent_select_queries) so the object conforms to the SelectSetQuery type
(and keep subsequent_select_queries mapping to SelectSetNode as before).
- Around line 157-159: DecimalType incorrectly sets data_type to "unknown";
change DecimalType (which extends ConstantType) to use data_type: "decimal" and
add "decimal" to the ConstantDataType union in constants.ts so the type is
recognized project-wide; update any related type guards or switch handling that
pattern-matches ConstantDataType (search for usages of DecimalType and
ConstantDataType) to handle the new "decimal" value.

In @internal-packages/tsql/src/query/escape.ts:
- Around line 188-206: The TSQL branch in SQLValueEscaper.visitDateTime builds a
datetime string but ignores this.timezone; update the this.dialect === "tsql"
branch in visitDateTime to include the timezone as the second argument (e.g.
return `toDateTime(${this.visitString(datetimeString)},
${this.visitString(this.timezone)})`) so it mirrors the ClickHouse branch's use
of visitString(this.timezone); ensure visitString is used for the timezone value
to maintain proper escaping/quoting.

In @internal-packages/tsql/src/query/parser.test.ts:
- Line 7: The imported local name `SyntaxError` shadows the global Error
constructor; rename the import (e.g., `import { SyntaxError as TsqlSyntaxError }
from "./errors.js";`) and update all references in this test file that intend
the tsql module's error to use the new name (replace usages of `SyntaxError`
such as the test assertion that expects the parser to throw the module error
with `TsqlSyntaxError`).
🧹 Nitpick comments (37)
internal-packages/clickhouse/src/taskRuns.ts (1)

48-48: LGTM! New field correctly integrated into TaskRunV2 schema.

The addition of max_duration_in_seconds aligns with the database schema migration and retrieved learnings about task duration management. The use of .nullish() is appropriate for this optional field.

Optional: Consider adding range validation

For extra safety, you could add .min(0) to match the ClickHouse UInt32 unsigned constraint:

-  max_duration_in_seconds: z.number().int().nullish(),
+  max_duration_in_seconds: z.number().int().min(0).nullish(),

However, this is optional since the pattern is consistent with other duration fields in the schema and validation may occur elsewhere.

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx (1)

520-520: Consider explicit prop passing for clarity.

The spread operator works correctly, but explicitly passing props makes the data flow clearer and avoids passing unnecessary properties:

<QueueName name={queue.name} type={queue.type} paused={queue.paused} />

This is a minor style preference—the current implementation is functional.

♻️ Optional refactor
-                              <QueueName {...queue} />
+                              <QueueName name={queue.name} type={queue.type} paused={queue.paused} />
internal-packages/tsql/package.json (2)

16-16: Grammar build script may not work on Windows.

The grammar:build:typescript script uses Unix shell commands (cat, tail, sed) which won't work on Windows without WSL or similar environment. Consider adding documentation about platform requirements or using a cross-platform build tool like Node.js scripts.

Alternative: Cross-platform Node.js script

Create a scripts/build-grammar.js file:

import { readFileSync, writeFileSync, unlinkSync } from 'fs';
import { execSync } from 'child_process';

const lexerTS = readFileSync('src/grammar/TSQLLexer.typescript.g4', 'utf8');
const lexerCommon = readFileSync('src/grammar/TSQLLexer.common.g4', 'utf8');
const lexerCommonLines = lexerCommon.split('\n').slice(1);
const modifiedLines = lexerCommonLines.map(line => 
  line.replace(/isOpeningTag/g, 'self.isOpeningTag')
);
const combined = lexerTS + '\n' + modifiedLines.join('\n');
writeFileSync('src/grammar/TSQLLexer.g4', combined);

execSync('antlr4ts src/grammar/TSQLLexer.g4');
unlinkSync('src/grammar/TSQLLexer.g4');
execSync('antlr4ts -visitor -no-listener -Dlanguage=TypeScript src/grammar/TSQLParser.g4');

Then update package.json:

-    "grammar:build:typescript": "cat src/grammar/TSQLLexer.typescript.g4 > src/grammar/TSQLLexer.g4 && tail -n +2 src/grammar/TSQLLexer.common.g4 |sed s/isOpeningTag/self.isOpeningTag/ >> src/grammar/TSQLLexer.g4 && antlr4ts src/grammar/TSQLLexer.g4 && rm src/grammar/TSQLLexer.g4 && antlr4ts -visitor -no-listener -Dlanguage=TypeScript src/grammar/TSQLParser.g4",
+    "grammar:build:typescript": "node scripts/build-grammar.js",

10-11: Consider evaluating zod upgrade to 4.x series if breaking changes are manageable.

The package uses [email protected], which is the latest (and only) available version for this package. Using an alpha version is unavoidable here. However, [email protected] is significantly outdated—zod 4.3.5 is the current stable release. If the codebase can accommodate zod 4's breaking changes, upgrading would align with the latest stable version.

internal-packages/tsql/src/grammar/TSQLLexer.typescript.g4 (1)

9-16: Consider using String.fromCodePoint for full Unicode support.

String.fromCharCode only handles code points in the BMP (0-0xFFFF). For characters above U+FFFF, fromCodePoint is needed. Since the check includes c > 0x10FFFF, it seems full Unicode was intended.

♻️ Suggested improvement
 private _peekChar(k: number): string {
     // Return the k-th look-ahead as a *single-char string* or '\0' at EOF.
     const c = this._input.LA(k);          // int code point or IntStream.EOF (-1)
     if (c < 0 || c > 0x10FFFF) {         // EOF or out-of-range → sentinel
         return '\0';
     }
-    return String.fromCharCode(c);
+    return String.fromCodePoint(c);
 }
internal-packages/tsql/src/query/parse_string.ts (1)

4-4: Consider renaming the import to avoid shadowing global SyntaxError.

The static analysis tool correctly flags that importing SyntaxError shadows the global. This can cause confusion when debugging. Consider using an alias like TSQLSyntaxError.

♻️ Suggested fix
-import { SyntaxError } from './errors';
+import { SyntaxError as TSQLSyntaxError } from './errors';

Then update line 43:

-        throw new SyntaxError(`Invalid string literal, must start and end with the same quote type: ${text}`);
+        throw new TSQLSyntaxError(`Invalid string literal, must start and end with the same quote type: ${text}`);
apps/webapp/evals/aiQuery.eval.ts (1)

45-52: Add error handling for JSON parsing.

If the AI model returns malformed JSON, JSON.parse will throw an unhandled exception. Consider wrapping in try/catch and returning a score of 0 for parse failures.

♻️ Suggested fix
     // Parse the output to extract the query
-    const outputParsed = JSON.parse(output) as ParsedQueryResult;
-    const expectedParsed = JSON.parse(expected) as ParsedQueryResult;
+    let outputParsed: ParsedQueryResult;
+    let expectedParsed: ParsedQueryResult;
+    try {
+      outputParsed = JSON.parse(output) as ParsedQueryResult;
+      expectedParsed = JSON.parse(expected) as ParsedQueryResult;
+    } catch {
+      // Malformed JSON should score 0
+      return 0;
+    }
internal-packages/database/prisma/schema.prisma (1)

2402-2439: Well-structured audit model with appropriate indexes.

The CustomerQuery model follows a sensible audit-log pattern (no updatedAt since records are immutable). The cascade behaviors are appropriate: Cascade for org/project/environment and SetNull for user.

Consider adding indexes for project/environment scoped queries.

If query history will be displayed at project or environment scope (not just organization), consider adding indexes:

@@index([projectId, createdAt(sort: Desc)])
@@index([environmentId, createdAt(sort: Desc)])

This would optimize history lookups when scoped to a specific project or environment. If queries are always fetched at org level and filtered client-side, the current indexes are sufficient.

internal-packages/tsql/src/query/constants.ts (2)

36-44: Prefer string union or const object over enum.

Per coding guidelines, enums should be avoided in favor of string unions or const objects.

♻️ Suggested refactor using const object
-export enum LimitContext {
-  QUERY = "query",
-  QUERY_ASYNC = "query_async",
-  EXPORT = "export",
-  COHORT_CALCULATION = "cohort_calculation",
-  HEATMAPS = "heatmaps",
-  SAVED_QUERY = "saved_query",
-  RETENTION = "retention",
-}
+export const LimitContext = {
+  QUERY: "query",
+  QUERY_ASYNC: "query_async",
+  EXPORT: "export",
+  COHORT_CALCULATION: "cohort_calculation",
+  HEATMAPS: "heatmaps",
+  SAVED_QUERY: "saved_query",
+  RETENTION: "retention",
+} as const;
+
+export type LimitContext = (typeof LimitContext)[keyof typeof LimitContext];

Based on coding guidelines, enums should be avoided.


47-71: Prefer types over interfaces.

Per coding guidelines, types should be used instead of interfaces.

♻️ Suggested refactor using types
-// Settings applied at the SELECT level
-export interface TSQLQuerySettings {
-  optimize_aggregation_in_order?: boolean;
-  date_time_output_format?: string;
-  date_time_input_format?: string;
-  join_algorithm?: string;
-}
-
-// Settings applied on top of all TSQL queries
-export interface TSQLGlobalSettings extends TSQLQuerySettings {
+// Settings applied at the SELECT level
+export type TSQLQuerySettings = {
+  optimize_aggregation_in_order?: boolean;
+  date_time_output_format?: string;
+  date_time_input_format?: string;
+  join_algorithm?: string;
+};
+
+// Settings applied on top of all TSQL queries
+export type TSQLGlobalSettings = TSQLQuerySettings & {
   readonly?: number;
   max_execution_time?: number;
   // ... rest of fields
-}
+};

Based on coding guidelines, types should be used over interfaces.

internal-packages/tsql/src/query/models.ts (2)

6-66: Prefer types over interfaces.

Per coding guidelines, types should be used instead of interfaces. While interfaces work here, consistency with the codebase guidelines is preferred.

Example transformation for a few interfaces:

export type FieldOrTable = {
  hidden?: boolean;
};

export type DatabaseField = FieldOrTable & {
  name: string;
  array?: boolean;
  nullable?: boolean;
  is_nullable?(): boolean;
  get_constant_type?(): ConstantType;
  default_value?(): any;
};

Based on coding guidelines, types should be used over interfaces.


247-253: Avoid any type - import the actual type.

The comment indicates LazyJoinType exists in ast.ts. Import and use the actual type for better type safety.

♻️ Suggested fix
-import type { Expr, ConstantType } from "./ast";
+import type { Expr, ConstantType, LazyJoinType } from "./ast";

// ... at line 251
-  lazy_join_type: any; // LazyJoinType from ast.ts
+  lazy_join_type: LazyJoinType;
apps/webapp/app/components/navigation/SideMenu.tsx (1)

8-8: Remove unused icon imports.

CircleStackIcon and MagnifyingGlassCircleIcon are imported but not used in this file. Only TableCellsIcon is used for the Query menu item.

♻️ Suggested fix
 import {
   ArrowPathRoundedSquareIcon,
   ArrowRightOnRectangleIcon,
   BeakerIcon,
   BellAlertIcon,
   ChartBarIcon,
   ChevronRightIcon,
-  CircleStackIcon,
   ClockIcon,
   Cog8ToothIcon,
   CogIcon,
   FolderIcon,
   FolderOpenIcon,
   GlobeAmericasIcon,
   IdentificationIcon,
   KeyIcon,
-  MagnifyingGlassCircleIcon,
   PencilSquareIcon,
   PlusIcon,
   RectangleStackIcon,
   ServerStackIcon,
   Squares2X2Icon,
   TableCellsIcon,
   UsersIcon,
 } from "@heroicons/react/20/solid";

Also applies to: 17-17

apps/webapp/app/components/code/tsql/tsqlLinter.test.ts (1)

66-71: Consider making the error format assertion more resilient.

The test assumes error messages contain the word "line" for position information. If the error format from the parser changes, this test could become brittle.

♻️ Alternative approach

Consider either:

  1. Testing for a more specific pattern (e.g., position/column info)
  2. Or simply verifying that a non-null, non-empty error is returned without asserting internal format:
     it("should include position information in error", () => {
       const error = getTSQLError("SELECT * FORM users");
       expect(error).not.toBeNull();
-      // Error message should contain line/column info
-      expect(error).toContain("line");
+      // Verify error contains useful diagnostic information
+      expect(error!.length).toBeGreaterThan(0);
     });
apps/webapp/app/components/AlphaBadge.tsx (1)

25-31: Consider adding flex styling for proper alignment.

AlphaTitle renders a span and AlphaBadge as siblings within a fragment. Depending on usage context, this may result in misaligned elements. Consider wrapping in a flex container for consistent alignment.

♻️ Suggested improvement
 export function AlphaTitle({ children }: { children: React.ReactNode }) {
   return (
-    <>
-      <span>{children}</span>
+    <span className="inline-flex items-center gap-1">
+      <span>{children}</span>
       <AlphaBadge />
-    </>
+    </span>
   );
 }
apps/webapp/app/components/code/tsql/tsqlCompletion.test.ts (1)

5-28: Consider using a more type-safe mock instead of as any.

The as any cast at line 27 bypasses TypeScript's type checking. Consider defining a proper type for the mock context or using a partial type assertion.

♻️ More type-safe alternative
+import type { CompletionContext } from "@codemirror/autocomplete";
+
 // Helper to create a mock completion context
 function createMockContext(doc: string, pos: number, explicit = false) {
   return {
     state: {
       doc: {
         toString: () => doc,
       },
     },
     pos,
     explicit,
     matchBefore: (regex: RegExp) => {
       const beforePos = doc.slice(0, pos);
       const match = beforePos.match(new RegExp(regex.source + "$"));
       if (match) {
         return {
           from: pos - match[0].length,
           to: pos,
           text: match[0],
         };
       }
       return null;
     },
-  } as any;
+  } as Partial<CompletionContext> as CompletionContext;
 }
internal-packages/clickhouse/src/client/client.ts (1)

234-304: Consider extracting common logic to reduce duplication.

The queryWithStats method shares significant code with the query method (lines 85-232). Consider extracting common logic for parameter validation, span setup, and error handling into shared helper functions.

This would improve maintainability and reduce the risk of divergence between the two methods when future changes are made.

apps/webapp/app/components/code/tsql/tsqlLinter.ts (1)

4-4: Consider renaming the imported SyntaxError to avoid shadowing the global.

The static analysis tool flags that importing SyntaxError shadows the global SyntaxError class. While this works correctly since the local import takes precedence, it could cause confusion during debugging or if someone inadvertently expects the global behavior.

♻️ Suggested fix
-import { parseTSQLSelect, SyntaxError, QueryError, validateQuery } from "@internal/tsql";
+import { parseTSQLSelect, SyntaxError as TSQLSyntaxError, QueryError, validateQuery } from "@internal/tsql";

Then update the usage on line 120:

-    if (error instanceof SyntaxError) {
+    if (error instanceof TSQLSyntaxError) {
internal-packages/tsql/src/index.ts (1)

210-233: Prefer type over interface for CompileTSQLOptions.

As per coding guidelines, use types over interfaces for TypeScript definitions.

Suggested change
-export interface CompileTSQLOptions {
+export type CompileTSQLOptions = {
   /** The organization ID for tenant isolation (required) */
   organizationId: string;
   /** The project ID for tenant isolation (optional - omit to query across all projects) */
   projectId?: string;
   /** The environment ID for tenant isolation (optional - omit to query across all environments) */
   environmentId?: string;
   /** Schema definitions for allowed tables and columns */
   tableSchema: TableSchema[];
   /** Optional query settings */
   settings?: Partial<QuerySettings>;
   /**
    * Runtime field mappings for dynamic value translation.
    * Maps internal ClickHouse values to external user-facing values.
    *
    * @example
    * ```typescript
    * {
    *   project: { "cm12345": "my-project-ref" },
    * }
    * ```
    */
   fieldMappings?: FieldMappings;
-}
+};
apps/webapp/app/services/queryService.server.ts (1)

100-112: Consider handling potential errors from history recording.

The prisma.customerQuery.create call is awaited but errors are not caught. If this fails (e.g., database issue), the entire executeQuery function will throw even though the actual query succeeded. Consider wrapping this in a try-catch or making it fire-and-forget.

Suggested approach
-    await prisma.customerQuery.create({
-      data: {
-        query: options.query,
-        scope: scopeToEnum[scope],
-        stats: { ...stats },
-        costInCents,
-        source: history.source,
-        organizationId,
-        projectId: scope === "project" || scope === "environment" ? projectId : null,
-        environmentId: scope === "environment" ? environmentId : null,
-        userId: history.userId ?? null,
-      },
-    });
+    // Record history but don't fail the query if recording fails
+    try {
+      await prisma.customerQuery.create({
+        data: {
+          query: options.query,
+          scope: scopeToEnum[scope],
+          stats: { ...stats },
+          costInCents,
+          source: history.source,
+          organizationId,
+          projectId: scope === "project" || scope === "environment" ? projectId : null,
+          environmentId: scope === "environment" ? environmentId : null,
+          userId: history.userId ?? null,
+        },
+      });
+    } catch (historyError) {
+      console.error("Failed to record query history:", historyError);
+    }
internal-packages/clickhouse/src/client/types.ts (1)

19-37: Consider using type instead of interface for consistency with coding guidelines.

The coding guidelines prefer types over interfaces for TypeScript definitions.

Suggested change
-export interface QueryStats {
+export type QueryStats = {
   read_rows: string;
   read_bytes: string;
   written_rows: string;
   written_bytes: string;
   total_rows_to_read: string;
   result_rows: string;
   result_bytes: string;
   elapsed_ns: string;
   byte_seconds: string;
-}
+};

-export interface QueryResultWithStats<TOutput> {
+export type QueryResultWithStats<TOutput> = {
   rows: TOutput[];
   stats: QueryStats;
-}
+};
internal-packages/tsql/src/query/errors.ts (1)

39-41: Consider renaming SyntaxError to avoid shadowing the global.

The SyntaxError class shadows the global SyntaxError. While the import is explicit where used, this can cause confusion. Consider renaming to TSQLSyntaxError for clarity.

This is a style suggestion. If the shadowing is intentional and the team prefers the cleaner name, this can be safely ignored. The current approach works correctly since imports are explicit.

internal-packages/clickhouse/src/client/tsql.ts (2)

31-69: Consider using type instead of interface per coding guidelines.

The coding guidelines specify using types over interfaces for TypeScript definitions in this codebase.

♻️ Suggested refactor
-export interface ExecuteTSQLOptions<TOut extends z.ZodSchema> {
+export type ExecuteTSQLOptions<TOut extends z.ZodSchema> = {
   /** The name of the operation (for logging/tracing) */
   name: string;
   // ... rest of properties
-}
+};

74-78: Consider using type instead of interface for consistency.

♻️ Suggested refactor
-export interface TSQLQuerySuccess<T> {
+export type TSQLQuerySuccess<T> = {
   rows: T[];
   columns: OutputColumnMetadata[];
   stats: QueryStats;
-}
+};
internal-packages/tsql/src/query/functions.ts (2)

9-28: Consider using type instead of interface per coding guidelines.

♻️ Suggested refactor
-export interface TSQLFunctionMeta {
+export type TSQLFunctionMeta = {
   /** The ClickHouse function name to use */
   clickhouseName: string;
   // ... rest of properties
-}
+};

564-589: Case-insensitive lookup logic has a subtle edge case.

The findFunction logic correctly handles case sensitivity, but when the exact-case lookup fails and lowercase lookup succeeds for a case-sensitive function, returning undefined is correct. However, consider adding a comment explaining this behavior for maintainability since it's non-obvious.

📝 Add clarifying comment
 function findFunction(
   name: string,
   functions: Record<string, TSQLFunctionMeta>
 ): TSQLFunctionMeta | undefined {
+  // First try exact case match
   const func = functions[name];
   if (func !== undefined) {
     return func;
   }

+  // Try lowercase lookup for case-insensitive functions
   const lowerFunc = functions[name.toLowerCase()];
   if (lowerFunc === undefined) {
     return undefined;
   }

-  // If we haven't found a function with the case preserved, but we have found it in lowercase,
-  // then the function names are different case-wise only.
+  // If the function requires exact case matching (caseSensitive: true),
+  // reject the lowercase match since the original name had different casing
   if (lowerFunc.caseSensitive) {
     return undefined;
   }

   return lowerFunc;
 }
apps/webapp/app/v3/services/aiQueryService.server.ts (3)

29-32: Consider using type instead of interface per coding guidelines.

♻️ Suggested refactor
-export interface AIQueryOptions {
+export type AIQueryOptions = {
   mode?: "new" | "edit";
   currentQuery?: string;
-}
+};

37-41: Consider using type instead of interface for internal types.


71-104: Duplicate tool definitions between streamQuery and call methods.

The validateTSQLQuery and getTableSchema tools are defined identically in both methods. Extract these to a private method or property to reduce duplication and ensure consistency.

♻️ Proposed refactor to DRY up tool definitions
+  private getTools() {
+    return {
+      validateTSQLQuery: tool({
+        description:
+          "Validate a TSQL query for syntax errors and schema compliance. Always use this tool to verify your query before returning it to the user.",
+        parameters: z.object({
+          query: z.string().describe("The TSQL query to validate"),
+        }),
+        execute: async ({ query }) => {
+          return this.validateQuery(query);
+        },
+      }),
+      getTableSchema: tool({
+        description:
+          "Get detailed schema information about available tables and columns. Use this to understand what data is available and how to query it.",
+        parameters: z.object({
+          tableName: z
+            .string()
+            .optional()
+            .describe("Optional: specific table name to get details for"),
+        }),
+        execute: async ({ tableName }) => {
+          return this.getSchemaInfo(tableName);
+        },
+      }),
+    };
+  }

   streamQuery(prompt: string, options: AIQueryOptions = {}) {
     // ...
     return streamText({
       model: this.model,
       system: systemPrompt,
       prompt: userPrompt,
-      tools: {
-        validateTSQLQuery: tool({ ... }),
-        getTableSchema: tool({ ... }),
-      },
+      tools: this.getTools(),
       maxSteps: 5,
       // ...
     });
   }

Also applies to: 126-159

internal-packages/tsql/src/query/ast.ts (2)

248-254: Consider using const objects instead of enums per coding guidelines.

The coding guidelines suggest avoiding enums in favor of string unions or const objects.

♻️ Suggested refactor using const object
-export enum ArithmeticOperationOp {
-  Add = "+",
-  Sub = "-",
-  Mult = "*",
-  Div = "/",
-  Mod = "%",
-}
+export const ArithmeticOperationOp = {
+  Add: "+",
+  Sub: "-",
+  Mult: "*",
+  Div: "/",
+  Mod: "%",
+} as const;
+
+export type ArithmeticOperationOp = (typeof ArithmeticOperationOp)[keyof typeof ArithmeticOperationOp];

256-277: Consider using const objects instead of enums per coding guidelines.

internal-packages/tsql/src/query/database.ts (2)

102-120: Consider using const object instead of enum per coding guidelines.

♻️ Suggested refactor
-export enum DatabaseSerializedFieldType {
-  STRING = "string",
-  INTEGER = "integer",
-  // ... etc
-}
+export const DatabaseSerializedFieldType = {
+  STRING: "string",
+  INTEGER: "integer",
+  FLOAT: "float",
+  DECIMAL: "decimal",
+  BOOLEAN: "boolean",
+  DATE: "date",
+  DATETIME: "datetime",
+  UUID: "uuid",
+  ARRAY: "array",
+  JSON: "json",
+  TUPLE: "tuple",
+  UNKNOWN: "unknown",
+  EXPRESSION: "expression",
+  VIEW: "view",
+  LAZY_TABLE: "lazy_table",
+  VIRTUAL_TABLE: "virtual_table",
+  FIELD_TRAVERSER: "field_traverser",
+} as const;
+
+export type DatabaseSerializedFieldType = (typeof DatabaseSerializedFieldType)[keyof typeof DatabaseSerializedFieldType];

25-130: Consider using type instead of interface for schema definitions.

Multiple interfaces in this range could be converted to types per coding guidelines.

apps/webapp/app/v3/querySchemas.ts (1)

400-409: Avoid logging inside whereTransform to reduce noise and potential PII leakage

bulk_action_group_ids.whereTransform currently logs every value it transforms. This will fire on each query that filters on this column, can generate a lot of log noise, and leaks raw bulk IDs into logs without strong justification.

Consider removing the log or gating it behind a dedicated debug flag.

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

2003-2021: Clarify units for byte_seconds in formatQueryStats

formatQueryStats passes byte_seconds through formatBytes and then appends "s", yielding values like "12.3 KBs". Since byte_seconds is a rate-like or composite metric (bytes * seconds), this label can be misleading.

Consider either:

  • Using a dedicated formatter that reflects the intended unit (e.g., "12.3 MiB·s"), or
  • Renaming the label to something more generic (e.g., "12.3 MB-equivalent"), or omitting it if it’s not user-facingly meaningful.
apps/webapp/app/components/code/tsql/tsqlCompletion.ts (1)

326-348: Escape enum values before wrapping them in quotes for completion labels

createEnumValueCompletions currently builds labels like:

label: `'${userFriendlyValue}'`,

or:

label: `'${value}'`,

If any user-friendly or allowed value ever contains a single quote, the inserted completion will produce invalid SQL (or at least require manual fixing). Today your enums (statuses, environment types) are probably simple strings, but this is an easy future footgun.

Consider adding a small helper to escape single quotes (e.g., replacing ' with '') before wrapping, so the completion always inserts syntactically valid string literals.

apps/webapp/app/components/code/ChartConfigPanel.tsx (1)

14-32: Align ChartConfiguration shape with current UI semantics and TS style guidelines

Two minor points here:

  1. Type vs interface
    The repo guidelines prefer type aliases over interfaces for TS. ChartConfiguration can be trivially expressed as a type without behavior change:

    export type ChartConfiguration = {
      chartType: ChartType;
      xAxisColumn: string | null;
      yAxisColumns: string[];
      groupByColumn: string | null;
      stacked: boolean;
      sortByColumn: string | null;
      sortDirection: SortDirection;
    };
  2. Single vs multiple Y‑axis columns
    yAxisColumns is typed as string[], but the panel only reads/writes yAxisColumns[0]. If multi-series Y support isn’t planned yet, consider simplifying this to a single yAxisColumn: string | null to better reflect current behavior (or extend the UI to genuinely support multiple Y selections).

Also applies to: 276-283

Comment on lines 286 to 301
<span
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
copy();
}}
className="absolute -right-2 top-1/2 z-10 hidden -translate-y-1/2 cursor-pointer group-hover/copyable-cell:flex"
>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use a button element instead of a span for the copy action.

The copy action uses a <span> with an onClick handler, which lacks semantic meaning and is not keyboard-accessible by default. Interactive elements should use appropriate semantic HTML elements.

Proposed fix
-          <span
+          <button
+            type="button"
             onClick={(e) => {
               e.stopPropagation();
               e.preventDefault();
               copy();
             }}
             className="absolute -right-2 top-1/2 z-10 hidden -translate-y-1/2 cursor-pointer group-hover/copyable-cell:flex"
           >
             <SimpleTooltip
               button={
                 <span
                   className={cn(
                     "flex size-6 items-center justify-center rounded border border-charcoal-650 bg-charcoal-750",
                     copied
                       ? "text-green-500"
                       : "text-text-dimmed hover:border-charcoal-600 hover:bg-charcoal-700 hover:text-text-bright"
                   )}
                 >
                   {copied ? (
                     <ClipboardCheckIcon className="size-3.5" />
                   ) : (
                     <ClipboardIcon className="size-3.5" />
                   )}
                 </span>
               }
               content={copied ? "Copied!" : "Copy"}
               disableHoverableContent
             />
-          </span>
+          </button>
🤖 Prompt for AI Agents
In @apps/webapp/app/components/primitives/Table.tsx around lines 286 - 293,
Replace the non-semantic <span> used for the copy action with a <button> element
(keep the same className including "absolute -right-2 top-1/2 z-10 hidden
-translate-y-1/2 cursor-pointer group-hover/copyable-cell:flex"), add
type="button", preserve the existing onClick handler logic (e.stopPropagation();
e.preventDefault(); copy();) and add an accessible label via aria-label (e.g.,
aria-label="Copy cell"). This ensures the interactive element is semantic and
keyboard-accessible while keeping the same styling and behavior.

@claude
Copy link

claude bot commented Jan 8, 2026

PR Review: TRQL and the Query Page

This is a substantial PR that introduces TRQL (TRiggerQueryLanguage), a custom SQL-like query language that compiles to ClickHouse SQL with automatic tenant isolation. The implementation is derived from PostHog's HogQL and represents a significant feature addition.

Overall Assessment

This is a well-architected feature with strong security foundations. The code demonstrates careful thought about SQL injection prevention and multi-tenant data isolation. The test coverage is impressive, particularly for security-related functionality.


Security Analysis ✅

Strengths

  1. Multi-tenant Isolation - The tenant guard injection system is robust:

    • Organization, project, and environment IDs are automatically injected as parameterized WHERE clauses
    • Guards are applied to all query types: SELECT, JOINs, UNIONs, and subqueries
    • The security tests in security.test.ts verify these guards cannot be bypassed via OR clauses or explicit conditions
  2. SQL Injection Prevention:

    • All user-provided string values are parameterized (using ClickHouse's {param: Type} syntax)
    • Identifier escaping properly handles special characters and prevents backtick injection
    • The parser rejects stacked statements (semicolon-separated queries)
    • Function allowlisting prevents access to dangerous ClickHouse functions like file()
  3. Table/Column Allowlisting:

    • Only explicitly defined tables in the schema can be queried
    • System tables are rejected
    • Database prefixes are blocked

Minor Security Considerations

  1. AI-Generated Queries (aiQueryService.server.ts): The AI service validates queries before returning them, which is good. However, consider adding rate limiting to the AI generation endpoint to prevent abuse.

  2. Query Cost Tracking: The CENTS_PER_QUERY_BYTE_SECOND billing mechanism is a good addition for cost control. Consider adding configurable limits per organization to prevent runaway costs.


Code Quality ✅

Strengths

  1. Strong TypeScript typing throughout the codebase with proper use of discriminated unions for AST nodes
  2. Comprehensive test suite with unit tests for escaping, parsing, printing, and security
  3. Clean separation of concerns - parser, validator, printer, and executor are well-isolated
  4. Good documentation in code comments explaining the PostHog HogQL derivation

Suggestions

  1. Duplicated Query Extraction Logic (aiQueryService.server.ts:472-486 and ai-generate.tsx:186-199):

    // This function appears in two files
    function extractQueryFromText(text: string): string | null {
      const codeBlockMatch = text.match(/```(?:sql)?\s*([\s\S]*?)```/i);
      // ...
    }

    Consider extracting this to a shared utility to maintain DRY principles.

  2. Error Handling Consistency: The route action returns different error formats:

    • Some return { error: string } with 400 status
    • Others use 403/404 status codes
      Consider standardizing the error response format.
  3. Magic Numbers: In queryService.server.ts, the cost calculation uses env.CENTS_PER_QUERY_BYTE_SECOND which is good, but ensure this env var has validation and a sensible default.


Performance Considerations ⚠️

Potential Issues

  1. Field Mappings Query (queryService.server.ts:73-85):

    const projects = await prisma.project.findMany({
      where: { organizationId },
      select: { id: true, externalRef: true },
    });
    const environments = await prisma.runtimeEnvironment.findMany({
      where: { project: { organizationId } },
      select: { id: true, slug: true },
    });

    These queries run for every query execution. For large organizations, consider:

    • Caching these mappings with a reasonable TTL
    • Lazy loading only when needed (if the query references those fields)
  2. Regex in Escape Functions (escape.ts): The character-by-character processing with .split("").map().join() is correct but could be optimized for very long strings using a single regex replace.

  3. AST Visitor Pattern: The printer uses a recursive visitor pattern which is clean but could potentially cause stack overflow for extremely deeply nested queries. Consider implementing a max depth limit.


Test Coverage ✅

The test coverage is excellent:

  • security.test.ts - 600+ lines of security-focused tests
  • escape.test.ts - Thorough coverage of escaping edge cases
  • printer.test.ts - 2200+ lines testing SQL generation
  • validator.test.ts - Schema validation tests
  • tsqlCompletion.test.ts and tsqlLinter.test.ts - Editor tooling tests

Suggestion

Consider adding integration tests that execute actual queries against a test ClickHouse instance to verify the end-to-end flow, particularly for edge cases around tenant isolation.


Database Migration ✅

The migration (20251220201230_add_customer_query/migration.sql) is well-structured:

  • Proper indexes on (organizationId, createdAt DESC) for history queries
  • CASCADE deletes maintain referential integrity
  • SET NULL on userId deletion is appropriate for audit trail preservation

Minor Issues & Nits

  1. Grammar File Maintenance: The generated ANTLR files (TSQLLexer.ts, TSQLParser.ts) are committed. Consider documenting the regeneration process in the README.

  2. Environment Variable: New env var CENTS_PER_QUERY_BYTE_SECOND should be documented in .env.example if not already.

  3. Feature Flag Check (route.tsx:77-108): The hasQueryAccess function queries the database for every request. Consider caching this check or moving it to middleware.


Summary

Recommendation: Approve with minor suggestions

This is a high-quality implementation of a complex feature. The security model is well thought out, the code is clean and well-tested, and the architecture allows for future extensibility. The suggestions above are minor optimizations and consistency improvements rather than blocking issues.

Key strengths:

  • 🔒 Strong security model with comprehensive tests
  • 📐 Clean architecture derived from battle-tested HogQL
  • ✅ Excellent test coverage
  • 📝 Well-documented code

The main areas for future improvement:

  • ⚡ Consider caching field mappings for performance
  • 🔄 Extract duplicated utility functions
  • 📊 Add integration tests with real ClickHouse

Great work on this substantial feature! 🎉

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
apps/webapp/app/components/code/ChartConfigPanel.tsx (3)

14-22: Consider using type instead of interface for ChartConfiguration.

The coding guidelines specify preferring types over interfaces for TypeScript definitions.

♻️ Refactor to use type alias
-export interface ChartConfiguration {
+export type ChartConfiguration = {
   chartType: ChartType;
   xAxisColumn: string | null;
   yAxisColumns: string[];
   groupByColumn: string | null;
   stacked: boolean;
   sortByColumn: string | null;
   sortDirection: SortDirection;
-}
+};

As per coding guidelines.


283-292: Consider preserving user's sort configuration when changing X-axis.

When manually selecting a datetime X-axis column, the sort order is always overridden to that column ascending, even if the user had previously configured a different sort. This differs from the initial auto-select behavior (lines 142-150) which only sets sort when none is configured.

This could be surprising if a user has carefully configured their sort order. Consider only auto-setting the sort when config.sortByColumn is null:

♻️ Preserve existing sort configuration
 setValue={(value) => {
   const updates: Partial<ChartConfiguration> = { xAxisColumn: value || null };
   // Auto-set sort to x-axis ASC if selecting a datetime column
-  if (value) {
+  if (value && !config.sortByColumn) {
     const selectedCol = columns.find((c) => c.name === value);
     if (selectedCol && isDateTimeType(selectedCol.type)) {
       updates.sortByColumn = value;
       updates.sortDirection = "asc";
     }
   }
   updateConfig(updates);
 }}

469-489: TypeBadge could better handle nested type wrappers.

The current sequential processing (Nullable first, then LowCardinality) doesn't optimally handle nested types like LowCardinality(Nullable(String)). The LowCardinality wrapper would be stripped, leaving Nullable(String), but the Nullable indicator isn't applied since that check already passed.

For the initial implementation this is acceptable, but you might consider recursively stripping both wrappers and collecting indicators:

♻️ Enhanced nested type handling
 function TypeBadge({ type }: { type: string }) {
-  // Simplify type for display
   let displayType = type;
-  if (type.startsWith("Nullable(")) {
-    displayType = type.slice(9, -1) + "?";
-  }
-  if (type.startsWith("LowCardinality(")) {
-    displayType = type.slice(15, -1);
-  }
+  let suffix = "";
+  
+  // Strip wrappers recursively
+  while (displayType.startsWith("Nullable(") || displayType.startsWith("LowCardinality(")) {
+    if (displayType.startsWith("Nullable(")) {
+      displayType = displayType.slice(9, -1);
+      suffix = "?";
+    } else if (displayType.startsWith("LowCardinality(")) {
+      displayType = displayType.slice(15, -1);
+    }
+  }
+  
+  displayType = displayType + suffix;
 
   // Shorten long type names
   if (displayType.length > 12) {
     displayType = displayType.slice(0, 10) + "…";
   }
 
   return (
     <span className="rounded bg-charcoal-750 px-1 py-0.5 font-mono text-xxs text-text-dimmed">
       {displayType}
     </span>
   );
 }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ca9d15 and 5cef0b4.

📒 Files selected for processing (1)
  • apps/webapp/app/components/code/ChartConfigPanel.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

Files:

  • apps/webapp/app/components/code/ChartConfigPanel.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/components/code/ChartConfigPanel.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • apps/webapp/app/components/code/ChartConfigPanel.tsx
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access all environment variables through the env export of env.server.ts instead of directly accessing process.env in the Trigger.dev webapp

Files:

  • apps/webapp/app/components/code/ChartConfigPanel.tsx
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: When importing from @trigger.dev/core in the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Files:

  • apps/webapp/app/components/code/ChartConfigPanel.tsx
**/*.{js,ts,jsx,tsx,json,md,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier

Files:

  • apps/webapp/app/components/code/ChartConfigPanel.tsx
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to internal-packages/database/**/*.{ts,tsx} : Use Prisma for database interactions in internal-packages/database with PostgreSQL
🧬 Code graph analysis (1)
apps/webapp/app/components/code/ChartConfigPanel.tsx (5)
internal-packages/tsql/src/query/printer.ts (1)
  • isDateTimeType (1178-1188)
apps/webapp/app/utils/cn.ts (1)
  • cn (77-79)
apps/webapp/app/components/primitives/Paragraph.tsx (1)
  • Paragraph (88-107)
apps/webapp/app/components/primitives/Buttons.tsx (1)
  • Button (296-329)
apps/webapp/app/components/primitives/Switch.tsx (1)
  • Switch (64-119)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (24)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: claude-review
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (4)
apps/webapp/app/components/code/ChartConfigPanel.tsx (4)

42-75: LGTM! Type detection helpers are well-implemented.

The type detection functions correctly handle ClickHouse types including Nullable wrappers and parameterized types like DateTime64(3).


79-156: LGTM! Auto-selection logic is well-designed.

The use of columnsKey for change detection and refs to avoid dependency issues demonstrates thoughtful handling of React's reactivity model. The auto-selection logic appropriately defaults to datetime columns for the X-axis and only auto-sets sorting when none is configured.


313-416: LGTM! Selection controls and conditional toggles are well-structured.

The consistent use of the __none__ sentinel value for optional selections and conditional rendering of the Stacked and Sort Direction controls based on configuration state creates a clean, intuitive interface.


421-467: LGTM! Helper components are clean and straightforward.

Both ConfigField and SortDirectionToggle serve their purposes well as simple internal components. The styling and interaction patterns are consistent with the rest of the component.

@claude
Copy link

claude bot commented Jan 8, 2026

PR Review: TRQL and the Query Page

This is an impressive and substantial PR introducing a new query language (TRQL) and a full-featured query page for trigger.dev. The implementation shows excellent attention to security, particularly around tenant isolation and SQL injection prevention. Below is my detailed review.


🔒 Security Analysis

Strengths:

  1. Robust Tenant Isolation - The createTenantGuard() method in printer.ts:1366-1424 correctly injects mandatory tenant guards (organization_id, project_id, environment_id) into all queries. The guards are properly ANDed with user WHERE clauses, preventing bypass via OR conditions.

  2. Parameterized Queries - String values are properly parameterized via PrinterContext.addValue() rather than interpolated directly into SQL. The escape functions in escape.ts correctly handle special characters.

  3. Function Allowlisting - Only functions defined in functions.ts are permitted. Dangerous functions like file() or system functions are correctly rejected (verified by security.test.ts:323-342).

  4. Comprehensive Security Tests - The security.test.ts file covers:

    • Cross-tenant access prevention (lines 66-127)
    • SQL injection via strings, comments, identifiers (lines 213-343)
    • Stacked query prevention (lines 344-356)
    • UNION-based injection (lines 358-371)

Potential Concerns:

  1. AI Query Service (aiQueryService.server.ts) - The AI-generated queries are validated before execution, which is good. However, consider adding rate limiting to prevent abuse of the AI endpoint.

  2. Feature Flag Check - The hasQueryAccess() function in the route file performs authorization correctly, but ensure the feature flag cannot be manipulated by users.


🐛 Potential Bugs or Issues

  1. Type Coercion in Chart Config - In QueryResultsChart.tsx, when processing numeric data for charts, ensure proper handling of nullable columns that might contain empty strings or null values to prevent NaN in visualizations.

  2. Error Boundary Missing - The main query route component doesn't appear to have an error boundary. If the TSQL compilation fails in an unexpected way, the UI might crash.

  3. History Query Race Condition - In the loader/action flow, if a user submits a query while the history is loading, there could be a mismatch. Consider optimistic updates or loading states.

  4. Parser Error Messages - When parseTSQLSelect() fails, the error message could be more user-friendly with line/column information (already present in the AST nodes as start/end properties).


⚡ Performance Considerations

  1. Query Limits - The maxRows limit in QuerySettings (defaulting based on context) is good. Consider also adding:

    • Query execution timeout (visible in the options but ensure ClickHouse settings are applied)
    • Maximum query complexity/depth limits
  2. ANTLR4 Parser Performance - ANTLR parsers can be memory-intensive. For very long queries, consider adding input size validation before parsing.

  3. Result Transformation - The transformResults() function iterates over all rows and columns. For large result sets, this could be a bottleneck. Consider lazy transformation or streaming if needed.

  4. Chart Rendering - For large datasets (>1000 rows), the chart component might struggle. Consider adding data sampling or aggregation on the server side.


📊 Test Coverage

Well Tested:

  • Parser and printer (parser.test.ts, printer.test.ts with 2244 lines!)
  • Security scenarios (security.test.ts with 601 lines)
  • Escape functions (escape.test.ts)
  • Integration tests against real ClickHouse (tsql.test.ts)
  • Frontend linting and completion (tsqlLinter.test.ts, tsqlCompletion.test.ts)

Consider Adding:

  • Tests for the AI query service edge cases
  • E2E tests for the query page UI flow
  • Tests for CSV/JSON export functionality
  • Tests for very large result sets
  • Tests for concurrent query execution

💻 Code Quality

Positive:

  1. TypeScript Port Quality - The HogQL port is well-documented with comments linking to the original Python implementation. The code maintains the same structure making it easier to sync updates.

  2. Separation of Concerns - Clean separation between:

    • Grammar/Parser (ANTLR generated)
    • AST types and visitors
    • Schema validation
    • SQL printing
    • ClickHouse execution
  3. Schema-Driven Design - The TableSchema type provides a single source of truth for:

    • Column names and types
    • Virtual columns with expressions
    • Value mappings (user-friendly ↔ internal)
    • Tenant column definitions
  4. Good Error Types - Custom error classes (QueryError, SyntaxError, NotImplementedError) with position information.

Suggestions:

  1. Magic Strings - Consider using constants for frequently used strings like column types ("String", "DateTime64", etc.) to prevent typos.

  2. Documentation - The NOTICE.md in the tsql package is great for attribution. Consider adding API documentation for the main entry points.


📁 Database Migration

The CustomerQuery table migration (20251220201230_add_customer_query) looks good:

  • Proper indexes for organization_id + createdAt queries
  • Foreign key cascades for cleanup
  • SET NULL for userId to preserve history if user is deleted

🎨 Minor Suggestions

  1. Rename TSQL - "TSQL" might be confused with Microsoft's T-SQL. The PR description mentions "TRQL" (pronounced Treacle) - consider using this consistently in the codebase.

  2. Error Message Consistency - Some error messages use single quotes, others double quotes for identifiers. Standardize for consistency.

  3. Virtual Column Documentation - Add JSDoc comments explaining the difference between regular columns, virtual columns, and columns with whereTransform.


✅ Summary

This is a well-architected PR with strong security foundations. The TSQL/TRQL implementation follows best practices for query languages with proper sandboxing, parameterization, and tenant isolation. The test coverage is comprehensive, especially for security-critical paths.

Recommended before merge:

  1. Add rate limiting to the AI query endpoint
  2. Add an error boundary to the query page
  3. Consider the performance suggestions for large datasets

Great work on this feature! 🎉

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
apps/webapp/app/components/code/ChartConfigPanel.tsx (1)

45-78: Consider extracting type detection helpers to a shared utility.

The type detection functions (isNumericType, isDateTimeType, isStringType) are well-implemented. However, a similar isDateTimeType exists in internal-packages/tsql/src/query/printer.ts with slight differences. Your version includes the check type.startsWith("DateTime64(") which handles parameterized DateTime64 types like DateTime64(3), making it more comprehensive.

♻️ Consider creating a shared utility module

These type detection helpers could be extracted to a shared utility module (e.g., apps/webapp/app/utils/clickhouse-types.ts) to avoid duplication and ensure consistency across the codebase. This would also make it easier to maintain and extend type detection logic in the future.

apps/webapp/app/components/code/QueryResultsChart.tsx (1)

149-187: Consider using mode instead of minimum for interval detection.

The current implementation uses the minimum gap (line 166) as a heuristic for data interval, which works for many cases but could be affected by outliers (e.g., one data point that's very close to another).

♻️ Consider calculating the mode (most common gap) for more robust detection

Using the most frequently occurring gap would be more resilient to outliers:

// After line 162, add:
// Count gap frequencies
const gapCounts = new Map<number, number>();
for (const gap of gaps) {
  // Round to nearest minute to group similar gaps
  const rounded = Math.round(gap / MINUTE) * MINUTE;
  gapCounts.set(rounded, (gapCounts.get(rounded) || 0) + 1);
}

// Find the most common gap
let mostCommonGap = minGap;
let maxCount = 0;
for (const [gap, count] of gapCounts) {
  if (count > maxCount) {
    maxCount = count;
    mostCommonGap = gap;
  }
}

// Use mostCommonGap instead of minGap for snapping logic
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5cef0b4 and 7e5a0cb.

📒 Files selected for processing (2)
  • apps/webapp/app/components/code/ChartConfigPanel.tsx
  • apps/webapp/app/components/code/QueryResultsChart.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

Files:

  • apps/webapp/app/components/code/QueryResultsChart.tsx
  • apps/webapp/app/components/code/ChartConfigPanel.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/components/code/QueryResultsChart.tsx
  • apps/webapp/app/components/code/ChartConfigPanel.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • apps/webapp/app/components/code/QueryResultsChart.tsx
  • apps/webapp/app/components/code/ChartConfigPanel.tsx
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access all environment variables through the env export of env.server.ts instead of directly accessing process.env in the Trigger.dev webapp

Files:

  • apps/webapp/app/components/code/QueryResultsChart.tsx
  • apps/webapp/app/components/code/ChartConfigPanel.tsx
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: When importing from @trigger.dev/core in the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Files:

  • apps/webapp/app/components/code/QueryResultsChart.tsx
  • apps/webapp/app/components/code/ChartConfigPanel.tsx
**/*.{js,ts,jsx,tsx,json,md,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier

Files:

  • apps/webapp/app/components/code/QueryResultsChart.tsx
  • apps/webapp/app/components/code/ChartConfigPanel.tsx
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to internal-packages/database/**/*.{ts,tsx} : Use Prisma for database interactions in internal-packages/database with PostgreSQL
🧬 Code graph analysis (2)
apps/webapp/app/components/code/QueryResultsChart.tsx (3)
apps/webapp/app/components/code/ChartConfigPanel.tsx (2)
  • ChartConfiguration (15-24)
  • AggregationType (13-13)
apps/webapp/app/components/primitives/Chart.tsx (1)
  • ChartConfig (10-18)
apps/webapp/app/components/primitives/Paragraph.tsx (1)
  • Paragraph (88-107)
apps/webapp/app/components/code/ChartConfigPanel.tsx (5)
internal-packages/tsql/src/query/printer.ts (1)
  • isDateTimeType (1178-1188)
apps/webapp/app/utils/cn.ts (1)
  • cn (77-79)
apps/webapp/app/components/primitives/Paragraph.tsx (1)
  • Paragraph (88-107)
apps/webapp/app/components/primitives/Buttons.tsx (1)
  • Button (296-329)
apps/webapp/app/components/primitives/Switch.tsx (1)
  • Switch (64-119)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (24)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: claude-review
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (12)
apps/webapp/app/components/code/ChartConfigPanel.tsx (4)

82-105: Column categorization logic is sound.

The categorization correctly identifies numeric, datetime, and categorical columns. Note that datetime columns are intentionally included in both dateTimeColumns and categoricalColumns (line 94), which allows them to be used for grouping purposes while maintaining their datetime nature for axis selection.


118-159: Auto-selection logic works correctly.

The auto-select defaults effect properly handles initial column selection with good prioritization:

  • X-axis: prefers datetime, then categorical, then any column
  • Y-axis: selects first numeric column
  • Sort: auto-sets to X-axis ascending when it's a datetime column

The use of columnsKey to detect actual column changes and refs to access current config without dependency issues is well-implemented.


241-451: Chart configuration UI is well-structured.

The UI properly handles:

  • Chart type selection with visual feedback
  • Dynamic option lists based on column types
  • Conditional visibility for stacked toggle (only when grouping) and sort direction (only when sorting)
  • Auto-configuration when selecting datetime columns for X-axis

The immediate auto-sort feedback in the X-axis onChange handler (lines 294-305) provides better UX compared to waiting for the next useEffect run.


453-521: Helper components are clean and functional.

The helper components are well-designed:

  • ConfigField provides consistent field layout with proper handling of empty labels
  • SortDirectionToggle offers clear ascending/descending selection
  • TypeBadge intelligently simplifies ClickHouse type names for display (strips wrapper types, truncates long names)
apps/webapp/app/components/code/QueryResultsChart.tsx (8)

72-143: Time granularity detection and formatting are well-implemented.

The detectTimeGranularity function uses sensible thresholds to determine the appropriate time scale, and formatDateByGranularity provides contextually appropriate formatting for each granularity level. The use of native date formatting APIs ensures locale-aware display.


193-288: Time gap filling with aggregation is correctly implemented.

This complex function properly handles:

  • Limiting maximum points to prevent performance issues (lines 207-224)
  • Bucketing data points to align with time intervals (lines 226-252)
  • Filling missing time slots with zeros (lines 254-287)
  • Applying aggregation to multiple values in the same bucket (line 269)

The logic ensures charts display continuous time series data with visible gaps rendered as zeros rather than connecting distant points.


323-379: Tick generation creates well-aligned, human-friendly labels.

The generateTimeTicks function intelligently:

  • Selects appropriate intervals from predefined "nice" values
  • Aligns ticks to natural boundaries (midnight, hour marks)
  • Ensures a reasonable number of ticks (2-8)
  • Provides padding to avoid cutting off edge data points

408-437: Date parsing handles multiple formats robustly.

The tryParseDate function defensively handles:

  • Date objects with validation
  • ISO string formats (regex check for YYYY-MM-DD)
  • Numbers as milliseconds (prioritized)
  • Numbers as Unix timestamps in seconds (fallback)
  • Invalid dates return null rather than throwing

The year range check (1970-2100) prevents accepting unrealistic timestamp values.


453-638: Data transformation logic is comprehensive and correct.

This critical function handles multiple complex scenarios:

Date detection (lines 471-499):

  • Uses an 80% threshold to determine if the X-axis is date-based
  • Calculates time domain with 2% padding for visual spacing
  • Generates appropriate tick positions

Non-grouped mode (lines 507-568):

  • Groups rows by X-axis value to handle duplicates
  • Applies configured aggregation to multiple values at the same X coordinate
  • Fills time gaps for continuous time series

Grouped mode (lines 570-637):

  • Pivots data so each group value becomes a separate chart series
  • Applies aggregation within each group
  • Fills time gaps while maintaining group structure

The defensive checks (line 520, 586) skip invalid dates for date-based axes, preventing chart rendering errors.


640-708: Utility functions are robust and well-implemented.

The helper functions handle edge cases properly:

  • toNumber: Converts various types to numbers with safe fallback
  • aggregateValues: Correctly implements all aggregation types (sum, avg, count, min, max) with empty array handling
  • sortData: Comprehensive sorting logic that tries date comparison first (using __rawDate), then numeric, then string comparison, with proper null handling

710-923: Chart rendering component handles multiple visualization scenarios correctly.

The QueryResultsChart component properly:

Data processing (lines 715-743):

  • Transforms data with memoization for performance
  • Forces chronological order for date-based axes (line 740), which is essential for time series visualization

Axis configuration (lines 752-853):

  • Creates dynamic formatters based on data characteristics
  • Uses different X-axis configurations for date-based (continuous numeric scale) vs categorical axes
  • Provides adaptive Y-axis formatting based on value ranges

Rendering (lines 855-920):

  • Conditionally renders BarChart, AreaChart (for stacked), or LineChart
  • Properly configures stacking, tooltips, and legends
  • Includes validation and empty states

The distinction between date-based continuous axes and categorical axes ensures proper visual representation of time series data.


925-986: Y-axis formatter and empty state are well-designed.

The createYAxisFormatter function provides intelligent formatting:

  • Uses K/M abbreviations for large numbers (≥1K, ≥1M)
  • Dynamically adjusts decimal places based on data range (more precision for smaller ranges)
  • Handles edge cases (infinite values, zero range)

The EmptyState component provides clear user feedback for various validation failures.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @apps/webapp/app/components/code/QueryResultsChart.tsx:
- Around line 101-143: formatDateByGranularity currently hardcodes the "en-US"
locale; change all instances of "en-US" to undefined so the runtime/browsers
default locale is used (e.g., in the Date.prototype.toLocaleTimeString and
toLocaleDateString calls inside formatDateByGranularity), and make the same
replacement in the formatDateForTooltip function to ensure dates are localized
to the user's environment.
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7e5a0cb and 546c1c3.

📒 Files selected for processing (1)
  • apps/webapp/app/components/code/QueryResultsChart.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

Files:

  • apps/webapp/app/components/code/QueryResultsChart.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/components/code/QueryResultsChart.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • apps/webapp/app/components/code/QueryResultsChart.tsx
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access all environment variables through the env export of env.server.ts instead of directly accessing process.env in the Trigger.dev webapp

Files:

  • apps/webapp/app/components/code/QueryResultsChart.tsx
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: When importing from @trigger.dev/core in the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Files:

  • apps/webapp/app/components/code/QueryResultsChart.tsx
**/*.{js,ts,jsx,tsx,json,md,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier

Files:

  • apps/webapp/app/components/code/QueryResultsChart.tsx
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to internal-packages/database/**/*.{ts,tsx} : Use Prisma for database interactions in internal-packages/database with PostgreSQL
🧬 Code graph analysis (1)
apps/webapp/app/components/code/QueryResultsChart.tsx (3)
apps/webapp/app/components/code/ChartConfigPanel.tsx (2)
  • ChartConfiguration (15-24)
  • AggregationType (13-13)
apps/webapp/app/components/primitives/Chart.tsx (1)
  • ChartConfig (10-18)
apps/webapp/app/components/primitives/Paragraph.tsx (1)
  • Paragraph (88-107)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (24)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: claude-review
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (2)
apps/webapp/app/components/code/QueryResultsChart.tsx (2)

671-711: Well-implemented date-aware sorting logic!

The special handling for date-based sorting (lines 689-696) ensures that when sorting by the X-axis column, dates are compared chronologically using the __rawDate metadata rather than as raw strings or timestamps. This prevents incorrect ordering in time-series charts and aligns with the PR objective of improving "sorting change when x axis is a date."

The implementation correctly:

  • Only applies date comparison when sortByColumn === xAxisColumn
  • Falls back to numeric/string comparison for non-date columns
  • Handles null/undefined values defensively

740-746: Correct enforcement of chronological ordering for date-based axes.

The sorting logic correctly ensures that date-based charts are always displayed in chronological order (line 743) by sorting on xDataKey ascending, overriding any user-specified sort configuration. This prevents confusion from disconnected or reversed time series.

For categorical (non-date) axes, the user's sort preferences are respected (line 745). This dual behavior is appropriate and well-implemented.

@claude
Copy link

claude bot commented Jan 9, 2026

PR Review: TRQL - TRigger Query Language

Overview

This is a substantial and well-architected feature addition that introduces TRQL (TRigger Query Language) - a custom query language that allows users to safely write queries against their data. The implementation is impressive in scope, covering parsing, validation, compilation to ClickHouse SQL, a full UI with editor/autocomplete/charts, AI-powered query generation, and query history tracking.


✅ Strengths

Security Architecture

  • Excellent tenant isolation: The query system enforces organization/project/environment isolation at the query layer, not just SQL layer. Tenant guards are automatically injected into all queries.
  • No SQL injection risk: The ANTLR4-based parser creates a proper AST which is then transpiled to parameterized ClickHouse SQL - user input never directly becomes SQL.
  • Schema-based validation: Queries are validated against a defined schema, preventing access to unauthorized tables/columns.
  • Feature flag protection: Query page is behind a feature flag for gradual rollout.

Code Quality

  • Clean separation of concerns: Parser → AST → Validator → Printer pipeline is well-structured.
  • Type safety: Strong TypeScript typing throughout with proper Zod validation for runtime data.
  • Attribution: NOTICE.md properly credits PostHog's HogQL as the origin (AGPL compliance).
  • Comprehensive function coverage: The functions.ts file provides extensive ClickHouse function mappings with proper argument validation.

Testing

  • Good coverage: 600+ lines of security tests, 600+ lines of parser tests, 220+ lines of escape tests.
  • E2E integration tests: Tests against real ClickHouse containers via testcontainers.
  • AI evaluation suite: evals/aiQuery.eval.ts for testing AI query generation quality.

UX

  • Rich editor experience: CodeMirror 6 with syntax highlighting, autocomplete, and linting.
  • Helpful documentation: Extensive inline documentation in the UI with function tooltips.
  • Export capabilities: CSV/JSON export for query results.
  • Charts: Line and bar charts with grouping/stacking options via Recharts.

⚠️ Potential Issues & Suggestions

1. Security Considerations

Field Mappings Lookup (queryService.server.ts:73-86)

const projects = await prisma.project.findMany({
  where: { organizationId },
  select: { id: true, externalRef: true },
});
  • This fetches ALL projects/environments in the organization for every query. For organizations with many projects, this could be slow and return unnecessary data.
  • Suggestion: Consider caching these mappings or only fetching them when needed for the specific query.

AI Query Service (aiQueryService.server.ts)

  • The AI service uses gpt-4o-mini by default. Ensure rate limiting and cost controls are in place.
  • The maxSteps: 5 limit is good, but consider adding a total token limit as well.

2. Performance Considerations

Query Page Route (route.tsx)

  • The route file is 2168 lines - consider splitting into smaller components for maintainability.
  • The useEffect that resets chart config on schema change (line 353-357) could cause unnecessary re-renders. The columnsKey approach is good but ensure stability.

Field Mappings on Every Query

  • Every query execution does 2 database lookups for field mappings. Consider:
    • Caching these mappings
    • Adding them to the loader so they're available on page load
    • Making them lazy-loaded only when needed

3. Code Quality Suggestions

Type Safety in Results Table

// route.tsx:209
schema: z.record(z.any()),
  • Using z.any() loses type safety. Consider defining expected column types from the schema.

Error Handling in AI Service

// aiQueryService.server.ts:182-186
if (text.toLowerCase().includes("cannot") || text.toLowerCase().includes("unable")) {
  return { success: false, error: text.slice(0, 200) };
}
  • This heuristic for detecting errors could have false positives/negatives. Consider a more structured approach using tool calls or explicit error markers.

Dead Code / Unused Variable

// route.tsx:2075
const suffix = "";
  • This variable is set but never used meaningfully.

4. Missing Items to Consider

Input Validation

  • The ActionSchema validates query is non-empty but doesn't enforce a maximum length. Consider adding a max length to prevent abuse.

Query Timeout

  • While ClickHouse has execution time limits, consider adding an application-level timeout as well.

Rate Limiting

  • Queries can be expensive. Consider implementing rate limiting per user/organization.

Audit Logging

  • Query history is saved to CustomerQuery, which is great. Ensure sensitive queries can't be logged if they contain PII.

5. Testing Gaps

  • No tests visible for the React components (TSQLEditor, TSQLResultsTable, ChartConfigPanel).
  • The AI query service tests appear to be evaluation-based rather than unit tests. Consider adding mocked unit tests.
  • Edge cases for very large result sets (pagination, streaming).

📝 Minor Suggestions

  1. Constants: The SQL_KEYWORDS array in route.tsx:2042-2070 could be imported from the shared tsql package.

  2. Accessibility: Ensure the CodeMirror editor has proper ARIA labels and keyboard navigation.

  3. Documentation: Consider adding JSDoc comments to the main entry points (compileTSQL, parseTSQLSelect).

  4. Error Messages: Some error messages could be more user-friendly. For example, schema validation errors could include suggestions for correct column names.


🏁 Summary

This is a high-quality, well-thought-out implementation of a complex feature. The security model is solid, the architecture is clean, and the test coverage is good. The main areas for improvement are:

  1. Performance optimization for field mappings
  2. Splitting the large route file into smaller components
  3. Adding component-level tests
  4. Implementing rate limiting and query size limits

Recommendation: ✅ Approve with minor suggestions

Great work on this substantial feature!

@claude
Copy link

claude bot commented Jan 9, 2026

PR Review: TRQL and the Query Page

Overview

This is a substantial PR (~43k additions) introducing TRQL (Trigger Query Language), a TypeScript port of PostHog HogQL that provides a safe SQL query interface for ClickHouse with tenant isolation.


Strengths

Security Architecture

  • Excellent tenant isolation: Automatic injection of organization/project/environment guards into all queries, including subqueries, JOINs, and UNIONs
  • Parameterized queries: String values are properly parameterized with typed placeholders, preventing SQL injection
  • Table allowlisting: Only schema-defined tables are queryable; system tables and unknown tables are rejected
  • Function allowlisting: Only known safe functions are permitted; file access functions are blocked
  • Stacked query prevention: Parser correctly rejects multi-statement queries with semicolons
  • Comprehensive security tests: The security.test.ts file (601 lines) thoroughly covers cross-tenant attacks, SQL injection vectors, and edge cases

Code Quality

  • Well-documented: Good JSDoc comments throughout, NOTICE.md for attribution, clear README
  • Proper separation of concerns: Parser, AST, printer, validator, and schema layers are cleanly separated
  • Type safety: Extensive use of TypeScript types for AST nodes, column metadata, and schema definitions
  • Error handling: Custom error classes (SyntaxError, QueryError, NotImplementedError) with position information
  • Escape utilities: Proper character escaping for identifiers and string values with test coverage

Feature Completeness

  • Virtual columns: Support for computed columns via expressions (e.g., execution_duration computed from timestamps)
  • Value mapping: User-friendly value display (e.g., Completed displayed but COMPLETED_SUCCESSFULLY stored)
  • Runtime field mappings: Dynamic value translation for references like project IDs
  • AI query generation: Integration with OpenAI for natural language query generation with validation tools
  • Rich result metadata: Output column types and custom render hints preserved through the pipeline

Suggestions for Improvement

1. AI Service Security Consideration

File: apps/webapp/app/v3/services/aiQueryService.server.ts

The AI service uses gpt-4o-mini by default. While the generated queries are validated, consider:

  • Adding rate limiting for AI query generation to prevent abuse
  • Logging AI-generated queries for audit purposes

2. Missing Error Boundary in Query Route

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

The query page catches errors in the action but consider adding an ErrorBoundary component for unexpected React errors.

3. Query History Cleanup

Question: Is there a mechanism to clean up old query history entries? The CustomerQueryHistory table could grow large over time. Consider adding:

  • TTL-based cleanup
  • Max history entries per user/org

4. Consider Adding Query Size Limits

The printer has maxRows settings, but consider also limiting:

  • Maximum query complexity (number of JOINs, subquery depth)
  • Maximum query string length before parsing

5. Minor: Console Logger in Production

File: apps/webapp/app/v3/querySchemas.ts:406

This debug log in the bulk_action_group_ids whereTransform should be removed or guarded with a debug flag for production.


Test Coverage

Strong test coverage overall:

  • 4,676 lines of tests in the TSQL package
  • Security tests cover cross-tenant attacks, SQL injection, and edge cases
  • Parser tests validate AST generation
  • Printer tests verify SQL output with tenant guards
  • Escape tests cover character encoding edge cases
  • Schema tests validate column/table resolution

Suggestion: Add Integration Tests

Consider adding end-to-end tests that actually execute queries against a test ClickHouse instance to verify:

  • Real query execution with tenant isolation
  • Performance with large result sets
  • Proper handling of ClickHouse-specific types

Security Checklist

  • Tenant ID injection in all query types (SELECT, JOIN, UNION, subqueries)
  • Parameterized values for strings
  • Table name allowlisting
  • Function allowlisting
  • Statement stacking prevention
  • Identifier escaping
  • Comment injection handling
  • Feature flag access control (hasQueryAccess)

Summary

This is a well-architected PR that introduces a powerful and secure query system. The security model is sound, with proper tenant isolation at the SQL generation level. The code quality is high with good separation of concerns and extensive test coverage.

Recommended: Approve with minor suggestions above.


Review generated by Claude

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (8)
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx (8)

120-123: Consider a more specific redirect or forbidden response.

Currently, users without query access are redirected to the home page ("/"). Consider returning a 403 response with a message or redirecting to a more contextual location (e.g., the environment page).

Alternative approach
  const canAccess = await hasQueryAccess(user.id, user.admin, organizationSlug);
  if (!canAccess) {
-   throw redirect("/");
+   throw new Response("You don't have access to query features", {
+     status: 403,
+     statusText: "Forbidden",
+   });
  }

346-357: Chart config resets on initial query execution.

The useEffect resets chartConfig whenever columnsKey changes. On the initial load, columnsKey is an empty string (no results yet). After the first query execution, columnsKey becomes non-empty, triggering the reset. This means users lose their chart configuration even on the very first query, which may not be the intended behavior.

Consider tracking whether this is truly a schema change vs. initial population:

Potential fix
+ const prevColumnsKeyRef = useRef<string>("");
+
  // Reset chart config only when column schema actually changes
  // This allows re-running queries with different WHERE clauses without losing config
  useEffect(() => {
-   if (columnsKey) {
+   if (columnsKey && prevColumnsKeyRef.current && columnsKey !== prevColumnsKeyRef.current) {
      setChartConfig(defaultChartConfig);
    }
+   prevColumnsKeyRef.current = columnsKey;
  }, [columnsKey]);

697-704: Consider using a timestamp key instead of setTimeout for re-triggering.

The current pattern (setting to undefined, then back to the value after setTimeout(0)) forces a re-render to re-trigger the same prompt. While functional, this relies on timing and state updates.

Alternative approach

Use a tuple with a timestamp or counter to ensure uniqueness:

- const [autoSubmitPrompt, setAutoSubmitPrompt] = useState<string | undefined>();
+ const [autoSubmitPrompt, setAutoSubmitPrompt] = useState<{ prompt: string; key: number } | undefined>();

  // In AIQueryInput, destructure: autoSubmitPrompt?.prompt

  // In the click handler:
  onClick={() => {
-   // Use a unique key to ensure re-trigger even if same prompt clicked twice
-   setAutoSubmitPrompt(undefined);
-   setTimeout(() => setAutoSubmitPrompt(example), 0);
+   setAutoSubmitPrompt({ prompt: example, key: Date.now() });
  }}

918-1875: Consider externalizing the function catalog.

The inline function documentation spans nearly 1,000 lines (lines 918-1875), making this file very large (2,169 lines total). While comprehensive, this approach has maintainability implications:

  • Duplicates documentation that might exist elsewhere (e.g., in the @internal/tsql package)
  • Makes the route file harder to navigate
  • Increases bundle size if not properly code-split

Consider moving the function catalog to a separate file or data structure that can be imported:

// Create: app/v3/tsqlFunctionCatalog.ts
export const tsqlFunctionCatalog = [
  {
    title: "Aggregate functions",
    functions: [
      { name: "count()", desc: "Count rows", example: "count()" },
      // ...
    ]
  },
  // ...
];

// Then in this route:
import { tsqlFunctionCatalog } from "~/v3/tsqlFunctionCatalog";

This would improve maintainability and allow reuse across the application.


2073-2112: Remove unused suffix variable.

The suffix variable is defined as an empty string at line 2076 and only checked at lines 2107-2109, but never assigned a non-empty value. This appears to be dead code.

Cleanup
  function highlightSQL(query: string): React.ReactNode[] {
    // Normalize whitespace for display (let CSS line-clamp handle truncation)
    const normalized = query.replace(/\s+/g, " ").slice(0, 200);
-   const suffix = "";

    // ... rest of function ...

    // Add remaining text
    if (lastIndex < normalized.length) {
      parts.push(normalized.slice(lastIndex));
    }

-   if (suffix) {
-     parts.push(suffix);
-   }

    return parts;
  }

2075-2075: Hard-coded query length limit may truncate important information.

The highlightSQL function truncates queries to 200 characters. For longer queries in the history popover, users may not see enough context to distinguish between similar queries.

Consider making this configurable or increasing the limit:

- const normalized = query.replace(/\s+/g, " ").slice(0, 200);
+ const normalized = query.replace(/\s+/g, " ").slice(0, 300);

Or extract as a constant:

const MAX_QUERY_PREVIEW_LENGTH = 200;

1-2169: Consider splitting this large route file into multiple modules.

This file contains 2,169 lines mixing route handlers, UI components, documentation content, and utilities. While well-organized, the size impacts maintainability and violates the single-responsibility principle.

Consider this structure:

route.tsx (loader, action, main Page) - ~200 lines
├── components/
│   ├── QueryEditorForm.tsx
│   ├── QueryHelpSidebar.tsx
│   ├── ExportResultsButton.tsx
│   └── QueryHistoryPopover.tsx
├── content/
│   ├── tsqlFunctionCatalog.ts (function documentation)
│   ├── exampleQueries.ts
│   └── TRQLGuideContent.tsx
└── utils/
    ├── queryFormatting.ts (highlightSQL, formatQueryStats, formatBytes)
    └── queryAccess.server.ts (hasQueryAccess)

This would improve:

  • Maintainability: Smaller, focused modules
  • Testability: Easier to unit test isolated functions
  • Reusability: Components/utils can be imported elsewhere
  • Navigation: Easier to find and modify specific functionality

205-220: Consider using a more specific output schema instead of z.record(z.any()).

The z.record(z.any()) schema at line 209 accepts any record structure without validation. While the tableSchema parameter validates the query itself and later transforms the results, the result rows bypass strict runtime validation. This approach loses compile-time type safety and could mask unexpected data shapes.

Define a more specific output schema that reflects the expected query result structure, or document why a fully permissive schema is appropriate for this use case.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7e6673b and ed058eb.

📒 Files selected for processing (1)
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access all environment variables through the env export of env.server.ts instead of directly accessing process.env in the Trigger.dev webapp

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: When importing from @trigger.dev/core in the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx
**/*.{js,ts,jsx,tsx,json,md,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx
🧠 Learnings (4)
📓 Common learnings
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to internal-packages/database/**/*.{ts,tsx} : Use Prisma for database interactions in internal-packages/database with PostgreSQL
📚 Learning: 2025-12-08T15:19:56.823Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2760
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx:278-281
Timestamp: 2025-12-08T15:19:56.823Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx, the tableState search parameter uses intentional double-encoding: the parameter value contains a URL-encoded URLSearchParams string, so decodeURIComponent(value("tableState") ?? "") is required to fully decode it before parsing with new URLSearchParams(). This pattern allows bundling multiple filter/pagination params as a single search parameter.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: The webapp at apps/webapp is a Remix 2.1 application using Node.js v20

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx
🧬 Code graph analysis (1)
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx (10)
apps/webapp/app/utils/pathBuilder.ts (1)
  • EnvironmentParamSchema (26-28)
apps/webapp/app/presenters/v3/QueryPresenter.server.ts (2)
  • QueryPresenter (13-43)
  • QueryHistoryItem (5-11)
apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx (1)
  • action (18-181)
apps/webapp/app/services/queryService.server.ts (2)
  • executeQuery (50-116)
  • QueryScope (16-16)
apps/webapp/app/v3/querySchemas.ts (2)
  • querySchemas (417-417)
  • defaultQuery (422-425)
apps/webapp/app/components/code/TSQLResultsTable.tsx (1)
  • TSQLResultsTable (402-459)
internal-packages/tsql/src/index.ts (2)
  • OutputColumnMetadata (73-73)
  • ColumnSchema (68-68)
apps/webapp/app/components/primitives/Popover.tsx (5)
  • Popover (242-242)
  • PopoverArrowTrigger (243-243)
  • PopoverContent (244-244)
  • PopoverMenuItem (246-246)
  • PopoverTrigger (249-249)
apps/webapp/app/components/primitives/CopyableText.tsx (1)
  • CopyableText (8-98)
apps/webapp/app/components/primitives/DateTime.tsx (1)
  • DateTime (54-98)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (24)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: claude-review
  • GitHub Check: Analyze (javascript-typescript)

@claude
Copy link

claude bot commented Jan 9, 2026

PR Review: TRQL and the Query Page

This is a substantial and well-architected PR that introduces TRQL (TRigger Query Language), a secure query interface for users to explore their ClickHouse data. The implementation demonstrates strong security practices and thoughtful design. Here's my detailed review:


🟢 Strengths

Security Architecture (Excellent)

The security model is well-designed with multiple layers of protection:

  1. Tenant Isolation: Automatic injection of tenant guards (organization_id, project_id, environment_id) into every query, including subqueries, JOINs, and UNIONs. The comprehensive security tests in security.test.ts cover critical scenarios including:

    • OR clause bypass attempts
    • Cross-tenant data access prevention
    • UNION-based injection attempts
    • Subquery isolation
  2. SQL Injection Prevention:

    • Parameterized queries with typed parameters ({param: Type})
    • Function allowlisting prevents access to dangerous functions like file() or system.*
    • Table allowlisting ensures users can only query approved tables
    • The escape utilities in escape.ts handle special characters properly
  3. Query Parsing & Validation: Using ANTLR4 for grammar-based parsing provides a robust foundation. The parser rejects stacked statements and validates syntax before execution.

Test Coverage (Good)

  • Security tests are comprehensive (security.test.ts - 600+ lines)
  • Integration tests with ClickHouse testcontainers
  • Unit tests for parser, printer, escape utilities, schema validation, and results transformation
  • Frontend tests for TSQL linter and autocompletion

Code Quality

  • Clean separation of concerns between grammar, parsing, printing, validation, and execution
  • Well-documented TypeScript interfaces and JSDoc comments
  • Proper error handling with custom error classes

🟡 Suggestions for Improvement

1. Rate Limiting & Resource Protection

The query endpoint should have rate limiting to prevent abuse:

// Consider adding in route.tsx action:
// - Per-user query rate limits
// - Query cost estimation before execution

The CENTS_PER_QUERY_BYTE_SECOND billing mechanism is good, but consider adding a cost estimation step before query execution to reject potentially expensive queries early.

2. Query Timeout Configuration

I see querySettings supports configuration but the default timeout should be documented and enforced at the route level:

// In queryService.server.ts, consider:
const MAX_QUERY_TIMEOUT_MS = 30000;
// Validate and cap user-provided timeouts

3. AI Query Generation Security

The AI query service (aiQueryService.server.ts) validates generated queries before returning them, which is good. However:

  • Consider adding a flag to log AI-generated queries separately for monitoring
  • The extractQueryFromText function extracts SQL from markdown code blocks - ensure edge cases are handled (nested blocks, malformed markdown)

4. Error Message Information Leakage

Be cautious about exposing detailed error messages to users:

// In route.tsx:242
const errorMessage = err instanceof Error ? err.message : "Unknown error executing query";

Some ClickHouse error messages might leak schema information. Consider sanitizing error messages before returning them to the frontend.

5. Session/History Privacy

The CustomerQuery table stores raw queries. Consider:

  • Adding data retention policies (auto-cleanup after N days)
  • Ensuring query history respects any data privacy requirements
  • The scope is tracked, but sensitive filter values in queries are persisted

6. Frontend Performance

For TSQLResultsTable.tsx with large result sets:

  • Consider virtualization for tables with 1000+ rows
  • The current implementation renders all rows which could impact performance

🔴 Potential Issues

1. Missing Input Validation for scope in Service Layer

In queryService.server.ts, the scope determines tenant isolation, but there's no validation that the user has permission for the requested scope:

// Line 64-69: Scope is trusted from the route
if (scope === "organization") {
  // User could potentially query all projects if they shouldn't have org-wide access
}

Recommendation: Add authorization checks to verify the user can query at the requested scope level.

2. Empty projectId with Environment Scope

In executeQuery, if scope === "environment" but projectId is undefined/empty, the query still executes with only org isolation:

tenantOptions.projectId = projectId; // Could be undefined

Recommendation: Validate that required tenant IDs are present for the given scope.

3. OpenAI API Key Exposure Check

The AI route correctly checks for OPENAI_API_KEY, but the error message should be generic for production:

// Line 70-81 in ai-generate.tsx
error: "OpenAI API key is not configured"
// Should be: "AI query generation is not available"

🧪 Test Suggestions

Consider adding tests for:

  1. Authorization boundary tests - Verify users can't query above their permission level
  2. Malformed input handling - Very long queries, binary data in query strings
  3. Concurrent query execution - Verify isolation under load
  4. Query cost estimation accuracy - If implementing cost limits

📊 Summary

Category Rating
Security ⭐⭐⭐⭐⭐
Code Quality ⭐⭐⭐⭐
Test Coverage ⭐⭐⭐⭐
Documentation ⭐⭐⭐⭐
Performance ⭐⭐⭐

Overall: This is a well-executed feature with strong security foundations. The concerns raised are mostly hardening suggestions rather than blockers. The security model with automatic tenant guard injection is particularly well done and should serve as a good pattern for similar features.

Great work on the ANTLR grammar port and the comprehensive security test suite! 🎉


Review generated by Claude

@claude
Copy link

claude bot commented Jan 9, 2026

PR Review: TRQL and the Query Page

Overview

This is an impressive, substantial PR introducing TRQL (TRiggerQueryLanguage) - a type-safe, tenant-safe query language that transpiles to ClickHouse SQL, along with a new Query Page UI. The PR adds ~43,000 lines across 100 files, which is a significant addition warranting careful review.


Code Quality and Best Practices

Strengths:

  • Proper attribution of PostHog's HogQL (MIT Licensed) in NOTICE.md
  • Well-structured package organization with clear separation of concerns (grammar, parsing, printing, validation, schema)
  • Comprehensive TypeScript typing throughout
  • Consistent use of Zod for runtime validation
  • Feature flag gating (hasQueryAccess) ensures safe rollout
  • Good use of existing patterns in the codebase (presenters, services, etc.)

Suggestions:

  1. Route file size: The main query route at apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx is 2,168 lines. Consider splitting into smaller components/hooks for maintainability.

  2. Generated parser code: The ANTLR-generated TSQLParser.ts is ~11,700 lines. Ensure the .gitignore and build process are set up correctly so regeneration is documented and CI can verify grammar changes.


Potential Bugs or Issues

  1. Query History Limit: The implementation stores only 20 most recent queries per organization. For high-volume users, this may be insufficient. Consider:

    • Making the limit configurable
    • Adding pagination for history access
    • Documenting this limitation for users
  2. Error Handling in AI Service: The AI query generation service (aiQueryService.server.ts) should ensure graceful degradation if OpenAI API is unavailable or rate-limited.

  3. Data Export Sizes: CSV/JSON export functionality doesn't appear to have size limits. Large result sets could cause memory issues or slow downloads. Consider:

    • Adding a row limit warning
    • Streaming exports for large datasets
    • Client-side pagination for exports
  4. Cost Tracking Field: The CustomerQuery table has a costInCents field, but the actual cost calculation implementation doesn't appear to be included. Ensure this is implemented before billing features are enabled.


Performance Considerations

  1. Query Execution Timeouts: Good to see execution time limits are implemented. Verify these are enforced at the ClickHouse level as well as application level.

  2. ANTLR Parser Performance: The generated parser is large. For the editor's linting/autocomplete, ensure parsing happens efficiently (debounced, in a worker, etc.) to maintain responsive UX.

  3. ClickHouse Query Optimization: Consider adding query plan analysis or query cost estimation before execution to prevent expensive queries from impacting the cluster.

  4. Client-Side State: The Query page appears to maintain significant client-side state. Ensure proper cleanup on unmount and consider persistence strategies for query drafts.


Security Concerns

Strong Security Foundations:

  • Automatic tenant isolation via WHERE clause injection (organization_id, project_id, environment_id)
  • Parameter binding prevents SQL injection
  • ANTLR-based parsing prevents malformed queries
  • Comprehensive security tests in security.test.ts

Recommendations:

  1. Rate Limiting for AI Service: The OpenAI integration should have rate limiting to prevent abuse and control costs. Consider per-user or per-organization limits.

  2. API Key Security: Ensure OPENAI_API_KEY is properly secured in environment configuration and not logged.

  3. Query Audit Trail: The CustomerQuery table tracks query history, which is good for audit purposes. Consider adding IP address or user agent for security investigations.

  4. Input Validation: Verify that the query input is sanitized for any potential XSS when displaying in the UI (especially in error messages or the AI response rendering).

  5. Feature Flag Enforcement: Ensure hasQueryAccess is checked on both loader and action functions, not just the UI rendering.


Test Coverage

Good Coverage:

  • tsqlCompletion.test.ts - Editor autocomplete tests
  • tsqlLinter.test.ts - Linting functionality tests
  • parser.test.ts - Grammar parsing tests
  • escape.test.ts - String escaping tests
  • printer.test.ts - SQL output generation (~2,244 lines)
  • schema.test.ts - Schema validation tests
  • security.test.ts - Security boundary tests (~601 lines)
  • validator.test.ts - Query validation tests
  • tsql.test.ts - ClickHouse integration tests (~1,548 lines)

Suggestions:

  1. Add integration tests for the Query page route (loader/action functions)
  2. Add E2E tests for the full user flow (write query → execute → view results)
  3. Consider property-based testing for the parser to catch edge cases
  4. Add tests for the AI query generation with mocked OpenAI responses

Additional Observations

  1. Dependencies Added:

    • streamdown - Animated markdown rendering
    • recharts - Charting library
    • @ai-sdk/openai + ai - Vercel AI SDK

    These are reasonable choices but ensure they're audited for security and bundle size impact.

  2. Database Migration: The new CustomerQuery table migration looks correct with proper foreign key relationships and cascading deletes.

  3. ClickHouse Migration: New column added via 012_add_task_runs_v2_max_duration.sql - ensure this runs correctly in all environments.

  4. Documentation: Consider adding:

    • User documentation for TRQL syntax and available functions
    • Developer documentation for extending the schema
    • Architecture decision records for the language design choices

Summary

This is a well-architected, security-conscious feature addition. The TSQL engine demonstrates solid engineering with proper tenant isolation and comprehensive testing. The main areas for improvement are:

  1. Breaking up large files for maintainability
  2. Adding rate limiting for the AI service
  3. Implementing the cost tracking logic
  4. Adding limits to data exports
  5. Expanding test coverage for integration/E2E scenarios

Overall, this PR represents significant work and the foundations are solid.

@claude
Copy link

claude bot commented Jan 9, 2026

PR Review: TRQL and the Query Page

This is an impressive and comprehensive addition to Trigger.dev, introducing a full query language engine (TRQL) derived from PostHog's HogQL. I've reviewed the code across the @internal/tsql package, ClickHouse integration, webapp components, and database migrations.

Overall Assessment: ✅ Strong Implementation

This PR demonstrates solid software engineering practices with clear separation of concerns, comprehensive security measures, and good test coverage. Below are my detailed findings:


🟢 Strengths

Security (Excellent)

  • Tenant isolation is properly enforced through automatic WHERE clause injection in printer.ts:1366-1424 - organization_id is always required, project_id/environment_id are optional
  • SQL injection prevention via parameterized queries - user values never interpolated directly into SQL (printer_context.ts addValue method)
  • Function allowlisting - only known safe functions are permitted (functions.ts)
  • Table/column validation against schema registry prevents arbitrary table access
  • Comprehensive security tests in security.test.ts covering:
    • Cross-tenant access prevention
    • SQL injection vectors (string injection, comment injection, identifier injection, stacked queries)
    • UNION-based bypass attempts
    • Subquery tenant guard enforcement

Code Quality

  • Clean TypeScript with proper type definitions throughout
  • Well-documented code with JSDoc comments explaining purpose and behavior
  • Consistent error handling with custom error types (QueryError, SyntaxError, ImpossibleASTError)
  • Good separation between parsing (ANTLR4), validation, and SQL generation
  • The printer implementation (printer.ts) properly handles AST traversal with visitor pattern

Test Coverage

Found 8 test files in the tsql package:

  • grammar/parser.test.ts
  • query/parser.test.ts
  • query/printer.test.ts
  • query/escape.test.ts
  • query/security.test.ts
  • query/schema.test.ts
  • query/results.test.ts
  • query/validator.test.ts

Plus webapp component tests for autocompletion and linting.

Database Migration

The CustomerQuery table migration is well-structured:

  • Proper indexes for common query patterns (organizationId + createdAt, createdAt)
  • Appropriate CASCADE delete behavior on foreign keys
  • Cost tracking for billing purposes

🟡 Suggestions for Improvement

1. Query Result Size Limits

In queryService.server.ts:98, the cost calculation uses byte_seconds:

const byteSeconds = parseFloat(stats.byte_seconds) || 0;
const costInCents = byteSeconds * env.CENTS_PER_QUERY_BYTE_SECOND;

Consider adding:

  • A maximum query cost limit to prevent expensive queries
  • A timeout for long-running queries at the ClickHouse level (I see maxExecutionTime in settings, ensure it's being used)

2. Error Handling in QueryService

queryService.server.ts:95 - The history recording happens after successful query:

if (result[0] === null && history) {

Consider also logging failed queries for debugging/monitoring purposes, perhaps with a separate flag.

3. Editor UX

In TSQLEditor.tsx:186-188, silent failure on format error:

} catch {
  // If formatting fails (e.g., invalid SQL), silently ignore
}

Consider showing a toast notification to inform users why formatting didn't work.

4. Input Validation

In printer.ts:1289-1290, table names are validated but the error message could include which tables ARE available:

if (typeof tableName \!== "string") {
  throw new QueryError("Table name must be a string");
}

The validator.ts does this well - consider adding similar context to printer errors.

5. Escape Function Edge Case

In escape.ts:74, the regex for valid identifiers:

if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) {

The tests in escape.test.ts should verify edge cases like identifiers starting with numbers or containing only special chars.


🔴 Potential Issues

1. Memory Usage in Large Results

The result transformation in results.ts processes all rows in memory. For very large result sets, consider:

  • Streaming results
  • Adding a hard limit on result set size
  • Pagination at the TSQL level

2. Rate Limiting

The query endpoint doesn't appear to have rate limiting. Malicious users could potentially:

  • Execute many expensive queries in parallel
  • DoS the ClickHouse cluster

Recommend adding rate limiting per organization/user.

3. Index Usage Verification

The CustomerQuery migration creates indexes, but uses regular CREATE INDEX instead of CONCURRENTLY as mentioned in CONTRIBUTING.md:179:

If you're adding any database indexes they must use CONCURRENTLY, otherwise they'll lock the table when executed.

However, since this is a new table being created in the same migration, this should be fine for initial deployment.


📋 Minor Nits

  1. NOTICE.md Attribution - Great job including proper PostHog HogQL attribution!

  2. TypeScript any usage - In printer.ts some cases use type assertions heavily. Consider adding more specific types where feasible.

  3. Magic numbers - In TSQLEditor.tsx:112, the linter delay is 300ms. Consider extracting to a named constant.


Summary

This is a well-architected feature with strong security fundamentals. The TRQL engine is comprehensive, the security model is sound with mandatory tenant isolation, and the test coverage is good.

The main areas to address are:

  1. Rate limiting for the query endpoint
  2. Memory management for large result sets
  3. Better user feedback on errors

Great work on this significant addition! 🎉

@claude
Copy link

claude bot commented Jan 9, 2026

PR Review: TRQL and the Query Page

This is a substantial PR introducing TRQL (TRigger Query Language), a custom query language for safely querying ClickHouse with tenant isolation. Overall, this is an impressive and well-architected feature. Here's my detailed review:


Security (Excellent) ✅

The security implementation is thorough and well-designed:

Tenant Isolation:

  • Organization, project, and environment ID guards are automatically injected into every query via createTenantGuard() in printer.ts:1366-1424
  • Guards are properly applied to all tables in JOINs, subqueries, and UNIONs
  • The security.test.ts has comprehensive tests for cross-tenant access prevention, including edge cases like OR 1=1 bypass attempts

SQL Injection Prevention:

  • String values are properly parameterized (never inlined into SQL)
  • Identifiers are validated and escaped via escape.ts
  • Unknown functions are rejected (printer.ts:2163-2164)
  • System tables and database prefixes are blocked
  • Stacked statements (semicolons) are rejected at parse time
  • Parameterized queries with type annotations like {tsql_param: String} add another layer of safety

Table Allowlisting:

  • Only tables defined in tableSchema can be queried
  • Attempts to query system.tables or unknown tables throw errors

Code Quality (Good) 👍

Strengths:

  • Clean separation between grammar (ANTLR), parsing, AST, validation, and printing
  • Well-documented code with JSDoc comments explaining security considerations
  • TypeScript port from PostHog's HogQL is well-acknowledged in NOTICE.md
  • Good use of Zod for schema validation throughout

Areas for Improvement:

  1. Duplicate code in AI service - extractQueryFromText() is duplicated between aiQueryService.server.ts:472-486 and ai-generate.tsx:190-204. Consider extracting to a shared utility.

  2. Magic strings in printer.ts - Status values like 'COMPLETED_SUCCESSFULLY', 'COMPLETED_WITH_ERRORS' etc. in the is_finished expression (querySchemas.ts:99-101) could be defined as constants.

  3. Error handling in queryService.server.ts:96-98 - The parseFloat(stats.byte_seconds) could fail silently if the value is unexpected. Consider adding validation:

const byteSeconds = parseFloat(stats.byte_seconds);
if (Number.isNaN(byteSeconds)) {
  logger.warn('Invalid byte_seconds value', { value: stats.byte_seconds });
}

Performance Considerations (Good) 👍

Positives:

  • whereTransform allows using raw column values for efficient index usage in WHERE clauses while showing user-friendly values in results
  • Query execution has configurable maxRows limit
  • Field mappings loaded per-query from database are efficient for the expected use case

Potential Concerns:

  1. Field mappings query - In queryService.server.ts:73-86, two database queries are made for every query execution to load project and environment mappings:
const projects = await prisma.project.findMany({...});
const environments = await prisma.runtimeEnvironment.findMany({...});

For organizations with many projects/environments, consider caching these mappings or fetching only what's needed.

  1. AI query generation - maxSteps: 5 in the AI service could potentially lead to multiple LLM round-trips. The streaming implementation mitigates user-perceived latency, but monitor for cost/performance.

Test Coverage (Good) 👍

The TSQL package has excellent test coverage:

  • security.test.ts - 600+ lines of security tests
  • printer.test.ts - 2200+ lines
  • escape.test.ts - 223 lines
  • parser.test.ts - 629 lines
  • schema.test.ts - 456 lines
  • validator.test.ts - 124 lines
  • results.test.ts - 234 lines

Missing tests:

  • No integration tests for the query page route (route.tsx)
  • No tests for queryService.server.ts
  • The AI service has evals (aiQuery.eval.ts) but no unit tests

Potential Issues 🔍

  1. Missing changeset - The changeset bot warns there's no changeset. If this is shipping as a feature, a changeset should be added.

  2. Feature flag check in AI route - The ai-generate.tsx route doesn't check hasQueryAccess like the main query route does. Consider adding the same access check:

// ai-generate.tsx:18-19
const canAccess = await hasQueryAccess(userId, user.admin, organizationSlug);
if (!canAccess) {
  return new Response(JSON.stringify({ type: "result", success: false, error: "Unauthorized" }), { status: 403, headers: { "Content-Type": "application/json" } });
}
  1. Hard-coded model fallback - In ai-generate.tsx:88:
openai(env.AI_RUN_FILTER_MODEL ?? "gpt-4o-mini")

The env var name AI_RUN_FILTER_MODEL suggests it's for a different feature. Consider adding a dedicated AI_QUERY_MODEL env var.

  1. Query history cost calculation - queryService.server.ts:98 uses env.CENTS_PER_QUERY_BYTE_SECOND. If this env var is missing, costInCents would be NaN. Add a fallback:
const costInCents = byteSeconds * (env.CENTS_PER_QUERY_BYTE_SECOND ?? 0);

Summary

This is a well-designed feature with strong security foundations. The tenant isolation and SQL injection prevention are particularly well-implemented. The main areas to address before merging:

  1. Critical: Add hasQueryAccess check to the AI generate route
  2. High: Add a changeset if this is a user-facing feature
  3. Medium: Consider caching field mappings for performance
  4. Low: Extract duplicate extractQueryFromText() function

Great work on this substantial feature! The TSQL architecture provides a solid foundation for safe user queries.

@claude
Copy link

claude bot commented Jan 9, 2026

PR Review: TRQL and Query Page

This is an impressive, well-architected feature! The TSQL implementation demonstrates a strong defense-in-depth security approach. Here's my detailed review:


🟢 Strengths

Security Architecture

  • Excellent SQL injection prevention: The combination of ANTLR4 parsing, parameterized queries, and function allowlisting provides robust protection
  • Strong tenant isolation: Automatic WHERE guard injection on all tables (including JOINs and subqueries) is well-implemented
  • Comprehensive security tests: 600+ lines of dedicated security tests covering injection vectors, tenant bypass attempts, and edge cases
  • Schema-based validation: Users can only query tables/columns explicitly defined in the schema

Code Quality

  • Clean separation of concerns between parser, validator, printer, and execution layers
  • Type-safe data handling with Zod schema validation
  • Proper authorization checks at the route level with feature flag gating
  • Good use of TypeScript patterns throughout

Test Coverage

  • Extensive test suites for parsing, validation, printing, and security
  • Tests cover SQL injection scenarios, tenant isolation bypasses, and edge cases

🟡 Areas for Improvement

High Priority

1. Missing authorization check in AI endpoint

The AI generation endpoint (resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx) does not verify the hasQueryAccess feature flag. A user could potentially use the AI endpoint without having query access:

// Current - only checks project access
export async function action({ request, params }: ActionFunctionArgs) {
  const userId = await requireUserId(request);
  const project = await findProjectBySlug(organizationSlug, projectParam, userId);
  // Missing: hasQueryAccess check
}

Recommendation: Add the same hasQueryAccess check from the main query route.

2. No rate limiting

The query endpoints allow unlimited:

  • Query executions (potentially expensive ClickHouse queries)
  • AI query generations (OpenAI API calls with associated costs)

Recommendation: Implement rate limiting at multiple levels:

  • Per-user query rate limits
  • Per-organization daily query limits
  • AI generation rate limits (especially important due to API costs)

Medium Priority

3. No input length validation

The Zod schemas don't include maximum length constraints:

// Current
prompt: z.string().min(1, "Prompt is required"),  // No .max()
query: z.string().min(1, "Query is required"),    // No .max()

Recommendation: Add .max() constraints (e.g., 10000 chars for queries, 1000 for prompts) to prevent oversized payloads.

4. Error message exposure

ClickHouse error messages may expose internal details to users. Consider sanitizing error messages before returning them to the client (full details can be logged server-side).

Low Priority

5. Query complexity limits

While there's a maxRows setting, consider adding limits on:

  • Number of joins
  • Subquery depth
  • Function nesting depth

This would prevent resource exhaustion from complex queries.


🔍 Security Notes

The security implementation is well-designed:

Aspect Implementation Assessment
SQL Injection Parameterized queries + function allowlist ✅ Robust
Tenant Isolation Automatic WHERE guards on all tables ✅ Robust
Authorization Feature flag + scope-based filtering ✅ Good (with AI endpoint fix)
Test Coverage 600+ lines security tests ✅ Excellent

📝 Minor Suggestions

  1. Consider adding integration tests that execute actual queries against a test ClickHouse instance to verify tenant isolation in practice
  2. Document the security model in a README (how guards work, what's validated, etc.)
  3. The whereTransform function in column schemas is powerful but could be dangerous if misused - consider adding validation or documentation around safe usage

Summary

This is a high-quality PR with strong security fundamentals. The main actionable items are:

  1. Add hasQueryAccess check to the AI endpoint (high priority)
  2. Implement rate limiting (high priority)
  3. Add input length validation to Zod schemas (medium priority)

Great work on the comprehensive security test coverage and the defense-in-depth approach! 🎉

@claude
Copy link

claude bot commented Jan 9, 2026

PR Review: Query Page with TSQL Query Language

This is a comprehensive and well-architected feature that adds a SQL query interface for task run analytics. I've reviewed the implementation across the TSQL package, ClickHouse client integration, webapp components, and security considerations.


Security Review

Strengths - This PR has excellent security practices:

  1. Multi-tenant Isolation - The createTenantGuard in printer.ts:1366-1424 automatically injects tenant isolation filters (organization_id, project_id, environment_id) into every query. The guards use AND composition ensuring user-provided OR clauses cannot bypass isolation.

  2. Parameterized Queries - String values are always parameterized via PrinterContext.addValue() (see printer.ts:1843), preventing SQL injection. The comprehensive security tests in security.test.ts verify:

    • Stacked statement prevention
    • Quote and backslash escape handling
    • UNION injection protection
    • Comment injection prevention
  3. Table/Function Allowlisting - Only tables defined in querySchemas are accessible. Unknown tables throw errors (printer.ts:2271-2273). Functions are explicitly allowlisted in functions.ts.

  4. Access Control - The hasQueryAccess function (route.tsx:77-108) properly checks user permissions via feature flags before allowing query execution.

Potential Improvements:

  1. Rate Limiting - Consider adding rate limiting to the query endpoint to prevent abuse. A user could potentially execute expensive queries in rapid succession.

  2. Query Cost Limits - While there's CENTS_PER_QUERY_BYTE_SECOND tracking in queryService.server.ts:98, consider adding a max byte-seconds limit per query to prevent extremely expensive queries.


Code Quality

Strengths:

  1. Well-structured Package Design - The @internal/tsql package cleanly separates concerns: parser, AST, printer, validator, and escape utilities.

  2. Comprehensive Type Safety - Good use of TypeScript throughout, with proper Zod schemas for validation (ActionSchema in route.tsx).

  3. Test Coverage - The security.test.ts has excellent coverage of security edge cases. The test file covers:

    • Cross-tenant data access prevention
    • SQL injection vectors
    • Parameter safety
    • Optional tenant filter combinations

Suggestions:

  1. Error Handling Consistency - In aiQueryService.server.ts:182-186, the error detection heuristic (text.toLowerCase().includes("cannot")) is fragile. Consider having the AI return structured responses or use a more robust error detection mechanism.

  2. Magic Numbers - In route.tsx:2076, the slice(0, 200) truncation and various hardcoded limits could be extracted to constants for clarity.

  3. Component Complexity - The main route file at 2170+ lines is quite large. Consider extracting some of the helper components (like FunctionCategory, TRQLGuideContent) into separate files.


Performance Considerations

Strengths:

  1. Query Capping - The maxRows limit in PrinterContext prevents unbounded result sets.

  2. Efficient Column Resolution - The tableContexts Map provides O(1) column lookups during query compilation.

  3. Memoization - The useCallback usage in route.tsx (e.g., handleChartConfigChange) prevents unnecessary re-renders.

Suggestions:

  1. Virtual Column Efficiency - When using whereTransform columns in WHERE clauses, the code correctly uses raw columns for index efficiency (printer.ts:1869-1874). Good practice.

  2. Large Result Sets - Consider adding virtualization to TSQLResultsTable for queries returning thousands of rows. The current implementation may struggle with very large result sets in the browser.


Minor Issues

  1. Unused Variable - In route.tsx:2076, suffix is declared but always empty - could be removed.

  2. SQL_KEYWORDS Constant - In route.tsx:2043-2071, this constant is defined at the module level but only used for syntax highlighting. Consider moving it closer to the highlightSQL function or into a separate utils file.

  3. History Query - In QueryPresenter.server.ts (if it exists), ensure the history query is also properly tenant-scoped.


Summary

This is a solid, security-conscious implementation. The multi-layer defense (tenant isolation, parameterization, allowlisting, access control) is well thought out. The main areas for improvement are around operational concerns (rate limiting, cost limits) and code organization (splitting large files).

Recommendation: Approve with minor suggestions

The security model is sound, and the code quality is high. The suggestions above are improvements but not blockers.

@claude
Copy link

claude bot commented Jan 9, 2026

PR Review: TRQL and the Query Page

This is an impressive and substantial PR introducing TRQL (TRigger Query Language), a TypeScript port of PostHog's HogQL, along with a new Query page for users to execute queries on their data. Below is my review covering code quality, security, performance, and potential issues.


✅ Strengths

  1. Excellent Security Design: The tenant isolation architecture is well-designed:

    • Automatic injection of tenant guards (organization_id, project_id, environment_id) on all queries
    • Table allowlisting prevents access to system tables
    • Parameterized queries prevent SQL injection
    • Comprehensive security tests in security.test.ts covering cross-tenant access, injection vectors, and bypass attempts
  2. Robust Parser Implementation: The ANTLR-based parser with proper error handling and position tracking is well-structured.

  3. Well-Organized Schema System: The TableSchema and ColumnSchema types with virtual columns, value mappings, and field transformations provide a clean abstraction layer.

  4. Comprehensive Test Coverage: Strong test suites for parsing, printing, escaping, and security scenarios.

  5. Feature Flag Protection: The query page is properly gated behind a feature flag.


🐛 Potential Bugs / Issues

1. Missing NaN Guard in queryService.server.ts:97-98

const byteSeconds = parseFloat(stats.byte_seconds) || 0;
const costInCents = byteSeconds * env.CENTS_PER_QUERY_BYTE_SECOND;

parseFloat can return NaN, and NaN || 0 evaluates to 0, which is correct. However, if stats.byte_seconds is malformed in a way that doesn't return NaN (e.g., "123abc" parses to 123), unexpected values could slip through. Consider using Number() with isNaN() check for defensive coding.

2. Division by Zero in clickhouse/client.ts:366

const elapsedSeconds = elapsedNs / 1_000_000_000;
const byteSeconds = elapsedSeconds > 0 ? readBytes / elapsedSeconds : 0;

Good defensive coding here with the elapsedSeconds > 0 check! However, ensure this pattern is consistent across all stats calculation paths.

3. Stale Closure in AIQueryInput.tsx

In submitQuery (lines 71-165), processStreamEvent is called inside the callback but processStreamEvent is defined with its own useCallback. The submitQuery useCallback has processStreamEvent missing from its dependency array at line 164:

[isLoading, resourcePath, mode, getCurrentQuery] // missing: processStreamEvent

This could cause stale closure issues where onQueryGenerated doesn't update properly.

4. Incomplete Error Handling in Stream Processing

In AIQueryInput.tsx lines 132-140, JSON parse errors are silently ignored. While this is intentional for streaming, a malformed event could leave the UI in an inconsistent state. Consider logging these errors in development.


🔒 Security Considerations

  1. SQL Escaping: The escape utilities in escape.ts look solid, properly handling:

    • Backquote escaping for identifiers
    • Single quote escaping for strings
    • Unicode and null byte handling
  2. Tenant Isolation: The printer correctly injects tenant guards as the outermost AND condition, preventing OR clauses from bypassing tenant restrictions (verified in security tests).

  3. AI Query Generation: The AI service validates generated queries before returning them, which is good. However:

    • The AI prompt includes full schema information. Ensure this doesn't leak sensitive column names to unauthorized users (verified: access is gated by feature flag and authentication).
  4. Rate Limiting: Consider adding rate limiting to the query execution endpoint to prevent abuse.


⚡ Performance Considerations

  1. Field Mappings Query: In queryService.server.ts:73-86, two database queries are executed to build field mappings on every query:
const projects = await prisma.project.findMany({...});
const environments = await prisma.runtimeEnvironment.findMany({...});

Consider caching these mappings or lazy-loading them only when the query actually uses those columns.

  1. Parser Memory: The ANTLR parser creates many objects per query. For high-volume usage, consider parser instance reuse.

  2. Query History: The CustomerQuery table will grow with every query. Ensure there's a retention policy or cleanup job planned.


📝 Code Quality Suggestions

  1. Consistent Logging Levels: In client.ts:362, there's a mix of log and debug:
this.logger.debug("parsedSummary", parsedSummary);
// Later in the same file:
this.log("Querying clickhouse...") // inconsistent
  1. Type Safety in tsconfig.build.json: The ClickHouse package disables noImplicitAny:
"noImplicitAny": false

This weakens type safety. Consider enabling it and fixing any type issues.

  1. Interface vs Type: Per common TypeScript conventions, consider using type for object shapes instead of interface when not extending (e.g., QueryValidationResult in aiQueryService).

  2. SyntaxError Shadowing: In several files (parser.ts, errors.ts), a custom SyntaxError class shadows the global SyntaxError. Consider renaming to TSQLSyntaxError for clarity (you already do this in index.ts - use it consistently).


🧪 Test Coverage

The test coverage is generally excellent. A few suggestions:

  1. Add tests for edge cases:

    • Very long queries approaching the max limit
    • Deeply nested subqueries
    • Unicode in identifiers and values
  2. Integration tests: Consider adding end-to-end tests that actually execute queries against a test ClickHouse instance (if not already present).

  3. Test file locations: Some test files in apps/webapp/app/components/code/tsql/ might be better co-located with their source files or in a dedicated test directory.


📋 Minor Issues

  1. In QueryResultsChart.tsx, ensure the sort function handles all expected data types, not just dates.

  2. The Table.tsx copy action uses a <span> with onClick - consider using a <button> for better accessibility.

  3. Missing cleanup of AbortController in AIQueryInput.tsx when the component unmounts.


Summary

This is a well-architected feature with strong security foundations. The main areas to address before merge are:

  1. Critical: Fix the stale closure issue in AIQueryInput.tsx
  2. Important: Add NaN/edge case guards in cost calculations
  3. Recommended: Consider caching field mappings for performance
  4. Nice-to-have: Enable stricter TypeScript settings

The security model with tenant isolation is particularly well-done. The comprehensive test suite gives confidence in the implementation.

Overall: Approve with minor changes 👍

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In
@apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx:
- Around line 12-16: The RequestSchema currently allows mode="edit" without
enforcing currentQuery, causing AIQueryService.streamQuery to treat empty
currentQuery as a "new" edit; update RequestSchema so that when mode is "edit"
currentQuery is required and non-empty (e.g., use z.discriminatedUnion or
.refine to require currentQuery when mode === "edit"), and ensure any form
parsing that relies on RequestSchema (referenced by RequestSchema and
AIQueryService.streamQuery) will validate and return a clear error instead of
silently falling back to "new".
- Around line 18-21: In the action function, replace
EnvironmentParamSchema.parse(params) with
EnvironmentParamSchema.safeParse(params) and check the result; if safeParse
returns success: false, return a 400 response (e.g., new Response or Remix json
with status 400) describing the invalid route params instead of letting parse
throw; otherwise extract organizationSlug/projectParam/envParam from result.data
and continue the existing flow (keep requireUserId(request) as-is).
- Around line 70-90: The env.OPENAI_API_KEY check is validated but not used;
update the AIQueryService instantiation to pass the API key explicitly by
replacing the openai(...) call with createOpenAI({ apiKey: env.OPENAI_API_KEY,
model: env.AI_RUN_FILTER_MODEL ?? "gpt-4o-mini" }) (or equivalent createOpenAI
call used elsewhere), keep the existing env.OPENAI_API_KEY validation, and
import createOpenAI instead of openai so the validated key is actually supplied
to AIQueryService.

In @internal-packages/tsql/src/grammar/parser.test.ts:
- Around line 7-13: The parse helper currently constructs
CharStreams.fromString, TSQLLexer, CommonTokenStream and TSQLParser but doesn't
surface syntax errors; update the parse function to attach an ANTLR error
listener (remove default listeners and add a custom listener) to both the
TSQLLexer and TSQLParser that collects errors, run the parse production(s) you
need, and if any errors were recorded throw or fail the test (or return the
collected errors) so tests fail when parse errors occur; reference the parse
function, TSQLLexer, TSQLParser, and CharStreams.fromString when making the
change.
🧹 Nitpick comments (10)
internal-packages/clickhouse/src/client/client.ts (2)

234-410: Consider extracting shared logic to reduce duplication.

The queryWithStats method shares ~95% of its implementation with the existing query method. The only meaningful differences are the stats extraction (lines 346-381) and the return type.

Consider refactoring to extract the common query execution logic into a private helper method, with both query and queryWithStats calling this helper and then processing the results according to their needs. This would improve maintainability and reduce the risk of divergence when fixing bugs or adding features.

♻️ Potential refactoring approach

Extract a private method that handles the common query execution:

private async executeQuery<TIn extends z.ZodSchema<any>, TOut extends z.ZodSchema<any>>(
  req: { name: string; query: string; params?: TIn; schema: TOut; settings?: ClickHouseSettings },
  params: z.input<TIn>,
  options?: { attributes?: Record<string, string | number | boolean>; params?: BaseQueryParams }
): Promise<Result<{ res: ResultSet; unparsedRows: Array<TOut> }, QueryError>>

Then both query and queryWithStats would call this helper and post-process the results (extracting stats, shaping the return value, etc.).


346-381: Stats calculation logic is correct.

The byte_seconds calculation properly guards against division by zero (line 366), and all stats fields correctly default to "0" when the summary header is missing or fields are undefined.

However, line 362 contains what appears to be debug logging without a descriptive message. Consider removing this or making it more informative:

♻️ Suggested improvement
-          this.logger.debug("parsedSummary", parsedSummary);
+          this.logger.debug("Parsed query statistics from ClickHouse summary header", { parsedSummary });

Or remove it entirely if not needed for production debugging.

internal-packages/tsql/src/grammar/parser.test.ts (2)

15-73: LGTM! Consider adding error case tests.

The SELECT statement tests provide good structural coverage of common query patterns. The assertions correctly verify the presence of expected AST nodes.

As an optional enhancement, consider adding tests for invalid SELECT statements to verify error handling behavior.


75-128: Strengthen text-based assertions.

The expression tests use weak text-based assertions that only check if substrings exist in the concatenated AST text. This doesn't validate:

  • Parse tree structure
  • Operator precedence
  • Token types
  • Expression nesting

Consider checking AST structure instead, e.g., verifying the specific operator nodes, operand types, and tree depth.

Example of stronger assertion

For the addition test, instead of:

expect(text).toContain("1");
expect(text).toContain("2");
expect(text).toContain("+");

Consider:

// Verify it's a binary operation
const children = columnExpr.children;
expect(children?.length).toBeGreaterThan(0);
// Check for operator token
expect(columnExpr.PLUS()).toBeDefined();
// Verify left and right operands exist
// ... more structural checks
internal-packages/tsql/src/query/ast.ts (1)

248-277: Prefer string unions over enums to align with coding guidelines.

The coding guidelines specify avoiding enums in favor of string unions or const objects. Consider refactoring these enums to string unions, similar to how SetOperator is defined (lines 290-295).

♻️ Suggested refactor to string unions
-export enum ArithmeticOperationOp {
-  Add = "+",
-  Sub = "-",
-  Mult = "*",
-  Div = "/",
-  Mod = "%",
-}
+export type ArithmeticOperationOp = "+" | "-" | "*" | "/" | "%";

-export enum CompareOperationOp {
-  Eq = "==",
-  NotEq = "!=",
-  Gt = ">",
-  GtEq = ">=",
-  Lt = "<",
-  LtEq = "<=",
-  Like = "like",
-  ILike = "ilike",
-  NotLike = "not like",
-  NotILike = "not ilike",
-  In = "in",
-  GlobalIn = "global in",
-  NotIn = "not in",
-  GlobalNotIn = "global not in",
-  InCohort = "in cohort",
-  NotInCohort = "not in cohort",
-  Regex = "=~",
-  IRegex = "=~*",
-  NotRegex = "!~",
-  NotIRegex = "!~*",
-}
+export type CompareOperationOp =
+  | "=="
+  | "!="
+  | ">"
+  | ">="
+  | "<"
+  | "<="
+  | "like"
+  | "ilike"
+  | "not like"
+  | "not ilike"
+  | "in"
+  | "global in"
+  | "not in"
+  | "global not in"
+  | "in cohort"
+  | "not in cohort"
+  | "=~"
+  | "=~*"
+  | "!~"
+  | "!~*";

You'll also need to update the NEGATED_COMPARE_OPS array to use string literals:

-export const NEGATED_COMPARE_OPS: CompareOperationOp[] = [
-  CompareOperationOp.NotEq,
-  CompareOperationOp.NotLike,
-  CompareOperationOp.NotILike,
-  CompareOperationOp.NotIn,
-  CompareOperationOp.GlobalNotIn,
-  CompareOperationOp.NotInCohort,
-  CompareOperationOp.NotRegex,
-  CompareOperationOp.NotIRegex,
-];
+export const NEGATED_COMPARE_OPS = [
+  "!=",
+  "not like",
+  "not ilike",
+  "not in",
+  "global not in",
+  "not in cohort",
+  "!~",
+  "!~*",
+] as const;

Based on coding guidelines specifying to avoid enums.

internal-packages/tsql/src/query/escape.ts (4)

20-21: Minor: Comment about \v may be overly cautious.

In modern JavaScript/TypeScript, \v is a valid and well-supported escape sequence for vertical tab (U+000B). The comment suggests it "may not work in all contexts," but this was primarily a concern in older JavaScript engines. Using \x0B is fine, but the comment could be clarified or simplified.


38-43: Consider simplifying safeIdentifier.

The includes check before replace is unnecessary—String.prototype.replace is efficient even when the pattern doesn't exist. Simplifying to just return identifier.replace(/%/g, "") would be cleaner.

♻️ Simplified implementation
 export function safeIdentifier(identifier: string): string {
-  if (identifier.includes("%")) {
-    return identifier.replace(/%/g, "");
-  }
-  return identifier;
+  return identifier.replace(/%/g, "");
 }

49-55: Optional: Consider optimizing for larger strings.

The split("").map().join() pattern creates intermediate arrays. For typical SQL values this is fine, but for large strings, a single-pass approach (loop or regex with replacer function) would be more efficient.


256-262: Array type inference assumes homogeneous arrays.

The function infers array type from the first element (Line 260), which assumes all elements have the same type. This is typically safe for ClickHouse arrays, but be aware that mixed-type arrays will be incorrectly typed.

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx (1)

187-204: Consider deduping query extraction with AIQueryService to avoid drift.

extractQueryFromText duplicates AIQueryService’s extraction logic (currently private). If one changes, the other can diverge; consider exporting a shared helper or making the service expose a utility for this.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed058eb and 9d0ceb4.

📒 Files selected for processing (9)
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx
  • apps/webapp/app/services/queryService.server.ts
  • internal-packages/clickhouse/src/client/client.ts
  • internal-packages/tsql/src/grammar/parser.test.ts
  • internal-packages/tsql/src/index.ts
  • internal-packages/tsql/src/query/ast.ts
  • internal-packages/tsql/src/query/escape.test.ts
  • internal-packages/tsql/src/query/escape.ts
  • internal-packages/tsql/src/query/printer.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/webapp/app/services/queryService.server.ts
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

Files:

  • internal-packages/tsql/src/query/escape.ts
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx
  • internal-packages/tsql/src/index.ts
  • internal-packages/clickhouse/src/client/client.ts
  • internal-packages/tsql/src/query/ast.ts
  • internal-packages/tsql/src/query/escape.test.ts
  • internal-packages/tsql/src/grammar/parser.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Files:

  • internal-packages/tsql/src/query/escape.ts
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx
  • internal-packages/tsql/src/index.ts
  • internal-packages/clickhouse/src/client/client.ts
  • internal-packages/tsql/src/query/ast.ts
  • internal-packages/tsql/src/query/escape.test.ts
  • internal-packages/tsql/src/grammar/parser.test.ts
**/*.{js,ts,jsx,tsx,json,md,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier

Files:

  • internal-packages/tsql/src/query/escape.ts
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx
  • internal-packages/tsql/src/index.ts
  • internal-packages/clickhouse/src/client/client.ts
  • internal-packages/tsql/src/query/ast.ts
  • internal-packages/tsql/src/query/escape.test.ts
  • internal-packages/tsql/src/grammar/parser.test.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • internal-packages/tsql/src/query/escape.ts
  • internal-packages/tsql/src/index.ts
  • internal-packages/clickhouse/src/client/client.ts
  • internal-packages/tsql/src/query/ast.ts
  • internal-packages/tsql/src/query/escape.test.ts
  • internal-packages/tsql/src/grammar/parser.test.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access all environment variables through the env export of env.server.ts instead of directly accessing process.env in the Trigger.dev webapp

Files:

  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: When importing from @trigger.dev/core in the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Files:

  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use vitest for all tests in the Trigger.dev repository

Files:

  • internal-packages/tsql/src/query/escape.test.ts
  • internal-packages/tsql/src/grammar/parser.test.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.test.{ts,tsx,js,jsx}: Test files should live beside the files under test and use descriptive describe and it blocks
Avoid mocks or stubs in tests; use helpers from @internal/testcontainers when Redis or Postgres are needed
Use vitest for unit tests

Files:

  • internal-packages/tsql/src/query/escape.test.ts
  • internal-packages/tsql/src/grammar/parser.test.ts
🧠 Learnings (10)
📓 Common learnings
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to internal-packages/database/**/*.{ts,tsx} : Use Prisma for database interactions in internal-packages/database with PostgreSQL
📚 Learning: 2025-12-08T15:19:56.823Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2760
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx:278-281
Timestamp: 2025-12-08T15:19:56.823Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx, the tableState search parameter uses intentional double-encoding: the parameter value contains a URL-encoded URLSearchParams string, so decodeURIComponent(value("tableState") ?? "") is required to fully decode it before parsing with new URLSearchParams(). This pattern allows bundling multiple filter/pagination params as a single search parameter.

Applied to files:

  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/**/*.{ts,tsx} : Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Applied to files:

  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx
📚 Learning: 2024-10-22T10:50:41.311Z
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 1424
File: packages/core/src/v3/errors.ts:155-189
Timestamp: 2024-10-22T10:50:41.311Z
Learning: When using `assertExhaustive` in a `switch` statement in TypeScript (e.g., in the `shouldRetryError` function in `packages/core/src/v3/errors.ts`), and it throws an error, it's acceptable not to add a `return` statement afterward, as control flow will not proceed beyond the `assertExhaustive` call.

Applied to files:

  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to internal-packages/database/**/*.{ts,tsx} : Use Prisma for database interactions in internal-packages/database with PostgreSQL

Applied to files:

  • internal-packages/tsql/src/index.ts
📚 Learning: 2025-11-27T16:27:48.109Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-27T16:27:48.109Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Use vitest for unit tests

Applied to files:

  • internal-packages/tsql/src/grammar/parser.test.ts
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Use vitest for all tests in the Trigger.dev repository

Applied to files:

  • internal-packages/tsql/src/grammar/parser.test.ts
📚 Learning: 2025-11-27T16:27:48.109Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-27T16:27:48.109Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Avoid mocks or stubs in tests; use helpers from `internal/testcontainers` when Redis or Postgres are needed

Applied to files:

  • internal-packages/tsql/src/grammar/parser.test.ts
📚 Learning: 2025-11-27T16:27:48.109Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-27T16:27:48.109Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : Test files should live beside the files under test and use descriptive `describe` and `it` blocks

Applied to files:

  • internal-packages/tsql/src/grammar/parser.test.ts
📚 Learning: 2025-11-27T16:26:58.661Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-11-27T16:26:58.661Z
Learning: Applies to apps/webapp/**/*.test.{ts,tsx} : Test files should only import classes and functions from `app/**/*.ts` files and should not import `env.server.ts` directly or indirectly; pass configuration through options instead

Applied to files:

  • internal-packages/tsql/src/grammar/parser.test.ts
🧬 Code graph analysis (5)
internal-packages/tsql/src/query/escape.ts (2)
internal-packages/tsql/src/index.ts (5)
  • escapeTSQLIdentifier (44-44)
  • escapeClickHouseIdentifier (42-42)
  • escapeTSQLString (45-45)
  • escapeClickHouseString (43-43)
  • getClickHouseType (46-46)
internal-packages/tsql/src/query/printer_context.ts (1)
  • timezone (89-91)
apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx (5)
apps/webapp/app/services/session.server.ts (1)
  • requireUserId (25-35)
apps/webapp/app/utils/pathBuilder.ts (1)
  • EnvironmentParamSchema (26-28)
apps/webapp/app/models/project.server.ts (1)
  • findProjectBySlug (136-147)
apps/webapp/app/models/runtimeEnvironment.server.ts (1)
  • findEnvironmentBySlug (116-145)
apps/webapp/app/v3/services/aiQueryService.server.ts (1)
  • AIQueryService (46-487)
internal-packages/clickhouse/src/client/client.ts (7)
internal-packages/clickhouse/src/client/types.ts (2)
  • ClickhouseQueryWithStatsFunction (39-45)
  • QueryStats (19-29)
packages/core/src/v3/utils/crypto.ts (1)
  • randomUUID (1-5)
internal-packages/tracing/src/index.ts (2)
  • startSpan (60-90)
  • recordSpanError (92-98)
packages/core/src/v3/utils/flattenAttributes.ts (1)
  • flattenAttributes (6-14)
internal-packages/clickhouse/src/client/errors.ts (1)
  • QueryError (33-42)
packages/core/src/v3/tryCatch.ts (1)
  • tryCatch (8-15)
internal-packages/clickhouse/src/client/tsql.ts (1)
  • QueryStats (24-24)
internal-packages/tsql/src/query/ast.ts (3)
internal-packages/tsql/src/query/context.ts (1)
  • TSQLContext (38-56)
internal-packages/tsql/src/query/constants.ts (2)
  • ConstantDataType (3-13)
  • TSQLQuerySettings (47-52)
internal-packages/tsql/src/query/models.ts (7)
  • Table (41-49)
  • LazyJoin (51-54)
  • LazyTable (56-56)
  • VirtualTable (58-58)
  • FieldOrTable (6-8)
  • UnknownDatabaseField (23-23)
  • DatabaseField (10-17)
internal-packages/tsql/src/query/escape.test.ts (1)
internal-packages/tsql/src/query/escape.ts (7)
  • escapeClickHouseIdentifier (89-105)
  • escapeTSQLIdentifier (61-83)
  • SQLValueEscaper (124-213)
  • escapeClickHouseString (225-227)
  • escapeTSQLString (218-220)
  • getClickHouseType (233-265)
  • safeIdentifier (38-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (24)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: claude-review
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (12)
internal-packages/clickhouse/src/client/client.ts (1)

19-19: LGTM!

The new type imports support the queryWithStats method and are properly scoped.

Also applies to: 23-23

internal-packages/tsql/src/grammar/parser.test.ts (2)

1-5: LGTM!

The imports are appropriate for testing an ANTLR-based parser with Vitest.


130-165: LGTM!

The program tests provide good structural validation of variable declarations and program-level constructs. The filtering approach to count varDecls is appropriate.

internal-packages/tsql/src/index.ts (3)

10-10: LGTM: Clean error handling and proper aliasing to avoid shadowing.

The import alias TSQLSyntaxError prevents shadowing the global SyntaxError, and the error listener implementation correctly captures parse errors.

Also applies to: 19-32


137-167: LGTM: Robust parsing with proper validation.

The function correctly handles ANTLR parsing, error capture, and AST validation before returning.


205-277: LGTM: Well-structured compilation function with clear tenant isolation.

The compileTSQL function cleanly orchestrates the parsing, schema creation, and printing steps. The tenant isolation design (required organizationId, optional projectId/environmentId) provides appropriate flexibility.

internal-packages/tsql/src/query/ast.ts (1)

654-672: LGTM: Robust helper with proper edge case handling.

The function correctly handles empty arrays (error), single queries (optimization), and multiple queries (construction). The error handling and optimization are appropriate.

internal-packages/tsql/src/query/escape.ts (4)

61-105: LGTM! Identifier escaping is secure and well-implemented.

Both escapeTSQLIdentifier and escapeClickHouseIdentifier properly validate against the % character to prevent injection, use appropriate regex patterns, and handle edge cases (like numeric identifiers in TSQL for array access). The dialect-specific differences (allowing $ in TSQL) are correctly implemented.


110-118: LGTM! Type definition is comprehensive.

The EscapableValue type covers all the value types handled by the escaper, including nested arrays.


124-213: LGTM! SQLValueEscaper class is well-structured.

The visitor pattern implementation correctly handles all supported types with appropriate dialect-specific behavior. Date formatting uses UTC methods consistently, and the timezone parameter is properly passed to ClickHouse functions. Error handling for unsupported types is appropriate.


218-227: LGTM! Wrapper functions provide clean API.

These functions appropriately delegate to SQLValueEscaper with the correct dialect settings.

internal-packages/tsql/src/query/escape.test.ts (1)

1-241: LGTM! Excellent test coverage.

This test suite comprehensively covers all exported functions from escape.ts with appropriate edge cases, dialect-specific behavior, error conditions, and type variations. The tests are well-structured using vitest's describe and it blocks, and assertions correctly verify the expected behavior.

Comment on lines +12 to +16
const RequestSchema = z.object({
prompt: z.string().min(1, "Prompt is required"),
mode: z.enum(["new", "edit"]).default("new"),
currentQuery: z.string().optional(),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

mode: "edit" should require currentQuery (otherwise silently behaves like “new”).

Right now { mode: "edit" } with no/empty currentQuery will fall back to “new” behavior in AIQueryService.streamQuery, which is surprising and makes “edit” nondeterministic.

Proposed fix
 const RequestSchema = z.object({
   prompt: z.string().min(1, "Prompt is required"),
   mode: z.enum(["new", "edit"]).default("new"),
   currentQuery: z.string().optional(),
-});
+}).superRefine((val, ctx) => {
+  if (val.mode === "edit" && !val.currentQuery?.trim()) {
+    ctx.addIssue({
+      code: z.ZodIssueCode.custom,
+      path: ["currentQuery"],
+      message: "Current query is required when mode is edit",
+    });
+  }
+});

Also applies to: 84-85

🤖 Prompt for AI Agents
In
@apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx
around lines 12 - 16, The RequestSchema currently allows mode="edit" without
enforcing currentQuery, causing AIQueryService.streamQuery to treat empty
currentQuery as a "new" edit; update RequestSchema so that when mode is "edit"
currentQuery is required and non-empty (e.g., use z.discriminatedUnion or
.refine to require currentQuery when mode === "edit"), and ensure any form
parsing that relies on RequestSchema (referenced by RequestSchema and
AIQueryService.streamQuery) will validate and return a clear error instead of
silently falling back to "new".

Comment on lines +18 to +21
export async function action({ request, params }: ActionFunctionArgs) {
const userId = await requireUserId(request);
const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid throwing 500s on bad route params (EnvironmentParamSchema.parse(params)).

parse() will throw and turn a bad URL into a 500 unless Remix catches and formats it. Prefer safeParse + 400.

Proposed fix
 export async function action({ request, params }: ActionFunctionArgs) {
   const userId = await requireUserId(request);
-  const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params);
+  const parsedParams = EnvironmentParamSchema.safeParse(params);
+  if (!parsedParams.success) {
+    return new Response(
+      JSON.stringify({
+        type: "result",
+        success: false,
+        error: "Invalid route parameters",
+      }),
+      { status: 400, headers: { "Content-Type": "application/json" } }
+    );
+  }
+  const { organizationSlug, projectParam, envParam } = parsedParams.data;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function action({ request, params }: ActionFunctionArgs) {
const userId = await requireUserId(request);
const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params);
export async function action({ request, params }: ActionFunctionArgs) {
const userId = await requireUserId(request);
const parsedParams = EnvironmentParamSchema.safeParse(params);
if (!parsedParams.success) {
return new Response(
JSON.stringify({
type: "result",
success: false,
error: "Invalid route parameters",
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
const { organizationSlug, projectParam, envParam } = parsedParams.data;
🤖 Prompt for AI Agents
In
@apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx
around lines 18 - 21, In the action function, replace
EnvironmentParamSchema.parse(params) with
EnvironmentParamSchema.safeParse(params) and check the result; if safeParse
returns success: false, return a 400 response (e.g., new Response or Remix json
with status 400) describing the invalid route params instead of letting parse
throw; otherwise extract organizationSlug/projectParam/envParam from result.data
and continue the existing flow (keep requireUserId(request) as-is).

Comment on lines +70 to +90
if (!env.OPENAI_API_KEY) {
return new Response(
JSON.stringify({
type: "result",
success: false,
error: "OpenAI API key is not configured",
}),
{
status: 400,
headers: { "Content-Type": "application/json" },
}
);
}

const { prompt, mode, currentQuery } = submission.data;

const service = new AIQueryService(
querySchemas,
openai(env.AI_RUN_FILTER_MODEL ?? "gpt-4o-mini")
);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's read the file to understand the env setup and imports
cat -n "apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx" | head -100

Repository: triggerdotdev/trigger.dev

Length of output: 176


🏁 Script executed:

# Check if env.server.ts exists and how env is exported
fd -t f "env.server.ts" apps/webapp

Repository: triggerdotdev/trigger.dev

Length of output: 98


🏁 Script executed:

# Search for other usages of @ai-sdk/openai in the webapp
rg "@ai-sdk/openai" apps/webapp -A 3 -B 1

Repository: triggerdotdev/trigger.dev

Length of output: 2984


🌐 Web query:

@ai-sdk/openai API key configuration - how to pass apiKey parameter

💡 Result:

Short answer — pass apiKey when creating the provider (or set OPENAI_API_KEY env var).

Examples:

  • Using createOpenAI:
    import { createOpenAI } from '@ai-sdk/openai'
    const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY || 'sk-...' })

  • Using the default provider (via env var):
    // set OPENAI_API_KEY=sk-... in your environment (or .env); the provider will use it by default.

Sources: ai-sdk docs and package readme. [1][2][3]

References

  1. AI SDK Getting Started / Configure OpenAI API key. [2]
  2. @ai-sdk/openai provider docs (createOpenAI apiKey option). [3]
  3. npm @ai-sdk/openai package readme (constructor/create options include apiKey). [5]

🏁 Script executed:

cat -n "apps/webapp/app/env.server.ts"

Repository: triggerdotdev/trigger.dev

Length of output: 50381


🏁 Script executed:

# Look at other usages of openai() in the codebase to understand the pattern
rg "openai\(" apps/webapp -B 2 -A 2 --type ts --type tsx

Repository: triggerdotdev/trigger.dev

Length of output: 96


🏁 Script executed:

# Search for OPENAI_API_KEY usage patterns
rg "OPENAI_API_KEY" apps/webapp -B 1 -A 1

Repository: triggerdotdev/trigger.dev

Length of output: 1816


Pass the API key explicitly to the OpenAI provider for consistency with the codebase.

While @ai-sdk/openai's default openai() function does read from process.env.OPENAI_API_KEY automatically, the codebase elsewhere (e.g., humanToCron.server.ts) explicitly passes the API key to the provider. The validation of env.OPENAI_API_KEY without using it is redundant. Use createOpenAI({ apiKey: env.OPENAI_API_KEY }) instead to align with best practices and ensure the validated value is actually used.

🤖 Prompt for AI Agents
In
@apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-generate.tsx
around lines 70 - 90, The env.OPENAI_API_KEY check is validated but not used;
update the AIQueryService instantiation to pass the API key explicitly by
replacing the openai(...) call with createOpenAI({ apiKey: env.OPENAI_API_KEY,
model: env.AI_RUN_FILTER_MODEL ?? "gpt-4o-mini" }) (or equivalent createOpenAI
call used elsewhere), keep the existing env.OPENAI_API_KEY validation, and
import createOpenAI instead of openai so the validated key is actually supplied
to AIQueryService.

Comment on lines +91 to +176
// Create a streaming response
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();

const sendEvent = (event: {
type: string;
content?: string;
tool?: string;
args?: unknown;
result?: unknown;
success?: boolean;
query?: string;
error?: string;
}) => {
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
};

try {
const result = service.streamQuery(prompt, { mode, currentQuery });

// Process the stream
for await (const part of result.fullStream) {
switch (part.type) {
case "text-delta": {
sendEvent({ type: "thinking", content: part.textDelta });
break;
}
case "tool-call": {
sendEvent({
type: "tool_call",
tool: part.toolName,
args: part.args,
});
break;
}
case "error": {
sendEvent({
type: "result",
success: false,
error: part.error instanceof Error ? part.error.message : String(part.error),
});
break;
}
case "finish": {
// Extract query from the final text
const finalText = await result.text;
const query = extractQueryFromText(finalText);

if (query) {
sendEvent({
type: "result",
success: true,
query,
});
} else if (
finalText.toLowerCase().includes("cannot") ||
finalText.toLowerCase().includes("unable")
) {
sendEvent({
type: "result",
success: false,
error: finalText.slice(0, 300),
});
} else {
sendEvent({
type: "result",
success: false,
error: "Could not generate a valid query",
});
}
break;
}
}
}
} catch (error) {
sendEvent({
type: "result",
success: false,
error: error instanceof Error ? error.message : "An error occurred",
});
} finally {
controller.close();
}
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

SSE stream can emit multiple terminal “result” events; also handle client abort + proxy buffering.

  • After emitting a terminal type:"result" (on "error" or "finish"), the for await loop continues; you can end up sending multiple results.
  • No abort handling: if the client disconnects, continuing to enqueue wastes tokens/time.
  • Consider SSE headers to prevent intermediary buffering.
Proposed fix
   const stream = new ReadableStream({
     async start(controller) {
       const encoder = new TextEncoder();
+      let done = false;
+
+      const abort = () => {
+        done = true;
+        try {
+          controller.close();
+        } catch {
+          // ignore
+        }
+      };
+      request.signal.addEventListener("abort", abort, { once: true });

       const sendEvent = (event: {
         type: string;
@@
       }) => {
-        controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
+        if (done) return;
+        controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
       };

       try {
         const result = service.streamQuery(prompt, { mode, currentQuery });

         // Process the stream
         for await (const part of result.fullStream) {
+          if (done) break;
           switch (part.type) {
             case "text-delta": {
               sendEvent({ type: "thinking", content: part.textDelta });
               break;
             }
@@
             case "error": {
               sendEvent({
                 type: "result",
                 success: false,
                 error: part.error instanceof Error ? part.error.message : String(part.error),
               });
-              break;
+              done = true;
+              break;
             }
             case "finish": {
@@
               } else {
                 sendEvent({
                   type: "result",
                   success: false,
                   error: "Could not generate a valid query",
                 });
               }
-              break;
+              done = true;
+              break;
             }
           }
+          if (done) break;
         }
       } catch (error) {
         sendEvent({
           type: "result",
           success: false,
           error: error instanceof Error ? error.message : "An error occurred",
         });
       } finally {
+        done = true;
         controller.close();
       }
     },
   });

   return new Response(stream, {
     headers: {
-      "Content-Type": "text/event-stream",
-      "Cache-Control": "no-cache",
+      "Content-Type": "text/event-stream; charset=utf-8",
+      "Cache-Control": "no-cache, no-transform",
       Connection: "keep-alive",
+      "X-Accel-Buffering": "no",
     },
   });

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +7 to +13
function parse(input: string) {
const inputStream = CharStreams.fromString(input);
const lexer = new TSQLLexer(inputStream);
const tokenStream = new CommonTokenStream(lexer);
const parser = new TSQLParser(tokenStream);
return parser;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error checking to verify parse success.

The helper function doesn't configure error handling, so tests may pass even when the parser encounters syntax errors. Consider adding an error listener to track parsing errors and fail tests if any occur.

🔍 Suggested enhancement
+import { ANTLRErrorListener, RecognitionException, Recognizer } from "antlr4ts";
+
+class ThrowingErrorListener implements ANTLRErrorListener<any> {
+  syntaxError(
+    recognizer: Recognizer<any, any>,
+    offendingSymbol: any,
+    line: number,
+    charPositionInLine: number,
+    msg: string,
+    e: RecognitionException | undefined
+  ): void {
+    throw new Error(`Syntax error at ${line}:${charPositionInLine}: ${msg}`);
+  }
+}
+
 function parse(input: string) {
   const inputStream = CharStreams.fromString(input);
   const lexer = new TSQLLexer(inputStream);
   const tokenStream = new CommonTokenStream(lexer);
   const parser = new TSQLParser(tokenStream);
+  parser.removeErrorListeners();
+  parser.addErrorListener(new ThrowingErrorListener());
   return parser;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function parse(input: string) {
const inputStream = CharStreams.fromString(input);
const lexer = new TSQLLexer(inputStream);
const tokenStream = new CommonTokenStream(lexer);
const parser = new TSQLParser(tokenStream);
return parser;
}
import { ANTLRErrorListener, RecognitionException, Recognizer } from "antlr4ts";
class ThrowingErrorListener implements ANTLRErrorListener<any> {
syntaxError(
recognizer: Recognizer<any, any>,
offendingSymbol: any,
line: number,
charPositionInLine: number,
msg: string,
e: RecognitionException | undefined
): void {
throw new Error(`Syntax error at ${line}:${charPositionInLine}: ${msg}`);
}
}
function parse(input: string) {
const inputStream = CharStreams.fromString(input);
const lexer = new TSQLLexer(inputStream);
const tokenStream = new CommonTokenStream(lexer);
const parser = new TSQLParser(tokenStream);
parser.removeErrorListeners();
parser.addErrorListener(new ThrowingErrorListener());
return parser;
}
🤖 Prompt for AI Agents
In @internal-packages/tsql/src/grammar/parser.test.ts around lines 7 - 13, The
parse helper currently constructs CharStreams.fromString, TSQLLexer,
CommonTokenStream and TSQLParser but doesn't surface syntax errors; update the
parse function to attach an ANTLR error listener (remove default listeners and
add a custom listener) to both the TSQLLexer and TSQLParser that collects
errors, run the parse production(s) you need, and if any errors were recorded
throw or fail the test (or return the collected errors) so tests fail when parse
errors occur; reference the parse function, TSQLLexer, TSQLParser, and
CharStreams.fromString when making the change.

@matt-aitken matt-aitken merged commit 49df40c into main Jan 9, 2026
39 checks passed
@matt-aitken matt-aitken deleted the TSQL branch January 9, 2026 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants