Skip to content

Conversation

@matt-aitken
Copy link
Member

Don’t allow aliased columns to be queried – it was actually safe but confusing. We call created_at -> triggered_at but we still allowed created_at which was confusing.

Now we have nice errors if you try select columns that aren’t selectable.

Also removed a ClickHouse setting allow_experimental_object_type which worked fine locally but stopped all queries working on ClickHouse Cloud 🤦‍♂️

@changeset-bot
Copy link

changeset-bot bot commented Jan 13, 2026

⚠️ No Changeset found

Latest commit: e5c9e55

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 13, 2026

Walkthrough

Removed a ClickHouse safety flag from the default settings. Added semantic state to the query printer to track SELECT aliases, allowed and internal-only ClickHouse columns, and a projection context flag. Enhanced field resolution to block unknown and internal-only columns in user-facing projections, permit certain internal columns in internal contexts (tenant/required-filter), and produce suggestion-rich errors for unknown columns. Adjusted error sanitization to avoid replacing names inside "Did you mean" suggestions. Escalated unknown-column reports from warnings to errors. Expanded and added tests covering aliasing, subqueries, clause-specific resolution, and internal-column blocking.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely incomplete, missing required sections from the template: no issue reference, incomplete checklist, missing detailed testing steps, and no changelog entry following the template structure. Complete the PR description by adding the issue reference, checking off all checklist items, documenting testing steps, adding a detailed changelog, and removing the incomplete placeholder format.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "Query fixes" is too vague and generic; it does not clearly communicate the primary changes such as column access control, error handling improvements, or ClickHouse setting removal. Use a more specific title that highlights the main change, such as "Block internal columns from user queries and improve error messages" or "Prevent aliased column queries and fix ClickHouse Cloud compatibility".

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

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8258fd6 and e5c9e55.

📒 Files selected for processing (7)
  • apps/webapp/app/services/queryService.server.ts
  • internal-packages/tsql/src/query/printer.test.ts
  • internal-packages/tsql/src/query/printer.ts
  • internal-packages/tsql/src/query/schema.test.ts
  • internal-packages/tsql/src/query/schema.ts
  • internal-packages/tsql/src/query/validator.test.ts
  • internal-packages/tsql/src/query/validator.ts
💤 Files with no reviewable changes (1)
  • apps/webapp/app/services/queryService.server.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal-packages/tsql/src/query/schema.ts
  • internal-packages/tsql/src/query/validator.test.ts
  • internal-packages/tsql/src/query/schema.test.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

Every Trigger.dev task must be exported; use task() function with unique id and run async function

Files:

  • internal-packages/tsql/src/query/validator.ts
  • internal-packages/tsql/src/query/printer.ts
  • internal-packages/tsql/src/query/printer.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/validator.ts
  • internal-packages/tsql/src/query/printer.ts
  • internal-packages/tsql/src/query/printer.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/validator.ts
  • internal-packages/tsql/src/query/printer.ts
  • internal-packages/tsql/src/query/printer.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/validator.ts
  • internal-packages/tsql/src/query/printer.ts
  • internal-packages/tsql/src/query/printer.test.ts
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js}: Import from @trigger.dev/core using subpaths only; never import from root of @trigger.dev/core
Always import task definitions from @trigger.dev/sdk, never from @trigger.dev/sdk/v3 or deprecated client.defineJob pattern

Files:

  • internal-packages/tsql/src/query/validator.ts
  • internal-packages/tsql/src/query/printer.ts
  • internal-packages/tsql/src/query/printer.test.ts
**/*.{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/printer.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/printer.test.ts
**/*.test.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Use vitest exclusively for testing and never mock anything; use testcontainers instead

Files:

  • internal-packages/tsql/src/query/printer.test.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.test.{ts,tsx}: Place test files next to source files with .test.ts naming convention (e.g., MyService.ts → MyService.test.ts)
Use testcontainers helpers (redisTest, postgresTest, containerTest from @internal/testcontainers) for Redis/PostgreSQL testing instead of mocks

Files:

  • internal-packages/tsql/src/query/printer.test.ts
🧠 Learnings (2)
📚 Learning: 2026-01-12T11:01:34.792Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-12T11:01:34.792Z
Learning: Applies to internal-packages/database/prisma/migrations/**/*.sql : Edit database schema in internal-packages/database/prisma/schema.prisma; remove extraneous lines from migrations related to BackgroundWorker, TaskRun tags, waitpoints, and SecretStore indexes

Applied to files:

  • internal-packages/tsql/src/query/printer.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/query/printer.test.ts
🧬 Code graph analysis (2)
internal-packages/tsql/src/query/printer.ts (3)
internal-packages/tsql/src/query/ast.ts (4)
  • Expression (47-78)
  • Alias (372-378)
  • Call (482-488)
  • ArithmeticOperation (380-385)
internal-packages/tsql/src/query/errors.ts (1)
  • QueryError (43-45)
internal-packages/tsql/src/query/schema.ts (1)
  • TableSchema (285-303)
internal-packages/tsql/src/query/printer.test.ts (3)
internal-packages/tsql/src/query/errors.ts (1)
  • QueryError (43-45)
internal-packages/tsql/src/query/schema.ts (3)
  • createSchemaRegistry (335-351)
  • TableSchema (285-303)
  • column (318-330)
internal-packages/tsql/src/query/printer_context.ts (1)
  • PrinterContext (51-181)
⏰ 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). (23)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • 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 (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 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 (8, 8)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (15)
internal-packages/tsql/src/query/validator.ts (2)

428-432: LGTM! Severity escalation for unknown qualified columns.

Changing unknown column issues from warnings to errors aligns with the PR objective to provide explicit, user-facing errors when attempting to access columns not in the schema. This ensures validateQuery returns valid: false for queries referencing unknown columns.


467-470: LGTM! Consistent severity escalation for unqualified columns.

This matches the qualified column handling at line 428, ensuring both forms of unknown column references are treated as errors.

internal-packages/tsql/src/query/printer.test.ts (3)

431-441: LGTM! Test updates for NULL handling.

The tests correctly verify that IS NULL and IS NOT NULL syntax generates the expected ClickHouse functions for nullable columns.


2175-2319: LGTM! Comprehensive test coverage for unknown column blocking.

This test suite thoroughly covers:

  • Unknown columns in SELECT, WHERE, ORDER BY, and GROUP BY clauses
  • "Did you mean" suggestions when using ClickHouse column names instead of TSQL names
  • Valid alias references in ORDER BY and HAVING
  • Subquery alias references

The tests follow vitest conventions and avoid mocks as per coding guidelines.


2428-2614: LGTM! Critical security tests for internal-only column blocking.

This test suite validates that:

  • Hidden tenant columns (organization_id, project_id, environment_id) are blocked in SELECT, ORDER BY, GROUP BY, and HAVING when not exposed in the schema
  • Hidden required filter columns (e.g., engine) are similarly blocked
  • Tenant guards and required filters still function correctly in internal WHERE clauses
  • Exposed tenant columns (when present in tableSchema.columns) remain queryable

This is essential for preventing users from accessing internal ClickHouse columns not meant for direct querying.

internal-packages/tsql/src/query/printer.ts (10)

119-139: LGTM! Well-documented semantic state fields.

The new printer state fields are clearly documented with their purpose:

  • selectAliases enables ORDER BY/HAVING to reference aliased columns
  • allowedInternalColumns tracks internal columns for tenant isolation
  • internalOnlyColumns blocks user queries from accessing internal columns
  • inProjectionContext controls when internal-only column blocking is active

334-349: LGTM! Proper context isolation for subqueries.

The context handling correctly ensures:

  • Top-level queries start with fresh contexts
  • Subqueries get isolated contexts without inheriting parent tables
  • Parent contexts are saved for later restoration (lines 544-549)

This prevents column scope leakage between parent and nested queries.


385-408: LGTM! Correct alias extraction ordering.

Extracting SELECT aliases before visiting columns ensures that ORDER BY and HAVING can reference these aliases during validation. The inProjectionContext flag appropriately blocks internal-only columns in user projections.


426-452: LGTM! Consistent projection context handling across clauses.

GROUP BY, HAVING, and ORDER BY all correctly set inProjectionContext to block internal-only columns, with proper cleanup after processing.


544-549: LGTM! Complete context restoration.

All saved contexts are properly restored after query processing, maintaining correct scoping for nested queries.


553-593: LGTM! Correct alias extraction logic.

The method properly distinguishes between:

  • Explicit aliases (SELECT x AS name → adds "name")
  • Implicit function names (SELECT COUNT() → adds "count")
  • Implicit arithmetic names (SELECT a + b → adds "plus")
  • Field references (SELECT status → correctly NOT added as alias)

The comment at lines 557-560 clearly explains the design decision.


1452-1487: LGTM! Correct dual-purpose column registration.

The logic correctly distinguishes between:

  1. allowedInternalColumns: Permits use in internal WHERE clauses (tenant guards, required filters)
  2. internalOnlyColumns: Blocks user access in SELECT, ORDER BY, GROUP BY, HAVING (unless exposed in schema)

This enables tenant isolation to work internally while preventing users from directly querying internal columns.


2232-2312: LGTM! Comprehensive field resolution with security enforcement.

The enhanced resolveFieldChain method correctly:

  1. Resolves columns through table context
  2. Allows SELECT alias references
  3. Blocks internal-only columns in projection contexts (SELECT, ORDER BY, GROUP BY, HAVING)
  4. Permits internal columns in non-projection contexts (WHERE tenant guards)
  5. Provides helpful "Did you mean" suggestions when users use ClickHouse column names
  6. Falls through gracefully for subqueries without table context

2314-2364: LGTM! Well-implemented helper methods.

The three helper methods provide clean abstractions:

  • getAvailableColumnNames: Aggregates column names for error messages
  • isColumnExposedInSchema: Correctly handles both direct names and clickhouseName mappings
  • findTSQLNameForClickHouseName: Enables helpful "Did you mean" suggestions

2366-2394: LGTM! Critical security improvement in column resolution.

The updated resolveColumnName now:

  1. Throws QueryError for columns not in the schema (security enforcement)
  2. Provides "Did you mean" suggestions when users type ClickHouse column names
  3. Lists available columns for discoverability

This prevents access to internal ClickHouse columns not explicitly exposed in the schema.


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.

@claude
Copy link

claude bot commented Jan 13, 2026

PR Review: Query fixes

Summary

This PR improves the TSQL query layer with two key changes:

  1. Block unknown/aliased columns - Prevents users from using internal ClickHouse column names (like created_at) instead of the exposed TSQL names (like triggered_at)
  2. Remove allow_experimental_object_type - Removes a ClickHouse setting that was breaking queries on ClickHouse Cloud

Overall Assessment

Good PR - Well-structured changes with comprehensive test coverage. The security improvements around column access are solid.


Code Quality & Best Practices

Strengths:

  • Excellent test coverage for the new unknown column blocking feature (150+ lines of new tests)
  • Clear comments explaining the design decisions (e.g., why SELECT aliases work differently)
  • Good error messages with helpful "Did you mean?" suggestions
  • Proper context save/restore pattern for nested queries

Minor suggestions:

  1. printer.ts:545-552 - The opNames object is recreated every time extractSelectAlias is called with an arithmetic operation. Consider moving it to a class-level constant:
private static readonly ARITHMETIC_OP_NAMES: Record<ArithmeticOperationOp, string> = {
  [ArithmeticOperationOp.Add]: "plus",
  // ...
};
  1. printer.ts:2237-2240 - The suggestion lookup in resolveFieldChain duplicates logic that also exists in resolveColumnName. Consider extracting to a shared helper to reduce duplication.

Potential Bugs or Issues

  1. CTE column references - The comment at printer.test.ts:2287-2288 notes that CTEs are not added to the table context, so CTE column references are treated as unknown. While the tests document this as "pre-existing limitation," it could cause false positives if users are using CTEs. Worth documenting in user-facing docs.

  2. printer.ts:2227-2228 - The check if (this.tableContexts.size > 0) means unknown columns in FROM subqueries without aliases will silently pass through. This is intentional per the comment, but edge cases like:

SELECT unknown_col FROM (SELECT * FROM runs)

would not throw. Consider if this should be documented or handled differently.


Performance Considerations

  • The findTSQLNameForClickHouseName method at printer.ts:2249-2258 iterates through all columns in all tables. For large schemas, this could be slow. However, since this only runs on error paths, the impact is negligible.

  • The new selectAliases Set and allowedInternalColumns Set add minimal memory overhead and are properly cleaned up via the save/restore pattern.


Security Considerations

This PR improves security by:

  • Preventing access to internal ClickHouse columns that aren't exposed in the schema
  • Upgrading unknown column validation from "warning" to "error" in the validator
  • Properly handling tenant columns through allowedInternalColumns

The "Did you mean?" error messages are safe because they only suggest columns that are already exposed in the TSQL schema.


Test Coverage

Excellent coverage with tests for:

  • Unknown columns in SELECT, WHERE, ORDER BY, GROUP BY
  • SELECT aliases (explicit and implicit)
  • Subquery alias references
  • Internal ClickHouse column name suggestions
  • Error message sanitization for "Did you mean" messages

One minor gap:

  • No explicit test for deeply nested subqueries (3+ levels) to verify the context save/restore works correctly. The current tests only go 1-2 levels deep.

Summary of Recommendations

  1. 🔸 Minor: Consider extracting ARITHMETIC_OP_NAMES to a class constant
  2. 🔸 Minor: Document CTE column reference limitation in user docs
  3. 🔹 Optional: Add test for deeply nested subquery context handling

Overall this is a well-implemented security improvement. Approve with minor suggestions.

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal-packages/tsql/src/query/printer.ts (1)

2177-2246: Unknown-column blocking: add “did you mean” support for dotted roots typed as ClickHouse names (e.g. created_at.foo).

Right now, resolveFieldChain() returns the chain unchanged when chain.length >= 2 and chain[0] isn’t a known table alias (Line 2203-2204). That preserves subquery alias refs, but it also skips the clickhouseName→tsqlName suggestion path if the root is a ClickHouse internal name used with dot access.

A targeted hardening that still preserves subquery aliases: when chain.length >= 2 and chain[0] is not a known table alias, check whether chain[0] matches any ColumnSchema.clickhouseName; if so, throw the same “Did you mean …?” error you already throw for unqualified/qualified access.

Proposed diff (minimal change, preserves subquery alias refs)
   if (chain.length >= 2) {
     const tableAlias = firstPart;
     const tableSchema = this.tableContexts.get(tableAlias);

     if (tableSchema) {
       // This is a table.column reference
       const columnName = chain[1];
       if (typeof columnName === "string") {
         const resolvedColumn = this.resolveColumnName(tableSchema, columnName, tableAlias);
         return [tableAlias, resolvedColumn, ...chain.slice(2)];
       }
     }
-    // Not a known table alias, might be a nested field or JSON path - return as-is
-    return chain;
+    // Not a known table alias.
+    // If the user used an internal ClickHouse column name as the root of a dotted path,
+    // still provide "Did you mean" guidance (instead of silently passing it through).
+    const suggestion = this.findTSQLNameForClickHouseName(tableAlias);
+    if (suggestion) {
+      throw new QueryError(`Unknown column "${tableAlias}". Did you mean "${suggestion}"?`);
+    }
+    // Otherwise, allow (e.g. subquery aliases like sub.total)
+    return chain;
   }

Also, if you keep allowedInternalColumns as a bypass, I’d strongly recommend adding an explicit denial path for user-authored references to internal-only columns (so the bypass is only usable for printer-generated guards).

Also applies to: 2259-2275, 2277-2305

🤖 Fix all issues with AI agents
In @internal-packages/tsql/src/query/printer.ts:
- Around line 1417-1433: When registering tenant/required-filter names into
allowedInternalColumns, also mark them as non-user-queryable unless they appear
in the table schema's public columns: create or update a companion set (e.g.,
internalNonQueryableColumns or internalOnlyColumns) and add
organizationId/projectId/environmentId and each requiredFilters[].column to it;
then update the printer paths that render/provide SELECT, ORDER BY, GROUP BY,
and projection logic to reject or strip any user-requested column present in
that internal-only set unless that column exists in TableSchema.columns (or
tableSchema.columns includes an explicit exposure flag). Ensure the check
references the existing symbols allowedInternalColumns, tableSchema, and
TableSchema.columns and that the guard runs wherever user-supplied column names
are validated/rendered.
- Around line 119-128: The field-resolution logic currently treats
allowedInternalColumns as universally resolvable, allowing user-authored queries
to reference internal/generated columns; update resolveFieldChain to only accept
names from allowedInternalColumns when the AST node was printer-injected (e.g.,
mark injected nodes or check a flag/property on the AST token), and otherwise
require resolution against TableSchema.columns/selectAliases; ensure the Set
allowedInternalColumns is only consulted for guard/inserted nodes
(printer-injected) and do not fall back to it for normal parsed identifiers to
prevent schema bypass.
🧹 Nitpick comments (4)
internal-packages/tsql/src/query/schema.ts (1)

859-871: Make the “Did you mean” detection more robust (quote/case variations).

result.includes('Did you mean "') is easy to miss if formatting changes (single quotes/backticks/case). Consider a case-insensitive regex gate.

Proposed diff
-  if (!result.includes('Did you mean "')) {
+  const hasDidYouMean = /did you mean\s+["'`]/i.test(result);
+  if (!hasDidYouMean) {
     // Sort by length descending to replace longer names first
     const sortedColumnNames = [...columnNameMap.entries()].sort(
       (a, b) => b[0].length - a[0].length
     );
     for (const [clickhouseName, { tsqlName }] of sortedColumnNames) {
       result = replaceAllOccurrences(result, clickhouseName, tsqlName);
     }
   }
internal-packages/tsql/src/query/printer.ts (2)

323-335: Wrap context restore in try/finally to avoid state corruption on throw.

visitSelectQuery() restores contexts/aliases at the end, but any QueryError thrown mid-walk will skip restore (relevant if ClickHousePrinter is reused).

Proposed diff (structure)
-    // Save and clear table contexts
-    // Top-level queries clear contexts; subqueries save parent context and create fresh context
-    const savedTableContexts = new Map(this.tableContexts);
-    const savedInternalColumns = new Set(this.allowedInternalColumns);
+    const savedTableContexts = new Map(this.tableContexts);
+    const savedInternalColumns = new Set(this.allowedInternalColumns);
+    const savedAliases = this.selectAliases;

     if (isTopLevelQuery) {
       this.tableContexts.clear();
       this.allowedInternalColumns.clear();
     } else {
       this.tableContexts = new Map();
       this.allowedInternalColumns = new Set();
     }

-    // Extract SELECT column aliases BEFORE visiting columns
-    // This allows ORDER BY/HAVING to reference aliased columns
-    const savedAliases = this.selectAliases;
-    this.selectAliases = new Set();
-    if (node.select) {
-      for (const col of node.select) {
-        this.extractSelectAlias(col);
-      }
-    }
+    try {
+      // Extract SELECT column aliases BEFORE visiting columns
+      this.selectAliases = new Set();
+      if (node.select) {
+        for (const col of node.select) {
+          this.extractSelectAlias(col);
+        }
+      }

       // ... existing method body ...
-    // Restore saved contexts (for nested queries)
-    this.selectAliases = savedAliases;
-    this.tableContexts = savedTableContexts;
-    this.allowedInternalColumns = savedInternalColumns;
-
-    return response;
+      return response;
+    } finally {
+      this.selectAliases = savedAliases;
+      this.tableContexts = savedTableContexts;
+      this.allowedInternalColumns = savedInternalColumns;
+    }

Also applies to: 510-514


371-379: Alias pre-scan is the right approach; consider de-duping implicit-name logic.

Extracting aliases before visiting columns is a good fix for ORDER BY/HAVING alias references. Small maintainability note: extractSelectAlias() overlaps conceptually with generateImplicitName() (and validator’s implicit-name logic), so drift is a risk.

Also applies to: 518-558

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

2175-2319: Nice coverage expansion for compile-time unknown-column blocking + alias/subquery allowances.

If you implement the dotted-root “Did you mean” hardening in resolveFieldChain(), it’d be worth adding a focused test case like SELECT created_at.foo FROM runs expecting Did you mean "created" (or whatever mapping applies) to prevent regression.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb25340 and 8258fd6.

📒 Files selected for processing (7)
  • apps/webapp/app/services/queryService.server.ts
  • internal-packages/tsql/src/query/printer.test.ts
  • internal-packages/tsql/src/query/printer.ts
  • internal-packages/tsql/src/query/schema.test.ts
  • internal-packages/tsql/src/query/schema.ts
  • internal-packages/tsql/src/query/validator.test.ts
  • internal-packages/tsql/src/query/validator.ts
💤 Files with no reviewable 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

Every Trigger.dev task must be exported; use task() function with unique id and run async function

Files:

  • internal-packages/tsql/src/query/schema.test.ts
  • internal-packages/tsql/src/query/schema.ts
  • internal-packages/tsql/src/query/validator.test.ts
  • internal-packages/tsql/src/query/printer.test.ts
  • internal-packages/tsql/src/query/printer.ts
  • internal-packages/tsql/src/query/validator.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/schema.test.ts
  • internal-packages/tsql/src/query/schema.ts
  • internal-packages/tsql/src/query/validator.test.ts
  • internal-packages/tsql/src/query/printer.test.ts
  • internal-packages/tsql/src/query/printer.ts
  • internal-packages/tsql/src/query/validator.ts
**/*.{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/schema.test.ts
  • internal-packages/tsql/src/query/validator.test.ts
  • internal-packages/tsql/src/query/printer.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/schema.test.ts
  • internal-packages/tsql/src/query/schema.ts
  • internal-packages/tsql/src/query/validator.test.ts
  • internal-packages/tsql/src/query/printer.test.ts
  • internal-packages/tsql/src/query/printer.ts
  • internal-packages/tsql/src/query/validator.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/schema.test.ts
  • internal-packages/tsql/src/query/validator.test.ts
  • internal-packages/tsql/src/query/printer.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/schema.test.ts
  • internal-packages/tsql/src/query/schema.ts
  • internal-packages/tsql/src/query/validator.test.ts
  • internal-packages/tsql/src/query/printer.test.ts
  • internal-packages/tsql/src/query/printer.ts
  • internal-packages/tsql/src/query/validator.ts
**/*.test.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Use vitest exclusively for testing and never mock anything; use testcontainers instead

Files:

  • internal-packages/tsql/src/query/schema.test.ts
  • internal-packages/tsql/src/query/validator.test.ts
  • internal-packages/tsql/src/query/printer.test.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.test.{ts,tsx}: Place test files next to source files with .test.ts naming convention (e.g., MyService.ts → MyService.test.ts)
Use testcontainers helpers (redisTest, postgresTest, containerTest from @internal/testcontainers) for Redis/PostgreSQL testing instead of mocks

Files:

  • internal-packages/tsql/src/query/schema.test.ts
  • internal-packages/tsql/src/query/validator.test.ts
  • internal-packages/tsql/src/query/printer.test.ts
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js}: Import from @trigger.dev/core using subpaths only; never import from root of @trigger.dev/core
Always import task definitions from @trigger.dev/sdk, never from @trigger.dev/sdk/v3 or deprecated client.defineJob pattern

Files:

  • internal-packages/tsql/src/query/schema.test.ts
  • internal-packages/tsql/src/query/schema.ts
  • internal-packages/tsql/src/query/validator.test.ts
  • internal-packages/tsql/src/query/printer.test.ts
  • internal-packages/tsql/src/query/printer.ts
  • internal-packages/tsql/src/query/validator.ts
🧠 Learnings (1)
📚 Learning: 2026-01-12T11:01:34.792Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-12T11:01:34.792Z
Learning: Applies to internal-packages/database/prisma/migrations/**/*.sql : Edit database schema in internal-packages/database/prisma/schema.prisma; remove extraneous lines from migrations related to BackgroundWorker, TaskRun tags, waitpoints, and SecretStore indexes

Applied to files:

  • internal-packages/tsql/src/query/schema.ts
🧬 Code graph analysis (3)
internal-packages/tsql/src/query/schema.test.ts (2)
internal-packages/tsql/src/index.ts (1)
  • sanitizeErrorMessage (82-82)
apps/webapp/app/v3/querySchemas.ts (1)
  • runsSchema (27-420)
internal-packages/tsql/src/query/printer.test.ts (4)
internal-packages/tsql/src/query/errors.ts (1)
  • QueryError (43-45)
internal-packages/tsql/src/query/schema.ts (1)
  • createSchemaRegistry (335-351)
apps/webapp/app/v3/querySchemas.ts (1)
  • runsSchema (27-420)
internal-packages/tsql/src/query/printer_context.ts (1)
  • createPrinterContext (205-214)
internal-packages/tsql/src/query/printer.ts (3)
internal-packages/tsql/src/query/ast.ts (4)
  • Expression (47-78)
  • Alias (372-378)
  • Call (482-488)
  • ArithmeticOperation (380-385)
internal-packages/tsql/src/query/errors.ts (1)
  • QueryError (43-45)
internal-packages/tsql/src/query/schema.ts (1)
  • TableSchema (285-303)
⏰ 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). (23)
  • 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 (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 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 / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 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 (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • 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: Analyze (javascript-typescript)
🔇 Additional comments (6)
internal-packages/tsql/src/query/validator.ts (2)

424-433: Unknown columns now fail validation (expected breaking change).

Severity is now "error" for qualified unknown columns, which will flip ValidationResult.valid to false (based on Line 102). Just ensure any callers/UI that previously tolerated warnings are updated accordingly.


463-471: Unqualified unknown columns now fail validation (consistent with qualified path).

This matches the new “unknown columns are errors” posture and aligns the two paths.

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

618-627: Good regression test for “Did you mean” sanitization.

Covers the exact failure mode that would otherwise rewrite the “unknown” token into the suggestion token.

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

50-59: Test correctly updated for unknown ORDER BY column now being an error.


101-108: Test correctly updated for unknown SELECT column now being an error.

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

432-441: NULL handling expectations updated appropriately (started_at).

@claude
Copy link

claude bot commented Jan 13, 2026

PR Review: Query fixes

Summary

This PR improves the TSQL query compiler by:

  1. Removing allow_experimental_object_type ClickHouse setting that broke queries on ClickHouse Cloud
  2. Blocking unknown/aliased columns from being queried directly
  3. Providing helpful error messages when users accidentally use internal ClickHouse column names

Code Quality ✅

Well-designed architecture: The implementation cleanly separates concerns:

  • selectAliases tracks user-defined aliases from SELECT clause
  • allowedInternalColumns tracks columns allowed for internal use (tenant isolation)
  • internalOnlyColumns tracks columns that exist but shouldn't be user-queryable
  • inProjectionContext flag distinguishes user-facing clauses from internal WHERE filters

Good error messages: The "Did you mean X?" suggestions when users type ClickHouse column names instead of TSQL names is a nice UX improvement.

Potential Issues

  1. Context save/restore pattern in visitSelectQuery (printer.ts:331-371, 541-544): The code saves and restores multiple contexts for nested queries. Consider grouping these into a single context object to make the save/restore more maintainable:

    interface QueryContext {
      tableContexts: Map<string, TableSchema>;
      allowedInternalColumns: Set<string>;
      internalOnlyColumns: Set<string>;
      selectAliases: Set<string>;
    }
  2. CTE limitation noted in tests (printer.test.ts:2295-2298): The comment notes that CTEs don't add to table context, so CTE column references are treated as unknown. This is documented as a "pre-existing limitation" but could cause confusion for users writing complex queries with CTEs.

  3. Subquery column visibility (printer.test.ts:2302-2321): The tests show subquery aliases work, but the implementation relies on subqueries getting fresh contexts. Worth verifying this handles deeply nested subqueries correctly.

Test Coverage ✅

Excellent test coverage with:

  • Tests for unknown columns in SELECT, WHERE, ORDER BY, GROUP BY
  • Tests for internal-only column blocking (tenant columns not exposed in schema)
  • Tests for SELECT alias references in ORDER BY/HAVING
  • Tests for helpful "Did you mean?" suggestions
  • Tests for subquery alias references

Performance Considerations

The column resolution now does additional lookups through tableContexts, selectAliases, internalOnlyColumns, and allowedInternalColumns for each field reference. These are all Set/Map operations (O(1)), so no performance concerns.

Security ✅

The change improves security by blocking access to internal columns that users shouldn't query directly (tenant isolation columns, internal filter columns like engine).

Minor Suggestions

  1. printer.ts:2262-2270: The inProjectionContext check happens before allowedInternalColumns check. This ordering is correct (internal-only columns should be blocked in projections), but a comment explaining this priority would help future maintainers.

  2. IS NULL test changes (printer.test.ts:432-440): The test changed from using error IS NULL to started_at IS NULL. The commit message mentions aliased columns but doesn't explain this test change - presumably error was removed or renamed in the schema?

Verdict

Approve - This is a well-implemented improvement that:

  • Fixes a production issue (ClickHouse Cloud compatibility)
  • Improves security by blocking internal column access
  • Provides better error messages for users
  • Has comprehensive test coverage

The changes are focused and don't over-engineer the solution.

@matt-aitken matt-aitken merged commit 1bca378 into main Jan 13, 2026
43 of 57 checks passed
@matt-aitken matt-aitken deleted the trql-improvements branch January 13, 2026 17:43
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