Opt to use connection string of read replica if available to power the table editor (#42856)

## Context

Part of dashboard scalability project

Opting to use the connection string of the project's read replica (if
available) for read queries on the database.

Trialing with the Table Editor as a first pass - changes involved will
opt to use replica connection string for `useTableRowsQuery`,
`useTableRowsCountQuery`, and `useForeignKeyConstraintsQuery`

There's definitely optimizations to be done for deciding which replica
to use - but am starting off with a rather naive logic to prioritize
replicas in the same region as the project.

## Changes involved

- We're no longer passing `connectionString` as a param into the
affected hooks, the `connectionString` is derived from within those
hooks instead
- Change is feature flagged, so things should be status quo if flag is
off (use primary database's connection string)
- Added `useConnectionStringForReadOps` hook which returns the replica's
connection string if (Otherwise defaults to primary database connection
string)
  - Feature flag is on
  - Project has a replica available

## To test

- [ ] Verify that the table editor works as expected for a project that
has read replicas (There shouldn't be any change really)
- [ ] Also just double check that updating cells in the table editor
works as well (There's no change there, we're using the primary DB's
connection string for mutation ops)
- [ ] ^ Same thing for a project that doesn't have read replicas
- [ ] ^ Same thing for local / self-host
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-03-04 16:34:36 +08:00
1 parent 26dd8ca958
commit dfd5461ef9
18 files changed
+344 -230

No files matched your search

@@ -77,7 +77,6 @@ export const SupabaseGrid = ({
} = useTableRowsQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
tableId,
sorts,
filters,
@@ -9,7 +9,7 @@ import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query'
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { RoleImpersonationState } from 'lib/role-impersonation'
import { ArrowLeft, ArrowRight, HelpCircle, Loader2 } from 'lucide-react'
import { AlertCircle, ArrowLeft, ArrowRight, HelpCircle, Loader2 } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state'
import { useTableEditorStateSnapshot } from 'state/table-editor'
@@ -90,7 +90,6 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
} = useTableRowsCountQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
tableId: snap.table.id,
filters,
enforceExactCount: snap.enforceExactCount,
@@ -102,6 +101,8 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
}
)
const count = data?.count ?? 0
const hasCountData = count >= 0
const isEstimateCount = data?.is_estimate ?? false
const countString = data?.is_estimate ? formatEstimatedCount(count) : count.toLocaleString()
const maxPages = Math.ceil(count / tableEditorSnap.rowsPerPage)
const totalPages = count > 0 ? maxPages : 1
@@ -113,7 +114,6 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
const { data: rowsData, isPending: isLoadingRows } = useTableRowsQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
tableId: id,
sorts,
filters,
@@ -144,12 +144,10 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
}
const onNextPage = () => {
if (page < maxPages) {
if (snap.selectedRows.size >= 1) {
setIsConfirmNextModalOpen(true)
} else {
goToNextPage()
}
if (snap.selectedRows.size >= 1) {
setIsConfirmNextModalOpen(true)
} else {
goToNextPage()
}
}
@@ -176,6 +174,7 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
const onRowsPerPageChange = (value: string | number) => {
const rowsPerPage = Number(value)
tableEditorSnap.setRowsPerPage(isNaN(rowsPerPage) ? 100 : rowsPerPage)
snap.setPage(1)
}
// keep input value in-sync with actual page
@@ -183,20 +182,6 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
setValue(String(page))
}, [page])
useEffect(() => {
if (!isForeignTableSelected && page && page > totalPages) {
snap.setPage(totalPages)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isForeignTableSelected, page, totalPages])
useEffect(() => {
if (id !== undefined) {
snap.setEnforceExactCount(rowsCountEstimate !== null && rowsCountEstimate <= THRESHOLD_COUNT)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id])
useEffect(() => {
// If the count query encountered a timeout error with exact count
// turn off the exact count to rely on approximate
@@ -206,6 +191,7 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isError, snap.enforceExactCount, error?.code])
// [Joshen] One to revisit if we can consolidate this and the main return statement
if (isForeignTableSelected) {
return (
<div className="flex items-center gap-x-2">
@@ -251,102 +237,107 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
return (
<div className="flex items-center gap-x-4">
{isLoading && (
<div className="flex items-center gap-x-2">
<Loader2 size={12} className="animate-spin" />
<p className="text-xs text-foreground-light">Loading records count...</p>
</div>
)}
<div className="flex items-center gap-x-2">
<Button
aria-label="Previous page"
icon={<ArrowLeft />}
type="outline"
className="px-1.5"
disabled={page <= 1 || isLoading}
onClick={onPreviousPage}
/>
{isSuccess && (
<>
<div className="flex items-center gap-x-2">
<p className="text-xs text-foreground-light">Page</p>
<Input
className="w-12"
size="tiny"
min={1}
max={isSuccess ? maxPages : undefined}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => {
const parsedValue = Number(value)
if (
(e.code === 'Enter' || e.code === 'NumpadEnter') &&
!Number.isNaN(parsedValue) &&
parsedValue >= 1
) {
onPageChange(parsedValue)
}
}}
/>
{isSuccess && hasCountData && (
<p className="text-xs text-foreground-light">of {totalPages.toLocaleString()}</p>
)}
<Button
aria-label="Next page"
icon={<ArrowRight />}
type="outline"
className="px-1.5"
disabled={isLastPage}
onClick={onNextPage}
/>
<RowCountSelector onRowsPerPageChange={onRowsPerPageChange} />
</div>
{isLoading ? (
<Button type="text" className="w-7" icon={<Loader2 size={12} className="animate-spin" />} />
) : isError ? (
<Tooltip>
<TooltipTrigger asChild>
<Button
aria-label="Previous page"
icon={<ArrowLeft />}
type="outline"
className="px-1.5"
disabled={page <= 1 || isLoading}
onClick={onPreviousPage}
/>
<p className="text-xs text-foreground-light">Page</p>
<Input
className="w-12"
size="tiny"
min={1}
max={maxPages}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => {
const parsedValue = Number(value)
if (
(e.code === 'Enter' || e.code === 'NumpadEnter') &&
!Number.isNaN(parsedValue) &&
parsedValue >= 1 &&
parsedValue <= maxPages
) {
onPageChange(parsedValue)
}
}}
type="text"
className="w-7"
loading={isFetching}
icon={<AlertCircle />}
/>
<p className="text-xs text-foreground-light">of {totalPages.toLocaleString()}</p>
<Button
aria-label="Next page"
icon={<ArrowRight />}
type="outline"
className="px-1.5"
disabled={page >= maxPages || isLoading}
onClick={onNextPage}
/>
<RowCountSelector onRowsPerPageChange={onRowsPerPageChange} />
</div>
{!isForeignTableSelected && (
<div className="flex items-center gap-x-2">
<p className="text-xs text-foreground-light">
{`${countString} ${count === 0 || count > 1 ? `records` : 'record'}`}{' '}
{data.is_estimate ? '(estimated)' : ''}
</p>
{data.is_estimate && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="tiny"
type="text"
className="px-1.5"
loading={isFetching}
icon={<HelpCircle />}
onClick={() => {
// Show warning if either NOT a table entity, or table rows estimate is beyond threshold
if (rowsCountEstimate === null || count > THRESHOLD_COUNT) {
setIsConfirmFetchExactCountModalOpen(true)
} else snap.setEnforceExactCount(true)
}}
/>
</TooltipTrigger>
<TooltipContent side="top" className="w-72">
This is an estimated value as your table has more than{' '}
{THRESHOLD_COUNT.toLocaleString()} rows. <br />
<span className="text-brand">
Click to retrieve the exact count of the table.
</span>
</TooltipContent>
</Tooltip>
)}
</div>
</TooltipTrigger>
<TooltipContent side="top">Failed to retrieve count: {error?.message}</TooltipContent>
</Tooltip>
) : !isForeignTableSelected ? (
<div className="flex items-center gap-x-2">
{hasCountData && (
<p className="text-xs text-foreground-light">
{`${countString} ${count === 0 || count > 1 ? `records` : 'record'}`}{' '}
{data.is_estimate ? '(estimated)' : ''}
</p>
)}
</>
)}
{isError && (
<p className="text-sm text-foreground-light">
Error fetching records count. Please refresh the page.
</p>
)}
{data.is_estimate && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="tiny"
type="text"
className="w-7"
loading={isFetching}
icon={<HelpCircle />}
onClick={() => {
// Show warning if either NOT a table entity, or table rows estimate is beyond threshold
if (rowsCountEstimate === null || count === -1 || count > THRESHOLD_COUNT) {
setIsConfirmFetchExactCountModalOpen(true)
} else {
snap.setEnforceExactCount(true)
}
}}
/>
</TooltipTrigger>
<TooltipContent side="top" className="w-72">
{hasCountData
? `This is an estimated value as your table has more than ${THRESHOLD_COUNT.toLocaleString()} rows.`
: `Count not automatically loaded as your table has more than ${THRESHOLD_COUNT.toLocaleString()} rows.`}{' '}
<br />
<span className="text-brand">Click to retrieve the exact count of the table.</span>
</TooltipContent>
</Tooltip>
)}
</div>
) : null}
<ConfirmationModal
visible={isConfirmPreviousModalOpen}
@@ -39,7 +39,6 @@ export const ReferenceRecordPeek = ({ table, column, value }: ReferenceRecordPee
} = useTableRowsQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
tableId: table.id,
filters: [{ column, operator: '=', value }],
page: 1,
@@ -113,7 +113,6 @@ export const Grid = memo(
const { data } = useForeignKeyConstraintsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: table?.schema ?? undefined,
})
@@ -245,7 +245,6 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
const { data } = useTableRowsQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
tableId: snap.table.id,
sorts,
filters,
@@ -260,7 +259,6 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
const { data: countData } = useTableRowsCountQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
tableId: snap.table.id,
filters,
enforceExactCount: snap.enforceExactCount,
@@ -246,7 +246,6 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
const { data } = useTableRowsQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
tableId: snap.table.id,
sorts,
filters,
@@ -261,7 +260,6 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
const { data: countData } = useTableRowsCountQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
tableId: snap.table.id,
filters,
enforceExactCount: snap.enforceExactCount,
@@ -10,16 +10,16 @@ import { isEqual } from 'lodash'
import { ChevronDown, List } from 'lucide-react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
type RoleImpersonationState,
useRoleImpersonationStateSnapshot,
type RoleImpersonationState,
} from 'state/role-impersonation-state'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import {
Button,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverSeparator_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
@@ -72,7 +72,6 @@ export const SortPopoverPrimitive = ({
const { data: countData } = useTableRowsCountQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
tableId: snap.table.id,
filters,
enforceExactCount: snap.enforceExactCount,
@@ -1,29 +1,29 @@
import { Loader2, X } from 'lucide-react'
import { useEffect, useState } from 'react'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { keepPreviousData } from '@tanstack/react-query'
import { useParams } from 'common'
import { FilterPopoverPrimitive } from 'components/grid/components/header/filter/FilterPopoverPrimitive'
import { RefreshButton } from 'components/grid/components/header/RefreshButton'
import { SortPopoverPrimitive } from 'components/grid/components/header/sort/SortPopoverPrimitive'
import {
formatSortURLParams,
loadTableEditorStateFromLocalStorage,
saveTableEditorStateToLocalStorage,
sortsToUrlParams,
} from 'components/grid/SupabaseGrid.utils'
import { RefreshButton } from 'components/grid/components/header/RefreshButton'
import { FilterPopoverPrimitive } from 'components/grid/components/header/filter/FilterPopoverPrimitive'
import { SortPopoverPrimitive } from 'components/grid/components/header/sort/SortPopoverPrimitive'
import type { Filter, Sort } from 'components/grid/types'
import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Loader2, X } from 'lucide-react'
import { useEffect, useState } from 'react'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import {
RoleImpersonationState,
useRoleImpersonationStateSnapshot,
} from 'state/role-impersonation-state'
import { TableEditorTableStateContextProvider } from 'state/table-editor-table'
import { Button, SidePanel } from 'ui'
import { ForeignKey } from '../../ForeignKeySelector/ForeignKeySelector.types'
import { convertByteaToHex } from '../RowEditor.utils'
import Pagination from './Pagination'
@@ -110,7 +110,6 @@ export const ForeignRowSelector = ({
} = useTableRowsQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
tableId: table?.id,
sorts,
filters,
@@ -90,7 +90,6 @@ export const EntityListItem = ({
const { data: countData } = useTableRowsCountQuery(
{
projectRef,
connectionString: project?.connectionString,
tableId: entity.id,
filters,
enforceExactCount: false,
@@ -1,6 +1,8 @@
import { QueryClient, useQuery } from '@tanstack/react-query'
import { IS_PLATFORM } from 'common'
import { UseCustomQueryOptions } from 'types'
import { useConnectionStringForReadOps } from '../read-replicas/replicas-query'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { databaseKeys } from './keys'
@@ -127,23 +129,32 @@ export type ForeignKeyConstraintsData = Awaited<ReturnType<typeof getForeignKeyC
export type ForeignKeyConstraintsError = ExecuteSqlError
export const useForeignKeyConstraintsQuery = <TData = ForeignKeyConstraintsData>(
{ projectRef, connectionString, schema }: ForeignKeyConstraintsVariables,
{
projectRef,
connectionString: connectionStringOverride,
schema,
}: ForeignKeyConstraintsVariables,
{
enabled = true,
...options
}: UseCustomQueryOptions<ForeignKeyConstraintsData, ForeignKeyConstraintsError, TData> = {}
) =>
useQuery<ForeignKeyConstraintsData, ForeignKeyConstraintsError, TData>({
queryKey: databaseKeys.foreignKeyConstraints(projectRef, schema),
) => {
const { connectionString: connectionStringReadOps } = useConnectionStringForReadOps()
const connectionString = connectionStringOverride || connectionStringReadOps
return useQuery<ForeignKeyConstraintsData, ForeignKeyConstraintsError, TData>({
queryKey: databaseKeys.foreignKeyConstraints(projectRef, schema, { connectionString }),
queryFn: ({ signal }) =>
getForeignKeyConstraints({ projectRef, connectionString, schema }, signal),
enabled:
enabled &&
typeof projectRef !== 'undefined' &&
typeof schema !== 'undefined' &&
(!IS_PLATFORM || typeof connectionString !== 'undefined') &&
schema.length > 0,
...options,
})
}
export function prefetchForeignKeyConstraints(
client: QueryClient,
+2 -2
View File
@@ -27,8 +27,8 @@ export const databaseKeys = {
['projects', projectRef, 'index-advisor', { query }] as const,
tableConstraints: (projectRef: string | undefined, id?: number) =>
['projects', projectRef, 'table-constraints', id] as const,
foreignKeyConstraints: (projectRef: string | undefined, schema?: string) =>
['projects', projectRef, 'foreign-key-constraints', schema] as const,
foreignKeyConstraints: (projectRef: string | undefined, schema?: string, options = {}) =>
['projects', projectRef, 'foreign-key-constraints', schema, options] as const,
databaseSize: (projectRef: string | undefined) =>
['projects', projectRef, 'database-size'] as const,
maxConnections: (projectRef: string | undefined) =>
@@ -1,7 +1,4 @@
import { QueryClient, useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'next/router'
import { PropsWithChildren, useCallback } from 'react'
import {
formatFilterURLParams,
formatSortURLParams,
@@ -13,8 +10,12 @@ import { prefetchTableEditor } from 'data/table-editor/table-editor-query'
import { prefetchTableRows } from 'data/table-rows/table-rows-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { RoleImpersonationState } from 'lib/role-impersonation'
import { useRouter } from 'next/router'
import { PropsWithChildren, useCallback } from 'react'
import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state'
import { TABLE_EDITOR_DEFAULT_ROWS_PER_PAGE } from 'state/table-editor'
import { useConnectionStringForReadOps } from '../read-replicas/replicas-query'
import PrefetchableLink, { PrefetchableLinkProps } from './PrefetchableLink'
interface PrefetchEditorTablePageArgs {
@@ -65,6 +66,7 @@ export function usePrefetchEditorTablePage() {
const router = useRouter()
const queryClient = useQueryClient()
const { data: project } = useSelectedProjectQuery()
const { connectionString } = useConnectionStringForReadOps()
const roleImpersonationState = useRoleImpersonationStateSnapshot()
return useCallback(
@@ -79,7 +81,7 @@ export function usePrefetchEditorTablePage() {
prefetchEditorTablePage({
queryClient,
projectRef: project.ref,
connectionString: project.connectionString,
connectionString,
id,
sorts,
filters,
@@ -88,7 +90,7 @@ export function usePrefetchEditorTablePage() {
// eat prefetching errors as they are not critical
})
},
[project, queryClient, roleImpersonationState, router]
[connectionString, project, queryClient, roleImpersonationState, router]
)
}
@@ -1,9 +1,11 @@
import { useQuery } from '@tanstack/react-query'
import { useFeatureFlags, useFlag } from 'common'
import type { components } from 'data/api'
import { get, handleError } from 'data/fetchers'
import type { ResponseError, UseCustomQueryOptions } from 'types'
import { replicaKeys } from './keys'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
export const MAX_REPLICAS_BELOW_XL = 2
export const MAX_REPLICAS_ABOVE_XL = 5
@@ -55,3 +57,44 @@ export const usePrimaryDatabase = ({ projectRef }: { projectRef?: string }) => {
const primaryDatabase = databases.find((x) => x.identifier === projectRef)
return { database: primaryDatabase, error, isLoading, isError, isSuccess }
}
/**
* [Joshen] JFYI this logic here can and should be optimized
* Returns the connection string of read replica if available, otherwise default to project's (primary)
* If multiple read replicas available, (naively) prioritise replica in the same region as primary
* to minimize any latency. Otherwise just use the first available read replica
*/
export const useConnectionStringForReadOps = (): {
type: 'replica' | 'primary' | undefined
identifier: string | undefined
connectionString: string | undefined | null
} => {
const { hasLoaded: flagsLoaded } = useFeatureFlags()
const defaultToReadReplicaConnectionString = useFlag('defaultToReadReplicaConnectionString')
const { data: project, isSuccess: isSuccessProject } = useSelectedProjectQuery()
const { data: databases = [], isLoading: isLoadingDatabases } = useReadReplicasQuery({
projectRef: project?.ref,
})
const readReplicas = databases.filter(
(x) => x.identifier !== project?.ref && x.status === 'ACTIVE_HEALTHY'
)
const readReplica = readReplicas.some((x) => x.region === project?.region)
? readReplicas.find((x) => x.region === project?.region)
: readReplicas[0]
if (!isSuccessProject || isLoadingDatabases || !flagsLoaded) {
return { connectionString: undefined, type: undefined, identifier: undefined }
}
if (!defaultToReadReplicaConnectionString) {
return { type: 'primary', identifier: project.ref, connectionString: project.connectionString }
}
return {
type: !!readReplica ? 'replica' : 'primary',
identifier: !!readReplica ? readReplica.identifier : project.ref,
connectionString: !!readReplica ? readReplica.connectionString : project.connectionString,
}
}
@@ -1,86 +1,23 @@
import { Query } from '@supabase/pg-meta/src/query'
import {
COUNT_ESTIMATE_SQL,
THRESHOLD_COUNT,
} from '@supabase/pg-meta/src/sql/studio/get-count-estimate'
import { QueryClient, useQuery, useQueryClient } from '@tanstack/react-query'
import { IS_PLATFORM } from 'common'
import { parseSupaTable } from 'components/grid/SupabaseGrid.utils'
import type { Filter, SupaTable } from 'components/grid/types'
import { prefetchTableEditor } from 'data/table-editor/table-editor-query'
import { RoleImpersonationState, wrapWithRoleImpersonation } from 'lib/role-impersonation'
import { isRoleImpersonationEnabled } from 'state/role-impersonation-state'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { tableRowKeys } from './keys'
import { formatFilterValue } from './utils'
import { UseCustomQueryOptions } from 'types'
type GetTableRowsCountArgs = {
import { useConnectionStringForReadOps } from '../read-replicas/replicas-query'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { tableRowKeys } from './keys'
import { getTableRowsCountSql } from './table-rows.sql'
export type GetTableRowsCountArgs = {
table?: SupaTable
filters?: Filter[]
enforceExactCount?: boolean
}
export const getTableRowsCountSql = ({
table,
filters = [],
enforceExactCount = false,
}: GetTableRowsCountArgs) => {
if (!table) return ``
if (enforceExactCount) {
const query = new Query()
let queryChains = query.from(table.name, table.schema ?? undefined).count()
filters
.filter((x) => x.value && x.value !== '')
.forEach((x) => {
const value = formatFilterValue(table, x)
queryChains = queryChains.filter(x.column, x.operator, value)
})
return `select (${queryChains.toSql().slice(0, -1)}), false as is_estimate;`
} else {
const selectQuery = new Query()
let selectQueryChains = selectQuery.from(table.name, table.schema ?? undefined).select('*')
filters
.filter((x) => x.value && x.value != '')
.forEach((x) => {
const value = formatFilterValue(table, x)
selectQueryChains = selectQueryChains.filter(x.column, x.operator, value)
})
const selectBaseSql = selectQueryChains.toSql()
const countQuery = new Query()
let countQueryChains = countQuery.from(table.name, table.schema ?? undefined).count()
filters
.filter((x) => x.value && x.value != '')
.forEach((x) => {
const value = formatFilterValue(table, x)
countQueryChains = countQueryChains.filter(x.column, x.operator, value)
})
const countBaseSql = countQueryChains.toSql().slice(0, -1)
const sql = `
${COUNT_ESTIMATE_SQL}
with approximation as (
select reltuples as estimate
from pg_class
where oid = ${table.id}
)
select
case
when estimate = -1 then (select pg_temp.count_estimate('${selectBaseSql.replaceAll("'", "''")}'))
when estimate > ${THRESHOLD_COUNT} then ${filters.length > 0 ? `pg_temp.count_estimate('${selectBaseSql.replaceAll("'", "''")}')` : 'estimate'}
else (${countBaseSql})
end as count,
estimate = -1 or estimate > ${THRESHOLD_COUNT} as is_estimate
from approximation;
`.trim()
return sql
}
}
export type TableRowsCount = {
count?: number
is_estimate?: boolean
@@ -106,7 +43,8 @@ export async function getTableRowsCount(
filters,
roleImpersonationState,
enforceExactCount,
}: TableRowsCountVariables,
isUsingReadReplica = false,
}: TableRowsCountVariables & { isUsingReadReplica?: boolean },
signal?: AbortSignal
) {
const entity = await prefetchTableEditor(queryClient, {
@@ -121,7 +59,7 @@ export async function getTableRowsCount(
const table = parseSupaTable(entity)
const sql = wrapWithRoleImpersonation(
getTableRowsCountSql({ table, filters, enforceExactCount }),
getTableRowsCountSql({ table, filters, enforceExactCount, isUsingReadReplica }),
roleImpersonationState
)
const { result } = await executeSql(
@@ -142,18 +80,44 @@ export async function getTableRowsCount(
}
export const useTableRowsCountQuery = <TData = TableRowsCountData>(
{ projectRef, connectionString, tableId, ...args }: Omit<TableRowsCountVariables, 'queryClient'>,
{
projectRef,
connectionString: connectionStringOverride,
tableId,
...args
}: Omit<TableRowsCountVariables, 'queryClient'>,
{
enabled = true,
...options
}: UseCustomQueryOptions<TableRowsCountData, TableRowsCountError, TData> = {}
) => {
const queryClient = useQueryClient()
const { connectionString: connectionStringReadOps, type } = useConnectionStringForReadOps()
const connectionString = connectionStringOverride || connectionStringReadOps
return useQuery<TableRowsCountData, TableRowsCountError, TData>({
queryKey: tableRowKeys.tableRowsCount(projectRef, { table: { id: tableId }, ...args }),
queryKey: tableRowKeys.tableRowsCount(projectRef, {
table: { id: tableId },
connectionString,
...args,
}),
queryFn: ({ signal }) =>
getTableRowsCount({ queryClient, projectRef, connectionString, tableId, ...args }, signal),
enabled: enabled && typeof projectRef !== 'undefined' && typeof tableId !== 'undefined',
getTableRowsCount(
{
queryClient,
projectRef,
connectionString,
tableId,
isUsingReadReplica: type === 'replica',
...args,
},
signal
),
enabled:
enabled &&
typeof projectRef !== 'undefined' &&
typeof tableId !== 'undefined' &&
(!IS_PLATFORM || typeof connectionString !== 'undefined'),
...options,
})
}
@@ -16,6 +16,7 @@ import { isRoleImpersonationEnabled } from 'state/role-impersonation-state'
import { ResponseError, UseCustomQueryOptions } from 'types'
import { handleError } from '../fetchers'
import { useConnectionStringForReadOps } from '../read-replicas/replicas-query'
import { executeSql, ExecuteSqlError } from '../sql/execute-sql-query'
import { tableRowKeys } from './keys'
import { formatFilterValue } from './utils'
@@ -390,10 +391,17 @@ async function getTableRows(
}
export const useTableRowsQuery = <TData = TableRowsData>(
{ projectRef, connectionString, tableId, ...args }: Omit<TableRowsVariables, 'queryClient'>,
{
projectRef,
connectionString: connectionStringOverride,
tableId,
...args
}: Omit<TableRowsVariables, 'queryClient'>,
{ enabled = true, ...options }: UseCustomQueryOptions<TableRowsData, TableRowsError, TData> = {}
) => {
const queryClient = useQueryClient()
const { connectionString: connectionStringReadOps } = useConnectionStringForReadOps()
const connectionString = connectionStringOverride || connectionStringReadOps
// [Joshen] Exclude preflightCheck from query key
const { preflightCheck, ...othersArgs } = args
@@ -401,11 +409,16 @@ export const useTableRowsQuery = <TData = TableRowsData>(
return useQuery<TableRowsData, TableRowsError, TData>({
queryKey: tableRowKeys.tableRows(projectRef, {
table: { id: tableId },
connectionString,
...othersArgs,
}),
queryFn: ({ signal }) =>
getTableRows({ queryClient, projectRef, connectionString, tableId, ...args }, signal),
enabled: enabled && typeof projectRef !== 'undefined' && typeof tableId !== 'undefined',
enabled:
enabled &&
typeof projectRef !== 'undefined' &&
typeof tableId !== 'undefined' &&
(!IS_PLATFORM || typeof connectionString !== 'undefined'),
...options,
})
}
@@ -417,6 +430,7 @@ export function prefetchTableRows(
return client.fetchQuery({
queryKey: tableRowKeys.tableRows(projectRef, {
table: { id: tableId },
connectionString,
...args,
}),
queryFn: ({ signal }) =>
@@ -0,0 +1,93 @@
import { Query } from '@supabase/pg-meta/src/query'
import {
COUNT_ESTIMATE_SQL,
THRESHOLD_COUNT,
} from '@supabase/pg-meta/src/sql/studio/get-count-estimate'
import { GetTableRowsCountArgs } from './table-rows-count-query'
import { formatFilterValue } from './utils'
/**
* [Joshen] Initially check reltuples from pg_class for an estimate of row count on the table
* - If reltuples = -1, table never been analyzed, assume small table -> return exact count
* - If reltuples exceeds threshold, return estimate count
* - Else return exact count
*/
export const getTableRowsCountSql = ({
table,
filters = [],
enforceExactCount = false,
isUsingReadReplica = false,
}: GetTableRowsCountArgs & { isUsingReadReplica?: boolean }) => {
if (!table) return ``
if (enforceExactCount) {
const query = new Query()
let queryChains = query.from(table.name, table.schema ?? undefined).count()
filters
.filter((x) => x.value && x.value !== '')
.forEach((x) => {
const value = formatFilterValue(table, x)
queryChains = queryChains.filter(x.column, x.operator, value)
})
return `select (${queryChains.toSql().slice(0, -1)}), false as is_estimate;`
} else {
const selectQuery = new Query()
let selectQueryChains = selectQuery.from(table.name, table.schema ?? undefined).select('*')
filters
.filter((x) => x.value && x.value != '')
.forEach((x) => {
const value = formatFilterValue(table, x)
selectQueryChains = selectQueryChains.filter(x.column, x.operator, value)
})
const selectBaseSql = selectQueryChains.toSql()
const countQuery = new Query()
let countQueryChains = countQuery.from(table.name, table.schema ?? undefined).count()
filters
.filter((x) => x.value && x.value != '')
.forEach((x) => {
const value = formatFilterValue(table, x)
countQueryChains = countQueryChains.filter(x.column, x.operator, value)
})
const countBaseSql = countQueryChains.toSql().slice(0, -1)
if (isUsingReadReplica) {
const sql = `
with approximation as (
select reltuples as estimate
from pg_class
where oid = ${table.id}
)
select
case
when estimate > ${THRESHOLD_COUNT} then (select -1)
else (${countBaseSql})
end as count,
estimate > ${THRESHOLD_COUNT} as is_estimate
from approximation;
`.trim()
return sql
} else {
const sql = `
${COUNT_ESTIMATE_SQL}
with approximation as (
select reltuples as estimate
from pg_class
where oid = ${table.id}
)
select
case
when estimate > ${THRESHOLD_COUNT} then ${filters.length > 0 ? `pg_temp.count_estimate('${selectBaseSql.replaceAll("'", "''")}')` : 'estimate'}
else (${countBaseSql})
end as count,
estimate > ${THRESHOLD_COUNT} as is_estimate
from approximation;
`.trim()
return sql
}
}
}
+12
View File
@@ -282,3 +282,15 @@ div[data-radix-portal]:not(.portal--toast) {
height: 6px !important;
@apply border-strong dark:border-r-2 dark:border-b-2 dark:border-overlay #{!important};
}
@keyframes typewriter {
from {
width: 0;
}
}
@keyframes blink-caret {
50% {
border-color: transparent;
}
}
-6
View File
@@ -99,12 +99,6 @@ module.exports = config({
transform: 'rotate(10deg) scale(1.5) translateY(2rem)',
},
},
typewriter: {
from: { width: '0' },
},
'blink-caret': {
'50%': { borderColor: 'transparent' },
},
},
},
},