Skip to content

Commit 6b74ca6

Browse files
committed
Added enforcedWhereClause, used for tenant columns and will be for time periods too
1 parent 1e938b3 commit 6b74ca6

File tree

10 files changed

+876
-300
lines changed

10 files changed

+876
-300
lines changed

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { ArrowDownTrayIcon, ArrowsPointingInIcon, ArrowsPointingOutIcon, ArrowTrendingUpIcon, ClipboardIcon, TableCellsIcon } from "@heroicons/react/20/solid";
1+
import { ArrowDownTrayIcon, ArrowsPointingOutIcon, ArrowTrendingUpIcon, ClipboardIcon, TableCellsIcon } from "@heroicons/react/20/solid";
2+
import type { OutputColumnMetadata } from "@internal/clickhouse";
3+
import { WhereClauseCondition } from "@internal/tsql";
24
import { useFetcher } from "@remix-run/react";
3-
import type { action as titleAction } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-title";
4-
import type { OutputColumnMetadata, WhereClauseFallback } from "@internal/clickhouse";
55
import {
66
redirect,
77
type ActionFunctionArgs,
88
type LoaderFunctionArgs,
99
} from "@remix-run/server-runtime";
10+
import parse from "parse-duration";
1011
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
1112
import { flushSync } from "react-dom";
1213
import { typedjson, useTypedFetcher, useTypedLoaderData } from "remix-typedjson";
@@ -23,8 +24,6 @@ import { autoFormatSQL, TSQLEditor } from "~/components/code/TSQLEditor";
2324
import { TSQLResultsTable } from "~/components/code/TSQLResultsTable";
2425
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
2526
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
26-
import { TimeFilter, timeFilters } from "~/components/runs/v3/SharedFilters";
27-
import { useSearchParams } from "~/hooks/useSearchParam";
2827
import { Button } from "~/components/primitives/Buttons";
2928
import { Callout } from "~/components/primitives/Callout";
3029
import { Card } from "~/components/primitives/charts/Card";
@@ -34,6 +33,7 @@ import {
3433
ClientTabsList,
3534
ClientTabsTrigger,
3635
} from "~/components/primitives/ClientTabs";
36+
import { Dialog, DialogContent, DialogHeader } from "~/components/primitives/Dialog";
3737
import { Header3 } from "~/components/primitives/Headers";
3838
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
3939
import { Paragraph } from "~/components/primitives/Paragraph";
@@ -51,28 +51,28 @@ import {
5151
import { Select, SelectItem } from "~/components/primitives/Select";
5252
import { Spinner } from "~/components/primitives/Spinner";
5353
import { Switch } from "~/components/primitives/Switch";
54+
import { SimpleTooltip } from "~/components/primitives/Tooltip";
55+
import { TimeFilter, timeFilters } from "~/components/runs/v3/SharedFilters";
5456
import { prisma } from "~/db.server";
5557
import { useEnvironment } from "~/hooks/useEnvironment";
5658
import { useOrganization } from "~/hooks/useOrganizations";
5759
import { useProject } from "~/hooks/useProject";
60+
import { useSearchParams } from "~/hooks/useSearchParam";
5861
import { findProjectBySlug } from "~/models/project.server";
5962
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
6063
import { QueryPresenter, type QueryHistoryItem } from "~/presenters/v3/QueryPresenter.server";
64+
import type { action as titleAction } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-title";
6165
import { executeQuery, type QueryScope } from "~/services/queryService.server";
66+
import { requireUser } from "~/services/session.server";
6267
import { downloadFile, rowsToCSV, rowsToJSON } from "~/utils/dataExport";
6368
import { EnvironmentParamSchema } from "~/utils/pathBuilder";
6469
import { FEATURE_FLAG, validateFeatureFlagValue } from "~/v3/featureFlags.server";
6570
import { querySchemas } from "~/v3/querySchemas";
71+
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
6672
import { QueryHelpSidebar } from "./QueryHelpSidebar";
6773
import { QueryHistoryPopover } from "./QueryHistoryPopover";
6874
import type { AITimeFilter } from "./types";
6975
import { formatQueryStats } from "./utils";
70-
import { requireUser } from "~/services/session.server";
71-
import parse from "parse-duration";
72-
import { SimpleTooltip } from "~/components/primitives/Tooltip";
73-
import { Dialog, DialogContent, DialogHeader, DialogPortal, DialogTrigger } from "~/components/primitives/Dialog";
74-
import { DialogOverlay } from "@radix-ui/react-dialog";
75-
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
7676

7777
/** Convert a Date or ISO string to ISO string format */
7878
function toISOString(value: Date | string): string {
@@ -273,7 +273,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
273273
defaultPeriod: DEFAULT_PERIOD,
274274
});
275275

276-
let triggeredAtFallback: WhereClauseFallback;
276+
let triggeredAtFallback: WhereClauseCondition;
277277
if (timeFilter.from && timeFilter.to) {
278278
// Both from and to specified - use BETWEEN
279279
triggeredAtFallback = { op: "between", low: timeFilter.from, high: timeFilter.to };

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

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
type TSQLQueryResult,
88
} from "@internal/clickhouse";
99
import type { CustomerQuerySource } from "@trigger.dev/database";
10-
import type { TableSchema } from "@internal/tsql";
10+
import type { TableSchema, WhereClauseCondition } from "@internal/tsql";
1111
import { type z } from "zod";
1212
import { prisma } from "~/db.server";
1313
import { env } from "~/env.server";
@@ -56,7 +56,7 @@ function getDefaultClickhouseSettings(): ClickHouseSettings {
5656

5757
export type ExecuteQueryOptions<TOut extends z.ZodSchema> = Omit<
5858
ExecuteTSQLOptions<TOut>,
59-
"tableSchema" | "organizationId" | "projectId" | "environmentId" | "fieldMappings"
59+
"tableSchema" | "organizationId" | "projectId" | "environmentId" | "fieldMappings" | "enforcedWhereClause"
6060
> & {
6161
tableSchema: TableSchema[];
6262
/** The scope of the query - determines tenant isolation */
@@ -137,22 +137,17 @@ export async function executeQuery<TOut extends z.ZodSchema>(
137137

138138
try {
139139
// Build tenant IDs based on scope
140-
const tenantOptions: {
141-
organizationId: string;
142-
projectId?: string;
143-
environmentId?: string;
140+
const enforcedWhereClause: {
141+
organization_id: WhereClauseCondition;
142+
project_id?: WhereClauseCondition;
143+
environment_id?: WhereClauseCondition;
144144
} = {
145-
organizationId,
145+
organization_id: { op: "eq", value: organizationId },
146+
project_id: scope === "project" || scope === "environment" ? { op: "eq", value: projectId } : undefined,
147+
environment_id: scope === "environment" ? { op: "eq", value: environmentId } : undefined,
148+
//todo add plan-based time limit
146149
};
147150

148-
if (scope === "project" || scope === "environment") {
149-
tenantOptions.projectId = projectId;
150-
}
151-
152-
if (scope === "environment") {
153-
tenantOptions.environmentId = environmentId;
154-
}
155-
156151
// Build field mappings for project_ref → project_id and environment_id → slug translation
157152
const projects = await prisma.project.findMany({
158153
where: { organizationId },
@@ -171,7 +166,7 @@ export async function executeQuery<TOut extends z.ZodSchema>(
171166

172167
const result = await executeTSQL(clickhouseClient.reader, {
173168
...baseOptions,
174-
...tenantOptions,
169+
enforcedWhereClause,
175170
fieldMappings,
176171
whereClauseFallback,
177172
clickhouseSettings: {

internal-packages/clickhouse/src/client/tsql.ts

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* TSQL Query Execution for ClickHouse
33
*
44
* This module provides a safe interface for executing TSQL queries against ClickHouse
5-
* with automatic tenant isolation and SQL injection protection.
5+
* with enforced WHERE clause conditions (tenant isolation + plan limits) and SQL injection protection.
66
*/
77

88
import type { ClickHouseSettings } from "@clickhouse/client";
@@ -14,7 +14,7 @@ import {
1414
type TableSchema,
1515
type QuerySettings,
1616
type FieldMappings,
17-
type WhereClauseFallback,
17+
type WhereClauseCondition
1818
} from "@internal/tsql";
1919
import type { ClickhouseReader, QueryStats } from "./types.js";
2020
import { QueryError } from "./errors.js";
@@ -25,7 +25,7 @@ const logger = new Logger("tsql", "info");
2525

2626
export type { QueryStats };
2727

28-
export type { TableSchema, QuerySettings, FieldMappings, WhereClauseFallback };
28+
export type { TableSchema, QuerySettings, FieldMappings, WhereClauseCondition };
2929

3030
/**
3131
* Options for executing a TSQL query
@@ -37,14 +37,26 @@ export interface ExecuteTSQLOptions<TOut extends z.ZodSchema> {
3737
query: string;
3838
/** The Zod schema for validating output rows */
3939
schema: TOut;
40-
/** The organization ID for tenant isolation (required) */
41-
organizationId: string;
42-
/** The project ID for tenant isolation (optional - omit to query across all projects) */
43-
projectId?: string;
44-
/** The environment ID for tenant isolation (optional - omit to query across all environments) */
45-
environmentId?: string;
4640
/** Schema registry defining allowed tables and columns */
4741
tableSchema: TableSchema[];
42+
/**
43+
* REQUIRED: Conditions always applied at the table level.
44+
* Must include tenant columns (e.g., organization_id) for multi-tenant tables.
45+
* Applied to every table reference including subqueries, CTEs, and JOINs.
46+
*
47+
* @example
48+
* ```typescript
49+
* {
50+
* // Tenant isolation
51+
* organization_id: { op: "eq", value: "org_123" },
52+
* project_id: { op: "eq", value: "proj_456" },
53+
* environment_id: { op: "eq", value: "env_789" },
54+
* // Plan-based time limit
55+
* triggered_at: { op: "gte", value: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }
56+
* }
57+
* ```
58+
*/
59+
enforcedWhereClause: Record<string, WhereClauseCondition>;
4860
/** Optional ClickHouse query settings */
4961
clickhouseSettings?: ClickHouseSettings;
5062
/** Optional TSQL query settings (maxRows, timezone, etc.) */
@@ -78,6 +90,7 @@ export interface ExecuteTSQLOptions<TOut extends z.ZodSchema> {
7890
/**
7991
* Fallback WHERE conditions to apply when the user hasn't filtered on a column.
8092
* Key is the column name, value is the fallback condition.
93+
* These are applied at the AST level (top-level query only).
8194
*
8295
* @example
8396
* ```typescript
@@ -87,7 +100,7 @@ export interface ExecuteTSQLOptions<TOut extends z.ZodSchema> {
87100
* }
88101
* ```
89102
*/
90-
whereClauseFallback?: Record<string, WhereClauseFallback>;
103+
whereClauseFallback?: Record<string, WhereClauseCondition>;
91104
}
92105

93106
/**
@@ -123,7 +136,7 @@ export type TSQLQueryResult<T> = [QueryError, null] | [null, TSQLQuerySuccess<T>
123136
* Execute a TSQL query against ClickHouse
124137
*
125138
* This function:
126-
* 1. Compiles the TSQL query to ClickHouse SQL (parse, validate, inject tenant guards)
139+
* 1. Compiles the TSQL query to ClickHouse SQL (parse, validate, inject enforced WHERE clauses)
127140
* 2. Executes the query and returns validated results
128141
*
129142
* @example
@@ -132,10 +145,12 @@ export type TSQLQueryResult<T> = [QueryError, null] | [null, TSQLQuerySuccess<T>
132145
* name: "get_task_runs",
133146
* query: "SELECT id, status FROM task_runs WHERE status = 'completed' ORDER BY created_at DESC LIMIT 100",
134147
* schema: z.object({ id: z.string(), status: z.string() }),
135-
* organizationId: "org_123",
136-
* projectId: "proj_456",
137-
* environmentId: "env_789",
138148
* tableSchema: [taskRunsSchema],
149+
* enforcedWhereClause: {
150+
* organization_id: { op: "eq", value: "org_123" },
151+
* project_id: { op: "eq", value: "proj_456" },
152+
* environment_id: { op: "eq", value: "env_789" },
153+
* },
139154
* });
140155
* ```
141156
*/
@@ -152,10 +167,8 @@ export async function executeTSQL<TOut extends z.ZodSchema>(
152167
try {
153168
// 1. Compile the TSQL query to ClickHouse SQL
154169
const { sql, params, columns, hiddenColumns } = compileTSQL(options.query, {
155-
organizationId: options.organizationId,
156-
projectId: options.projectId,
157-
environmentId: options.environmentId,
158170
tableSchema: options.tableSchema,
171+
enforcedWhereClause: options.enforcedWhereClause,
159172
settings: options.querySettings,
160173
fieldMappings: options.fieldMappings,
161174
whereClauseFallback: options.whereClauseFallback,
@@ -284,9 +297,11 @@ export async function executeTSQL<TOut extends z.ZodSchema>(
284297
* name: "get_task_runs",
285298
* query: "SELECT * FROM task_runs LIMIT 10",
286299
* schema: taskRunRowSchema,
287-
* organizationId: "org_123",
288-
* projectId: "proj_456",
289-
* environmentId: "env_789",
300+
* enforcedWhereClause: {
301+
* organization_id: { op: "eq", value: "org_123" },
302+
* project_id: { op: "eq", value: "proj_456" },
303+
* environment_id: { op: "eq", value: "env_789" },
304+
* },
290305
* });
291306
* ```
292307
*/

internal-packages/clickhouse/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export {
5656
type TSQLQuerySuccess,
5757
type QueryStats,
5858
type FieldMappings,
59-
type WhereClauseFallback,
59+
type WhereClauseCondition,
6060
} from "./client/tsql.js";
6161
export type { OutputColumnMetadata } from "@internal/tsql";
6262

0 commit comments

Comments
 (0)