mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
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:
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,
|
||||
|
||||
+8
-9
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -99,12 +99,6 @@ module.exports = config({
|
||||
transform: 'rotate(10deg) scale(1.5) translateY(2rem)',
|
||||
},
|
||||
},
|
||||
typewriter: {
|
||||
from: { width: '0' },
|
||||
},
|
||||
'blink-caret': {
|
||||
'50%': { borderColor: 'transparent' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user