11import { ArrowDownTrayIcon , ArrowsPointingOutIcon , ArrowTrendingUpIcon , ClipboardIcon , TableCellsIcon } from "@heroicons/react/20/solid" ;
22import type { OutputColumnMetadata } from "@internal/clickhouse" ;
3- import { WhereClauseCondition } from "@internal/tsql" ;
3+ import { type WhereClauseCondition } from "@internal/tsql" ;
44import { useFetcher } from "@remix-run/react" ;
55import {
66 redirect ,
@@ -11,6 +11,7 @@ import parse from "parse-duration";
1111import { forwardRef , useCallback , useEffect , useImperativeHandle , useRef , useState } from "react" ;
1212import { flushSync } from "react-dom" ;
1313import { typedjson , useTypedFetcher , useTypedLoaderData } from "remix-typedjson" ;
14+ import simplur from "simplur" ;
1415import { z } from "zod" ;
1516import { AISparkleIcon } from "~/assets/icons/AISparkleIcon" ;
1617import { AlphaTitle } from "~/components/AlphaBadge" ;
@@ -24,7 +25,7 @@ import { autoFormatSQL, TSQLEditor } from "~/components/code/TSQLEditor";
2425import { TSQLResultsTable } from "~/components/code/TSQLResultsTable" ;
2526import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel" ;
2627import { PageBody , PageContainer } from "~/components/layout/AppLayout" ;
27- import { Button } from "~/components/primitives/Buttons" ;
28+ import { Button , LinkButton } from "~/components/primitives/Buttons" ;
2829import { Callout } from "~/components/primitives/Callout" ;
2930import { Card } from "~/components/primitives/charts/Card" ;
3031import {
@@ -62,18 +63,18 @@ import { findProjectBySlug } from "~/models/project.server";
6263import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server" ;
6364import { QueryPresenter , type QueryHistoryItem } from "~/presenters/v3/QueryPresenter.server" ;
6465import type { action as titleAction } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query.ai-title" ;
66+ import { getLimit } from "~/services/platform.v3.server" ;
6567import { executeQuery , type QueryScope } from "~/services/queryService.server" ;
6668import { requireUser } from "~/services/session.server" ;
6769import { downloadFile , rowsToCSV , rowsToJSON } from "~/utils/dataExport" ;
68- import { EnvironmentParamSchema } from "~/utils/pathBuilder" ;
70+ import { EnvironmentParamSchema , organizationBillingPath } from "~/utils/pathBuilder" ;
6971import { FEATURE_FLAG , validateFeatureFlagValue } from "~/v3/featureFlags.server" ;
7072import { querySchemas } from "~/v3/querySchemas" ;
7173import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route" ;
7274import { QueryHelpSidebar } from "./QueryHelpSidebar" ;
7375import { QueryHistoryPopover } from "./QueryHistoryPopover" ;
7476import type { AITimeFilter } from "./types" ;
7577import { formatQueryStats } from "./utils" ;
76- import { getLimit } from "~/services/platform.v3.server" ;
7778
7879/** Convert a Date or ISO string to ISO string format */
7980function toISOString ( value : Date | string ) : string {
@@ -199,6 +200,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
199200 hiddenColumns : null ,
200201 explainOutput : null ,
201202 generatedSql : null ,
203+ periodClipped : null ,
202204 } ,
203205 { status : 403 }
204206 ) ;
@@ -215,6 +217,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
215217 hiddenColumns : null ,
216218 explainOutput : null ,
217219 generatedSql : null ,
220+ periodClipped : null ,
218221 } ,
219222 { status : 404 }
220223 ) ;
@@ -231,6 +234,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
231234 hiddenColumns : null ,
232235 explainOutput : null ,
233236 generatedSql : null ,
237+ periodClipped : null ,
234238 } ,
235239 { status : 404 }
236240 ) ;
@@ -256,6 +260,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
256260 hiddenColumns : null ,
257261 explainOutput : null ,
258262 generatedSql : null ,
263+ periodClipped : null ,
259264 } ,
260265 { status : 400 }
261266 ) ;
@@ -274,30 +279,41 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
274279 defaultPeriod : DEFAULT_PERIOD ,
275280 } ) ;
276281
282+ // Calculate the effective "from" date the user is requesting (for period clipping check)
283+ // This is null only when the user specifies just a "to" date (rare case)
284+ let requestedFromDate : Date | null = null ;
285+ if ( timeFilter . from ) {
286+ requestedFromDate = new Date ( timeFilter . from ) ;
287+ } else if ( ! timeFilter . to ) {
288+ // Period specified (or default) - calculate from now
289+ const periodMs = parse ( timeFilter . period ?? DEFAULT_PERIOD ) ?? 7 * 24 * 60 * 60 * 1000 ;
290+ requestedFromDate = new Date ( Date . now ( ) - periodMs ) ;
291+ }
292+
293+ // Build the fallback WHERE condition based on what the user specified
277294 let triggeredAtFallback : WhereClauseCondition ;
278295 if ( timeFilter . from && timeFilter . to ) {
279- // Both from and to specified - use BETWEEN
280296 triggeredAtFallback = { op : "between" , low : timeFilter . from , high : timeFilter . to } ;
281297 } else if ( timeFilter . from ) {
282- // Only from specified
283298 triggeredAtFallback = { op : "gte" , value : timeFilter . from } ;
284299 } else if ( timeFilter . to ) {
285- // Only to specified
286300 triggeredAtFallback = { op : "lte" , value : timeFilter . to } ;
287301 } else {
288- // Period specified (or default) - calculate from now
289- const periodMs = parse ( timeFilter . period ?? DEFAULT_PERIOD ) ?? 7 * 24 * 60 * 60 * 1000 ;
290- triggeredAtFallback = { op : "gte" , value : new Date ( Date . now ( ) - periodMs ) } ;
302+ triggeredAtFallback = { op : "gte" , value : requestedFromDate ! } ;
291303 }
292304
293- const maxQueryPeriod = await getLimit ( project . organizationId , "queryPeriodDays" , 5 ) ;
305+ const maxQueryPeriod = await getLimit ( project . organizationId , "queryPeriodDays" , 30 ) ;
306+ const maxQueryPeriodDate = new Date ( Date . now ( ) - maxQueryPeriod * 24 * 60 * 60 * 1000 ) ;
307+
308+ // Check if the requested time period exceeds the plan limit
309+ const periodClipped = requestedFromDate !== null && requestedFromDate < maxQueryPeriodDate ;
294310
295311 // Force tenant isolation and time period limits
296312 const enforcedWhereClause = {
297313 organization_id : { op : "eq" , value : project . organizationId } ,
298314 project_id : scope === "project" || scope === "environment" ? { op : "eq" , value : project . id } : undefined ,
299315 environment_id : scope === "environment" ? { op : "eq" , value : environment . id } : undefined ,
300- triggered_at : { op : "gte" , value : new Date ( Date . now ( ) - maxQueryPeriod * 24 * 60 * 60 * 1000 ) }
316+ triggered_at : { op : "gte" , value : maxQueryPeriodDate } ,
301317 } satisfies Record < string , WhereClauseCondition | undefined > ;
302318
303319 try {
@@ -341,6 +357,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
341357 explainOutput : null ,
342358 generatedSql : null ,
343359 queryId : null ,
360+ periodClipped : null ,
344361 } ,
345362 { status : 400 }
346363 ) ;
@@ -355,6 +372,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
355372 explainOutput : result . explainOutput ?? null ,
356373 generatedSql : result . generatedSql ?? null ,
357374 queryId,
375+ periodClipped : periodClipped ? maxQueryPeriod : null ,
358376 } ) ;
359377 } catch ( err ) {
360378 const errorMessage = err instanceof Error ? err . message : "Unknown error executing query" ;
@@ -368,6 +386,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
368386 explainOutput : null ,
369387 generatedSql : null ,
370388 queryId : null ,
389+ periodClipped : null ,
371390 } ,
372391 { status : 500 }
373392 ) ;
@@ -781,16 +800,27 @@ export default function Page() {
781800 </ div >
782801 ) : results ?. rows && results ?. columns ? (
783802 < div className = "flex h-full flex-col overflow-hidden" >
784- { results . hiddenColumns && results . hiddenColumns . length > 0 && (
785- < div className = "p-2" >
786- < Callout variant = "warning" className = "shrink-0 text-sm" >
787- < code > SELECT *</ code > doesn't return all columns because it's slow. The
788- following columns are not shown:{ " " }
789- < span className = "font-mono text-xs" >
790- { results . hiddenColumns . join ( ", " ) }
791- </ span >
792- . Specify them explicitly to include them.
793- </ Callout >
803+ { ( results . hiddenColumns ?. length || results . periodClipped ) && (
804+ < div className = "flex flex-col gap-2 p-2" >
805+ { results . hiddenColumns && results . hiddenColumns . length > 0 && (
806+ < Callout variant = "warning" className = "shrink-0 text-sm" >
807+ < code > SELECT *</ code > doesn't return all columns because it's slow. The
808+ following columns are not shown:{ " " }
809+ < span className = "font-mono text-xs" >
810+ { results . hiddenColumns . join ( ", " ) }
811+ </ span >
812+ . Specify them explicitly to include them.
813+ </ Callout >
814+ ) }
815+ { results . periodClipped && (
816+ < Callout
817+ variant = "pricing"
818+ cta = { < LinkButton variant = "primary/small" to = { organizationBillingPath ( { slug : organization . slug } ) } > Upgrade</ LinkButton > }
819+ className = "items-center"
820+ >
821+ { simplur `Results are limited to the last ${ results . periodClipped } day[|s] based on your plan.` }
822+ </ Callout >
823+ ) }
794824 </ div >
795825 ) }
796826 < div className = "h-full bg-charcoal-900 p-2" >
0 commit comments