chore: clean up query insights query files

This handles a little bit of concern separation and isolating our hooks and sql queries so we can modify them.
This commit is contained in:
kemal committed 2025-08-28 12:55:18 +01:00
1 parent c4676cb5a2
commit 1837644c7e
9 files changed
+559 -598

No files matched your search

@@ -2,7 +2,7 @@ import { ArrowDown, ArrowUp } from 'lucide-react'
import { Column } from 'react-data-grid'
import { Badge } from 'ui'
import { type QueryInsightsQuery } from 'data/query-insights/query-metrics-query'
import { type InsightsQuery } from 'data/query-insights/insights-queries-query'
import { ColumnConfiguration, QUERY_INSIGHTS_TABLE_COLUMNS } from './QueryInsights.constants'
export const formatNumberWithCommas = (num: number | string | undefined | null): string => {
@@ -29,7 +29,7 @@ export const formatQueryInsightsColumns = ({
let gridColumns = QUERY_INSIGHTS_TABLE_COLUMNS.map((col) => {
const savedConfig = config.find((c) => c.id === col.id)
const res: Column<QueryInsightsQuery> = {
const res: Column<InsightsQuery> = {
key: col.id,
name: col.name,
resizable: col.resizable ?? true,
@@ -53,8 +53,8 @@ export const formatQueryInsightsColumns = ({
)
},
renderCell: (props) => {
const value = props.row?.[col.id as keyof QueryInsightsQuery]
const value = props.row?.[col.id as keyof InsightsQuery]
if (col.id === 'query') {
return (
<div className="w-full flex items-center gap-x-2">
@@ -81,7 +81,10 @@ export const formatQueryInsightsColumns = ({
return (
<div className="w-full flex-col justify-center font-mono text-xs inline-flex">
<span>
<Badge variant={getBadgeVariant(value as string)} className="text-xs !text-center !inline-flex">
<Badge
variant={getBadgeVariant(value as string)}
className="text-xs !text-center !inline-flex"
>
{value}
</Badge>
</span>
@@ -111,7 +114,9 @@ export const formatQueryInsightsColumns = ({
return (
<div className="w-full flex flex-col justify-center font-mono text-xs text-right">
<p>{formattedValue}</p>
{value && <p className="text-foreground-lighter">{((value as number) / 1000).toFixed(2)}s</p>}
{value && (
<p className="text-foreground-lighter">{((value as number) / 1000).toFixed(2)}s</p>
)}
</div>
)
}
@@ -121,7 +126,9 @@ export const formatQueryInsightsColumns = ({
return (
<div className="w-full flex flex-col justify-center font-mono text-xs text-right">
<p>{formattedValue}</p>
{value && <p className="text-foreground-lighter">{((value as number) / 1000).toFixed(2)}s</p>}
{value && (
<p className="text-foreground-lighter">{((value as number) / 1000).toFixed(2)}s</p>
)}
</div>
)
}
@@ -10,9 +10,9 @@ import {
} from 'ui'
import { QueryRowExplorer } from './QueryRowExplorer'
import {
useQueryInsightsMetrics,
usePreFetchQueryInsightsData,
} from 'data/query-insights/query-metrics-query'
useInsightsMetricsQuery,
useInsightsPrefetchQuery,
} from 'data/query-insights/insights-metrics-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import ComposedChart from 'components/ui/Charts/ComposedChart'
import { MultiAttribute } from 'components/ui/Charts/ComposedChart.utils'
@@ -59,7 +59,7 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
}, [startTime, endTime])
// Pre-fetch query data for the QueryRowExplorer
usePreFetchQueryInsightsData(project?.ref, effectiveStartTime, effectiveEndTime)
useInsightsPrefetchQuery(project?.ref, effectiveStartTime, effectiveEndTime)
// Debug logging
console.log('QueryMetricExplorer Debug:', {
@@ -73,7 +73,7 @@ export const QueryMetricExplorer = ({ startTime, endTime }: QueryMetricExplorerP
data: metricsData,
isLoading,
error,
} = useQueryInsightsMetrics(project?.ref, selectedMetric, effectiveStartTime, effectiveEndTime)
} = useInsightsMetricsQuery(project?.ref, selectedMetric, effectiveStartTime, effectiveEndTime)
// Debug logging for query results
console.log('QueryMetricExplorer Query Results:', {
@@ -1,5 +1,5 @@
import { useMemo } from 'react'
import { useQueryInsightsGlance } from 'data/query-insights/query-glance-query'
import { useInsightsGlanceQuery } from 'data/query-insights/insights-glance-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Card, CardContent } from 'ui'
import { Database, Clock, AlertCircle } from 'lucide-react'
@@ -36,7 +36,7 @@ export const QueryQuickGlance = ({ startTime, endTime }: QueryQuickGlanceProps)
data: glanceData,
isLoading,
error,
} = useQueryInsightsGlance(project?.ref, effectiveStartTime, effectiveEndTime)
} = useInsightsGlanceQuery(project?.ref, effectiveStartTime, effectiveEndTime)
// Debug logging for glance results
console.log('QueryQuickGlance Results:', {
@@ -21,15 +21,15 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FilterPopover } from 'components/ui/FilterPopover'
import {
useQueryInsightsQueries,
type QueryInsightsQuery,
} from 'data/query-insights/query-metrics-query'
useInsightsQueriesQuery,
type InsightsQuery,
} from 'data/query-insights/insights-queries-query'
import {
useSingleQueryLatency,
useSingleQueryCalls,
useSingleQueryRows,
useSingleQueryRowsWritten,
} from 'data/query-insights/single-query-latency-query'
useInsightsSingleQueryLatency,
useInsightsSingleQueryCalls,
useInsightsSingleQueryRows,
useInsightsSingleQueryRowsWritten,
} from 'data/query-insights/insights-single-ql-query'
import { ColumnConfiguration, QUERY_INSIGHTS_TABLE_COLUMNS } from './QueryInsights.constants'
import { formatQueryInsightsColumns } from './QueryInsights.utils'
@@ -44,7 +44,7 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const { data: queries, isLoading, error } = useQueryInsightsQueries(ref, startTime, endTime)
const { data: queries, isLoading, error } = useInsightsQueriesQuery(ref, startTime, endTime)
// Debug logging
console.log('QueryRowExplorer Debug:', {
@@ -128,15 +128,15 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
total_cost_after: 0,
index_statements: [],
errors: [],
} as QueryInsightsQuery,
} as InsightsQuery,
]
// Sort data based on current sort state
const sortedData = [...reportData].sort((a, b) => {
if (!sort) return 0
const aValue = a[sort.column as keyof QueryInsightsQuery]
const bValue = b[sort.column as keyof QueryInsightsQuery]
const aValue = a[sort.column as keyof InsightsQuery]
const bValue = b[sort.column as keyof InsightsQuery]
if (aValue === undefined || bValue === undefined) return 0
@@ -159,28 +159,28 @@ export const QueryRowExplorer = ({ startTime, endTime }: QueryRowExplorerProps)
const selectedQuery = selectedRow !== undefined ? filteredData[selectedRow] : undefined
// Single query metrics hooks
const { data: singleQueryLatency } = useSingleQueryLatency(
const { data: singleQueryLatency } = useInsightsSingleQueryLatency(
ref,
startTime,
endTime,
selectedQuery?.query_id?.toString()
)
const { data: singleQueryCalls } = useSingleQueryCalls(
const { data: singleQueryCalls } = useInsightsSingleQueryCalls(
ref,
startTime,
endTime,
selectedQuery?.query_id?.toString()
)
const { data: singleQueryRows } = useSingleQueryRows(
const { data: singleQueryRows } = useInsightsSingleQueryRows(
ref,
startTime,
endTime,
selectedQuery?.query_id?.toString()
)
const { data: singleQueryRowsWritten } = useSingleQueryRowsWritten(
const { data: singleQueryRowsWritten } = useInsightsSingleQueryRowsWritten(
ref,
startTime,
endTime,
@@ -2,7 +2,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { queryInsightsKeys } from './keys'
export type QueryInsightsGlance = {
export type InsightsGlance = {
unique_queries: number
total_queries: number
avg_query_time: number
@@ -11,7 +11,7 @@ export type QueryInsightsGlance = {
avg_error_rate: number
}
export const getQueriesGlanceMetrics = (startTime: string, endTime: string) =>
export const getInsightsGlanceMetrics = (startTime: string, endTime: string) =>
/* SQL */ `
SELECT
COUNT(DISTINCT query) as unique_queries,
@@ -30,18 +30,18 @@ export const getQueriesGlanceMetrics = (startTime: string, endTime: string) =>
AND bucket_done = true
`.trim()
export type QueryInsightsGlanceVariables = {
export type InsightsGlanceVariables = {
projectRef?: string
connectionString?: string | null
startTime: string
endTime: string
}
export async function getQueryInsightsGlance(
{ projectRef, connectionString, startTime, endTime }: QueryInsightsGlanceVariables,
export async function getInsightsGlance(
{ projectRef, connectionString, startTime, endTime }: InsightsGlanceVariables,
signal?: AbortSignal
) {
const sql = getQueriesGlanceMetrics(startTime, endTime)
const sql = getInsightsGlanceMetrics(startTime, endTime)
const { result } = await executeSql(
{
@@ -53,33 +53,33 @@ export async function getQueryInsightsGlance(
signal
)
return result[0] as QueryInsightsGlance
return result[0] as InsightsGlance
}
export type QueryInsightsGlanceData = Awaited<ReturnType<typeof getQueryInsightsGlance>>
export type QueryInsightsGlanceError = ExecuteSqlError
export type InsightsGlanceData = Awaited<ReturnType<typeof getInsightsGlance>>
export type InsightsGlanceError = ExecuteSqlError
export function useQueryInsightsGlance(
export function useInsightsGlanceQuery(
projectRef: string | undefined,
startTime: string,
endTime: string,
options?: UseQueryOptions<QueryInsightsGlanceData>
options?: UseQueryOptions<InsightsGlanceData>
) {
return useQuery({
queryKey: queryInsightsKeys.glance(projectRef, startTime, endTime),
queryFn: async () => {
if (!projectRef) throw new Error('Project ref is required')
const sql = getQueriesGlanceMetrics(startTime, endTime)
console.log(`[useQueryInsightsGlance] Executing SQL:`, sql)
const sql = getInsightsGlanceMetrics(startTime, endTime)
console.log(`[useInsightsGlanceQuery] Executing SQL:`, sql)
const { result } = await executeSql({
projectRef,
sql,
})
console.log(`[useQueryInsightsGlance] Result:`, result)
return result[0] as QueryInsightsGlance
console.log(`[useInsightsGlanceQuery] Result:`, result)
return result[0] as InsightsGlance
},
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
...options,
@@ -0,0 +1,261 @@
import { useQuery, UseQueryOptions, useQueryClient } from '@tanstack/react-query'
import { executeSql } from '../sql/execute-sql-query'
import { queryInsightsKeys } from './keys'
import { useEffect, useCallback } from 'react'
export type InsightsMetric = {
timestamp: string
value?: number
rows?: number
cmd_type_text?: string
database: string
shared_blks_read?: number
shared_blks_hit?: number
shared_blks_dirtied?: number
shared_blks_written?: number
mean_exec_time?: number
total_exec_time?: number
p50?: number
p95?: number
p99?: number
p99_9?: number
}
export type InsightsMetrics = {
queries: {
total: number
total_time: number
avg_time: number
total_rows: number
}
cache: {
hit_ratio: number
hits: number
misses: number
}
errors: {
total: number
}
}
const getMetricsSql = (metric: string, startTime: string, endTime: string) => {
switch (metric) {
case 'rows_read':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
SUM(rows) as value,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true
AND cmd_type = 1
GROUP BY bucket_start_time, datname
ORDER BY timestamp ASC
`
case 'query_latency':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
percentile_cont(0.50) WITHIN GROUP (ORDER BY mean_exec_time) as p50,
percentile_cont(0.95) WITHIN GROUP (ORDER BY mean_exec_time) as p95,
percentile_cont(0.99) WITHIN GROUP (ORDER BY mean_exec_time) as p99,
percentile_cont(0.999) WITHIN GROUP (ORDER BY mean_exec_time) as p99_9,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true
GROUP BY bucket_start_time, datname
ORDER BY timestamp ASC
`
case 'queries_per_second':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
COUNT(*) / 300.0 as value,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true
GROUP BY bucket_start_time, datname
ORDER BY timestamp ASC
`
case 'calls':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
SUM(calls) as value,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true
GROUP BY bucket_start_time, datname
ORDER BY timestamp ASC
`
case 'cache_hits':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
SUM(shared_blks_hit) as shared_blks_hit,
SUM(shared_blks_read) as shared_blks_read,
SUM(shared_blks_dirtied) as shared_blks_dirtied,
SUM(shared_blks_written) as shared_blks_written,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true
GROUP BY bucket_start_time, datname
ORDER BY timestamp ASC
`
case 'issues':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) as value,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true
GROUP BY bucket_start_time, datname
ORDER BY timestamp ASC
`
default:
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
SUM(rows) / 300 as value,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true
GROUP BY bucket_start_time, datname
ORDER BY timestamp ASC
`
}
}
export function useInsightsMetricsQuery(
projectRef: string | undefined,
metric: string,
startTime: string,
endTime: string,
options?: UseQueryOptions<InsightsMetric[]>
) {
return useQuery({
queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime),
queryFn: async () => {
if (!projectRef) throw new Error('Project ref is required')
const sql = getMetricsSql(metric, startTime, endTime)
console.log(`[useInsightsMetricsQuery] Executing SQL for ${metric}:`, sql)
const { result } = await executeSql({
projectRef,
sql,
})
console.log(`[useInsightsMetricsQuery] Result for ${metric}:`, result)
return result as InsightsMetric[]
},
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
...options,
})
}
// Hook to pre-fetch all metrics data for the current time range
export function useInsightsPrefetchQuery(
projectRef: string | undefined,
startTime: string,
endTime: string
) {
const queryClient = useQueryClient()
useEffect(() => {
if (!projectRef) return
// Pre-fetch all metric types
const metricTypes: Array<{ id: string; label: string }> = [
{ id: 'query_latency', label: 'Query latency' },
{ id: 'rows_read', label: 'Rows read' },
{ id: 'calls', label: 'Calls' },
{ id: 'cache_hits', label: 'Cache hits' },
{ id: 'issues', label: 'Issues' },
]
// Pre-fetch metrics data
metricTypes.forEach((metric) => {
queryClient.prefetchQuery({
queryKey: queryInsightsKeys.metrics(projectRef, metric.id, startTime, endTime),
queryFn: async () => {
const sql = getMetricsSql(metric.id, startTime, endTime)
console.log(`[useInsightsPrefetchQuery] Executing SQL for ${metric.id}:`, sql)
const { result } = await executeSql({
projectRef,
sql,
})
console.log(`[useInsightsPrefetchQuery] Result for ${metric.id}:`, result)
return result as InsightsMetric[]
},
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
})
})
}, [projectRef, startTime, endTime, queryClient])
}
// Hook for better cache management and manual refresh control
export function useInsightsCacheManager(
projectRef: string | undefined,
startTime: string,
endTime: string
) {
const queryClient = useQueryClient()
const refreshAllData = useCallback(async () => {
if (!projectRef) return
const metricTypes = ['query_latency', 'rows_read', 'calls', 'cache_hits', 'issues']
// Invalidate and refetch all metrics
await Promise.all([
...metricTypes.map((metric) =>
queryClient.invalidateQueries({
queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime),
})
),
])
}, [projectRef, startTime, endTime, queryClient])
const isAnyDataStale = useCallback(() => {
if (!projectRef) return false
const metricTypes = ['query_latency', 'rows_read', 'calls', 'cache_hits', 'issues']
return metricTypes.some((metric) => {
const query = queryClient.getQueryData(
queryInsightsKeys.metrics(projectRef, metric, startTime, endTime)
)
return !query
})
}, [projectRef, startTime, endTime, queryClient])
return {
refreshAllData,
isAnyDataStale,
}
}
@@ -0,0 +1,202 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { executeSql } from '../sql/execute-sql-query'
import { queryInsightsKeys } from './keys'
export type InsightsQuery = {
query_id: number
query: string
total_time: number
calls: number
rows_read: number
rows_insert: number
rows_update: number
rows_delete: number
shared_blks_read: number
shared_blks_hit: number
mean_exec_time: number
database: string
timestamp: string
cmd_type_text: string
application_name: string
badness_score: number
slowness_rating: string
// index_advisor results
startup_cost_before?: number
startup_cost_after?: number
total_cost_before?: number
total_cost_after?: number
index_statements?: string[]
errors?: string[]
// Error tracking fields
error_count?: number
}
const getQueriesSql = (startTime: string, endTime: string) => /* SQL */ `
WITH base_queries AS (
SELECT
queryid AS query_id,
COALESCE(MAX(top_query), MAX(query)) AS query,
SUM(total_exec_time) AS total_time,
SUM(calls) AS calls,
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) AS rows_read,
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) AS rows_insert,
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) AS rows_update,
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) AS rows_delete,
SUM(shared_blks_read) AS shared_blks_read,
SUM(shared_blks_hit) AS shared_blks_hit,
SUM(total_exec_time) / NULLIF(SUM(calls), 0) AS mean_exec_time,
STRING_AGG(DISTINCT datname, ', ') AS database,
MAX(bucket_start_time) AS timestamp,
MAX(get_cmd_type(cmd_type)) AS cmd_type_text,
STRING_AGG(DISTINCT COALESCE(application_name, 'Unknown'), ', ') AS application_name,
(
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END)
) AS rows_total,
COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) AS error_count,
(
(SUM(total_exec_time) / NULLIF(SUM(calls), 1)) *
LOG(GREATEST(SUM(calls), 1)) /
GREATEST(
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END),
1)
) AS badness_score,
CASE
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 100 THEN 'GREAT'
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 200 THEN 'ACCEPTABLE'
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 1000 THEN 'NOTICEABLE'
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 5000 THEN 'SLOW'
ELSE 'CRITICAL'
END AS slowness_rating
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'
AND bucket_start_time <= '${endTime}'
AND bucket_done = true
GROUP BY queryid
)
SELECT
q.*,
ia.*
FROM
base_queries q
LEFT JOIN LATERAL (
SELECT *
FROM index_advisor(q.query)
) ia ON q.query ILIKE 'SELECT%' OR q.query ILIKE 'WITH%'
ORDER BY badness_score DESC
LIMIT 9999
`
const getQueriesWithErrorsSql = (startTime: string, endTime: string) => /* SQL */ `
WITH base_queries AS (
SELECT
queryid AS query_id,
COALESCE(MAX(top_query), MAX(query)) AS query,
SUM(total_exec_time) AS total_time,
SUM(calls) AS calls,
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) AS rows_read,
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) AS rows_insert,
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) AS rows_update,
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) AS rows_delete,
SUM(shared_blks_read) AS shared_blks_read,
SUM(shared_blks_hit) AS shared_blks_hit,
SUM(total_exec_time) / NULLIF(SUM(calls), 0) AS mean_exec_time,
STRING_AGG(DISTINCT datname, ', ') AS database,
MAX(bucket_start_time) AS timestamp,
MAX(get_cmd_type(cmd_type)) AS cmd_type_text,
STRING_AGG(DISTINCT COALESCE(application_name, 'Unknown'), ', ') AS application_name,
(
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END)
) AS rows_total,
COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) AS error_count,
(
(SUM(total_exec_time) / NULLIF(SUM(calls), 1)) *
LOG(GREATEST(SUM(calls), 1)) /
GREATEST(
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END),
1)
) AS badness_score,
CASE
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 100 THEN 'GREAT'
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 200 THEN 'ACCEPTABLE'
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 1000 THEN 'NOTICEABLE'
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 5000 THEN 'SLOW'
ELSE 'CRITICAL'
END AS slowness_rating
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'
AND bucket_start_time <= '${endTime}'
AND bucket_done = true
AND mean_exec_time > 1000
AND calls > 1
GROUP BY queryid
)
SELECT
q.*,
ia.*
FROM
base_queries q
LEFT JOIN LATERAL (
SELECT *
FROM index_advisor(q.query)
) ia ON q.query ILIKE 'SELECT%' OR q.query ILIKE 'WITH%'
ORDER BY error_count DESC, badness_score DESC
LIMIT 9999
`
export function useInsightsQueriesQuery(
projectRef: string | undefined,
startTime: string,
endTime: string,
options?: UseQueryOptions<InsightsQuery[]>
) {
return useQuery({
queryKey: queryInsightsKeys.queries(projectRef, startTime, endTime),
queryFn: async () => {
if (!projectRef) throw new Error('Project ref is required')
const { result } = await executeSql({
projectRef,
sql: getQueriesSql(startTime, endTime),
})
return result as InsightsQuery[]
},
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
...options,
})
}
export function useInsightsQueriesWithErrorsQuery(
projectRef: string | undefined,
startTime: string,
endTime: string,
options?: UseQueryOptions<InsightsQuery[]>
) {
return useQuery({
queryKey: queryInsightsKeys.queriesWithErrors(projectRef, startTime, endTime),
queryFn: async () => {
if (!projectRef) throw new Error('Project ref is required')
const { result } = await executeSql({
projectRef,
sql: getQueriesWithErrorsSql(startTime, endTime),
})
return result as InsightsQuery[]
},
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
...options,
})
}
@@ -1,25 +1,26 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { executeSql } from '../sql/execute-sql-query'
import { QueryInsightsMetric } from './query-metrics-query'
import { InsightsMetric } from './insights-metrics-query'
const singleQueryLatencyKeys = {
base: ['single-query-latency'] as const,
base: ['projects', 'query-insights', 'single-query'] as const,
latency: (
projectRef: string | undefined,
queryId: string | undefined,
startTime: string,
endTime: string
) => [...singleQueryLatencyKeys.base, projectRef, queryId, startTime, endTime] as const,
) =>
[...singleQueryLatencyKeys.base, 'latency', projectRef, queryId, startTime, endTime] as const,
}
const singleQueryCallsKeys = {
base: ['single-query-calls'] as const,
base: ['projects', 'query-insights', 'single-query-calls'] as const,
calls: (
projectRef: string | undefined,
queryId: string | undefined,
startTime: string,
endTime: string
) => [...singleQueryCallsKeys.base, projectRef, queryId, startTime, endTime] as const,
) => [...singleQueryCallsKeys.base, 'calls', projectRef, queryId, startTime, endTime] as const,
}
const getSingleQueryLatencySql = (
@@ -30,42 +31,43 @@ const getSingleQueryLatencySql = (
SELECT
bucket_start_time as timestamp,
mean_exec_time as value,
calls,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true -- Only include completed buckets
AND queryid = ${queryId}::bigint
AND bucket_done = true
AND queryid = ${queryId}
ORDER BY timestamp ASC
`
const getSingleQueryRowsSql = (queryId: string, startTime: string, endTime: string) => /* SQL */ `
SELECT
bucket_start_time as timestamp,
SUM(rows) as value,
rows as value,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true
AND queryid = ${queryId}::bigint
AND cmd_type = 1
GROUP BY bucket_start_time, datname
AND queryid = ${queryId}
ORDER BY timestamp ASC
`
const getSingleQueryRowsWrittenSql = (queryId: string, startTime: string, endTime: string) => /* SQL */ `
const getSingleQueryRowsWrittenSql = (
queryId: string,
startTime: string,
endTime: string
) => /* SQL */ `
SELECT
bucket_start_time as timestamp,
SUM(CASE WHEN cmd_type IN (2, 3, 4) THEN rows ELSE 0 END) as value,
rows as value,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true
AND queryid = ${queryId}::bigint
GROUP BY bucket_start_time, datname
AND queryid = ${queryId}
AND cmd_type IN (2, 3, 4) -- INSERT, UPDATE, DELETE
ORDER BY timestamp ASC
`
@@ -78,17 +80,16 @@ const getSingleQueryCallsSql = (queryId: string, startTime: string, endTime: str
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true
AND queryid = ${queryId}::bigint
GROUP BY bucket_start_time, calls, datname
AND queryid = ${queryId}
ORDER BY timestamp ASC
`
export function useSingleQueryLatency(
export function useInsightsSingleQueryLatency(
projectRef: string | undefined,
startTime: string,
endTime: string,
queryId: string | undefined,
options?: UseQueryOptions<QueryInsightsMetric[]>
options?: UseQueryOptions<InsightsMetric[]>
) {
return useQuery({
queryKey: singleQueryLatencyKeys.latency(projectRef, queryId, startTime, endTime),
@@ -102,19 +103,19 @@ export function useSingleQueryLatency(
})
console.log('Chart data points:', result)
return result as QueryInsightsMetric[]
return result as InsightsMetric[]
},
enabled: !!queryId && !!projectRef,
...options,
})
}
export function useSingleQueryRows(
export function useInsightsSingleQueryRows(
projectRef: string | undefined,
startTime: string,
endTime: string,
queryId: string | undefined,
options?: UseQueryOptions<QueryInsightsMetric[]>
options?: UseQueryOptions<InsightsMetric[]>
) {
return useQuery({
queryKey: [...singleQueryLatencyKeys.base, 'rows', projectRef, queryId, startTime, endTime],
@@ -123,56 +124,63 @@ export function useSingleQueryRows(
if (!queryId) return []
const sql = getSingleQueryRowsSql(queryId, startTime, endTime)
console.log('[useSingleQueryRows] Executing SQL:', sql)
console.log('[useInsightsSingleQueryRows] Executing SQL:', sql)
const { result } = await executeSql({
projectRef,
sql,
})
console.log('[useSingleQueryRows] Result:', result)
return result as QueryInsightsMetric[]
console.log('[useInsightsSingleQueryRows] Result:', result)
return result as InsightsMetric[]
},
enabled: !!queryId && !!projectRef,
...options,
})
}
export function useSingleQueryRowsWritten(
export function useInsightsSingleQueryRowsWritten(
projectRef: string | undefined,
startTime: string,
endTime: string,
queryId: string | undefined,
options?: UseQueryOptions<QueryInsightsMetric[]>
options?: UseQueryOptions<InsightsMetric[]>
) {
return useQuery({
queryKey: [...singleQueryLatencyKeys.base, 'rows-written', projectRef, queryId, startTime, endTime],
queryKey: [
...singleQueryLatencyKeys.base,
'rows-written',
projectRef,
queryId,
startTime,
endTime,
],
queryFn: async () => {
if (!projectRef) throw new Error('Project ref is required')
if (!queryId) return []
const sql = getSingleQueryRowsWrittenSql(queryId, startTime, endTime)
console.log('[useSingleQueryRowsWritten] Executing SQL:', sql)
console.log('[useInsightsSingleQueryRowsWritten] Executing SQL:', sql)
const { result } = await executeSql({
projectRef,
sql,
})
console.log('[useSingleQueryRowsWritten] Result:', result)
return result as QueryInsightsMetric[]
console.log('[useInsightsSingleQueryRowsWritten] Result:', result)
return result as InsightsMetric[]
},
enabled: !!queryId && !!projectRef,
...options,
})
}
export function useSingleQueryCalls(
export function useInsightsSingleQueryCalls(
projectRef: string | undefined,
startTime: string,
endTime: string,
queryId: string | undefined,
options?: UseQueryOptions<QueryInsightsMetric[]>
options?: UseQueryOptions<InsightsMetric[]>
) {
return useQuery({
queryKey: singleQueryCallsKeys.calls(projectRef, queryId, startTime, endTime),
@@ -185,9 +193,9 @@ export function useSingleQueryCalls(
sql: getSingleQueryCallsSql(queryId, startTime, endTime),
})
return result as QueryInsightsMetric[]
return result as InsightsMetric[]
},
enabled: !!queryId && !!projectRef,
...options,
})
}
}
@@ -1,517 +0,0 @@
import { useQuery, UseQueryOptions, useQueryClient } from '@tanstack/react-query'
import { executeSql } from '../sql/execute-sql-query'
import { queryInsightsKeys } from './keys'
import { useEffect, useCallback } from 'react'
export type QueryInsightsMetric = {
timestamp: string
value?: number
rows?: number
cmd_type_text?: string
database: string
shared_blks_read?: number
shared_blks_hit?: number
shared_blks_dirtied?: number
shared_blks_written?: number
mean_exec_time?: number
total_exec_time?: number
p50?: number
p95?: number
p99?: number
p99_9?: number
}
export type QueryInsightsQuery = {
query_id: number
query: string
total_time: number
calls: number
rows_read: number
rows_insert: number
rows_update: number
rows_delete: number
shared_blks_read: number
shared_blks_hit: number
mean_exec_time: number
database: string
timestamp: string
cmd_type_text: string
application_name: string
badness_score: number
slowness_rating: string
// index_advisor results
startup_cost_before?: number
startup_cost_after?: number
total_cost_before?: number
total_cost_after?: number
index_statements?: string[]
errors?: string[]
// Error tracking fields
error_count?: number
}
export type QueryInsightsMetrics = {
queries: {
total: number
total_time: number
avg_time: number
total_rows: number
}
cache: {
hit_ratio: number
hits: number
misses: number
}
errors: {
total: number
}
}
const getMetricsSql = (metric: string, startTime: string, endTime: string) => {
switch (metric) {
case 'rows_read':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
SUM(rows) as value,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true
AND cmd_type = 1
GROUP BY bucket_start_time, datname
ORDER BY timestamp ASC
`
case 'query_latency':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
percentile_cont(0.50) WITHIN GROUP (ORDER BY mean_exec_time) as p50,
percentile_cont(0.95) WITHIN GROUP (ORDER BY mean_exec_time) as p95,
percentile_cont(0.99) WITHIN GROUP (ORDER BY mean_exec_time) as p99,
percentile_cont(0.999) WITHIN GROUP (ORDER BY mean_exec_time) as p99_9,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true -- Only include completed buckets
GROUP BY bucket_start_time, datname
ORDER BY timestamp ASC
`
case 'queries_per_second':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
COUNT(*) / 300.0 as value, -- Convert to per-second rate (5 mins = 300 seconds)
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true -- Only include completed buckets
GROUP BY bucket_start_time, datname
ORDER BY timestamp ASC
`
case 'calls':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
SUM(calls) as value,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true -- Only include completed buckets
GROUP BY bucket_start_time, datname
ORDER BY timestamp ASC
`
case 'cache_hits':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
SUM(shared_blks_hit) as shared_blks_hit,
SUM(shared_blks_read) as shared_blks_read,
SUM(shared_blks_dirtied) as shared_blks_dirtied,
SUM(shared_blks_written) as shared_blks_written,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true -- Only include completed buckets
GROUP BY bucket_start_time, datname
ORDER BY timestamp ASC
`
case 'issues':
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) as value,
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true -- Only include completed buckets
GROUP BY bucket_start_time, datname
ORDER BY timestamp ASC
`
default:
return /* SQL */ `
SELECT
bucket_start_time as timestamp,
SUM(rows) / 300 as value, -- Convert to per-second rate (5 mins = 300 seconds)
datname as database
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'::timestamptz
AND bucket_start_time <= '${endTime}'::timestamptz
AND bucket_done = true -- Only include completed buckets
GROUP BY bucket_start_time, datname
ORDER BY timestamp ASC
`
}
}
const getQueriesSql = (startTime: string, endTime: string) => /* SQL */ `
WITH base_queries AS (
SELECT
queryid AS query_id,
-- Prefer top-level query if available, else fallback to normalized form
COALESCE(MAX(top_query), MAX(query)) AS query,
-- Total time spent executing this query across all calls
SUM(total_exec_time) AS total_time,
-- Number of times this query was executed
SUM(calls) AS calls,
-- Total rows affected or returned depending on command type
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) AS rows_read, -- SELECT
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) AS rows_insert, -- INSERT
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) AS rows_update, -- UPDATE
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) AS rows_delete, -- DELETE
-- Buffer activity: disk reads vs cache hits
SUM(shared_blks_read) AS shared_blks_read,
SUM(shared_blks_hit) AS shared_blks_hit,
-- Mean execution time per call
SUM(total_exec_time) / NULLIF(SUM(calls), 0) AS mean_exec_time,
-- Metadata
STRING_AGG(DISTINCT datname, ', ') AS database,
MAX(bucket_start_time) AS timestamp,
MAX(get_cmd_type(cmd_type)) AS cmd_type_text,
STRING_AGG(DISTINCT COALESCE(application_name, 'Unknown'), ', ') AS application_name,
-- Total number of rows affected or returned across all executions
(
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END)
) AS rows_total,
-- Error information (using available columns)
COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) AS error_count,
-- Heuristic "badness score":
-- Penalizes queries that are slow, frequent, and touch few rows
(
(SUM(total_exec_time) / NULLIF(SUM(calls), 1)) * -- mean execution time
LOG(GREATEST(SUM(calls), 1)) / -- scaled by frequency
GREATEST( -- penalize low-row queries
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END),
1)
) AS badness_score,
-- Slowness rating based on mean execution time
CASE
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 100 THEN 'GREAT'
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 200 THEN 'ACCEPTABLE'
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 1000 THEN 'NOTICEABLE'
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 5000 THEN 'SLOW'
ELSE 'CRITICAL'
END AS slowness_rating
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'
AND bucket_start_time <= '${endTime}'
AND bucket_done = true
-- AND cmd_type IN (1, 2, 3, 4) -- Only consider SELECT, INSERT, UPDATE, DELETE
GROUP BY queryid
)
SELECT
q.*,
ia.*
FROM
base_queries q
LEFT JOIN LATERAL (
SELECT *
FROM index_advisor(q.query)
) ia ON q.query ILIKE 'SELECT%' OR q.query ILIKE 'WITH%'
ORDER BY badness_score DESC
LIMIT 9999;
`
const getQueriesWithErrorsSql = (startTime: string, endTime: string) => /* SQL */ `
WITH base_queries AS (
SELECT
queryid AS query_id,
-- Prefer top-level query if available, else fallback to normalized form
COALESCE(MAX(top_query), MAX(query)) AS query,
-- Total time spent executing this query across all calls
SUM(total_exec_time) AS total_time,
-- Number of times this query was executed
SUM(calls) AS calls,
-- Total rows affected or returned depending on command type
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) AS rows_read, -- SELECT
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) AS rows_insert, -- INSERT
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) AS rows_update, -- UPDATE
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END) AS rows_delete, -- DELETE
-- Buffer activity: disk reads vs cache hits
SUM(shared_blks_read) AS shared_blks_read,
SUM(shared_blks_hit) AS shared_blks_hit,
-- Mean execution time per call
SUM(total_exec_time) / NULLIF(SUM(calls), 0) AS mean_exec_time,
-- Metadata
STRING_AGG(DISTINCT datname, ', ') AS database,
MAX(bucket_start_time) AS timestamp,
MAX(get_cmd_type(cmd_type)) AS cmd_type_text,
STRING_AGG(DISTINCT COALESCE(application_name, 'Unknown'), ', ') AS application_name,
-- Total number of rows affected or returned across all executions
(
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END)
) AS rows_total,
-- Error information (using available columns)
COUNT(CASE WHEN mean_exec_time > 1000 AND calls > 1 THEN 1 END) AS error_count,
-- Heuristic "badness score":
-- Penalizes queries that are slow, frequent, and touch few rows
(
(SUM(total_exec_time) / NULLIF(SUM(calls), 1)) * -- mean execution time
LOG(GREATEST(SUM(calls), 1)) / -- scaled by frequency
GREATEST( -- penalize low-row queries
SUM(CASE WHEN cmd_type = 1 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 2 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 3 THEN rows ELSE 0 END) +
SUM(CASE WHEN cmd_type = 4 THEN rows ELSE 0 END),
1)
) AS badness_score,
-- Slowness rating based on mean execution time
CASE
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 100 THEN 'GREAT'
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 200 THEN 'ACCEPTABLE'
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 1000 THEN 'NOTICEABLE'
WHEN SUM(total_exec_time) / NULLIF(SUM(calls), 0) < 5000 THEN 'SLOW'
ELSE 'CRITICAL'
END AS slowness_rating
FROM pg_stat_monitor
WHERE bucket_start_time >= '${startTime}'
AND bucket_start_time <= '${endTime}'
AND bucket_done = true
AND mean_exec_time > 1000
AND calls > 1 -- Only include queries that are slow and called multiple times
GROUP BY queryid
)
SELECT
q.*,
ia.*
FROM
base_queries q
LEFT JOIN LATERAL (
SELECT *
FROM index_advisor(q.query)
) ia ON q.query ILIKE 'SELECT%' OR q.query ILIKE 'WITH%'
ORDER BY error_count DESC, badness_score DESC
LIMIT 9999;
`
export function useQueryInsightsMetrics(
projectRef: string | undefined,
metric: string,
startTime: string,
endTime: string,
options?: UseQueryOptions<QueryInsightsMetric[]>
) {
return useQuery({
queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime),
queryFn: async () => {
if (!projectRef) throw new Error('Project ref is required')
const sql = getMetricsSql(metric, startTime, endTime)
console.log(`[useQueryInsightsMetrics] Executing SQL for ${metric}:`, sql)
const { result } = await executeSql({
projectRef,
sql,
})
console.log(`[useQueryInsightsMetrics] Result for ${metric}:`, result)
return result as QueryInsightsMetric[]
},
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
...options,
})
}
export function useQueryInsightsQueries(
projectRef: string | undefined,
startTime: string,
endTime: string,
options?: UseQueryOptions<QueryInsightsQuery[]>
) {
return useQuery({
queryKey: queryInsightsKeys.queries(projectRef, startTime, endTime),
queryFn: async () => {
if (!projectRef) throw new Error('Project ref is required')
const { result } = await executeSql({
projectRef,
sql: getQueriesSql(startTime, endTime),
})
return result as QueryInsightsQuery[]
},
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
...options,
})
}
export function useQueryInsightsQueriesWithErrors(
projectRef: string | undefined,
startTime: string,
endTime: string,
options?: UseQueryOptions<QueryInsightsQuery[]>
) {
return useQuery({
queryKey: queryInsightsKeys.queriesWithErrors(projectRef, startTime, endTime),
queryFn: async () => {
if (!projectRef) throw new Error('Project ref is required')
const { result } = await executeSql({
projectRef,
sql: getQueriesWithErrorsSql(startTime, endTime),
})
return result as QueryInsightsQuery[]
},
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
...options,
})
}
// Hook to pre-fetch all metrics data for the current time range
export function usePreFetchQueryInsightsData(
projectRef: string | undefined,
startTime: string,
endTime: string
) {
const queryClient = useQueryClient()
useEffect(() => {
if (!projectRef) return
// Pre-fetch all metric types
const metricTypes: Array<{ id: string; label: string }> = [
{ id: 'query_latency', label: 'Query latency' },
{ id: 'rows_read', label: 'Rows read' },
{ id: 'calls', label: 'Calls' },
{ id: 'cache_hits', label: 'Cache hits' },
{ id: 'issues', label: 'Issues' },
]
// Pre-fetch metrics data
metricTypes.forEach((metric) => {
queryClient.prefetchQuery({
queryKey: queryInsightsKeys.metrics(projectRef, metric.id, startTime, endTime),
queryFn: async () => {
const sql = getMetricsSql(metric.id, startTime, endTime)
console.log(`[usePreFetchQueryInsightsData] Executing SQL for ${metric.id}:`, sql)
const { result } = await executeSql({
projectRef,
sql,
})
console.log(`[usePreFetchQueryInsightsData] Result for ${metric.id}:`, result)
return result as QueryInsightsMetric[]
},
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
})
})
// Pre-fetch queries data
queryClient.prefetchQuery({
queryKey: queryInsightsKeys.queries(projectRef, startTime, endTime),
queryFn: async () => {
const { result } = await executeSql({
projectRef,
sql: getQueriesSql(startTime, endTime),
})
return result as QueryInsightsQuery[]
},
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
})
// Pre-fetch queries with errors data
queryClient.prefetchQuery({
queryKey: queryInsightsKeys.queriesWithErrors(projectRef, startTime, endTime),
queryFn: async () => {
const { result } = await executeSql({
projectRef,
sql: getQueriesWithErrorsSql(startTime, endTime),
})
return result as QueryInsightsQuery[]
},
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
})
}, [projectRef, startTime, endTime, queryClient])
}
// Hook for better cache management and manual refresh control
export function useQueryInsightsCacheManager(
projectRef: string | undefined,
startTime: string,
endTime: string
) {
const queryClient = useQueryClient()
const refreshAllData = useCallback(async () => {
if (!projectRef) return
const metricTypes = ['query_latency', 'rows_read', 'calls', 'cache_hits', 'issues']
// Invalidate and refetch all metrics
await Promise.all([
...metricTypes.map((metric) =>
queryClient.invalidateQueries({
queryKey: queryInsightsKeys.metrics(projectRef, metric, startTime, endTime),
})
),
queryClient.invalidateQueries({
queryKey: queryInsightsKeys.queries(projectRef, startTime, endTime),
}),
queryClient.invalidateQueries({
queryKey: queryInsightsKeys.queriesWithErrors(projectRef, startTime, endTime),
}),
])
}, [projectRef, startTime, endTime, queryClient])
const isAnyDataStale = useCallback(() => {
if (!projectRef) return false
const metricTypes = ['query_latency', 'rows_read', 'calls', 'cache_hits', 'issues']
return (
metricTypes.some((metric) => {
const query = queryClient.getQueryData(
queryInsightsKeys.metrics(projectRef, metric, startTime, endTime)
)
return !query
}) ||
!queryClient.getQueryData(queryInsightsKeys.queries(projectRef, startTime, endTime)) ||
!queryClient.getQueryData(queryInsightsKeys.queriesWithErrors(projectRef, startTime, endTime))
)
}, [projectRef, startTime, endTime, queryClient])
return {
refreshAllData,
isAnyDataStale,
}
}