diff --git a/apps/studio/components/grid/SupabaseGrid.tsx b/apps/studio/components/grid/SupabaseGrid.tsx
index be97e1cefa5..04990fd8d49 100644
--- a/apps/studio/components/grid/SupabaseGrid.tsx
+++ b/apps/studio/components/grid/SupabaseGrid.tsx
@@ -77,7 +77,6 @@ export const SupabaseGrid = ({
} = useTableRowsQuery(
{
projectRef: project?.ref,
- connectionString: project?.connectionString,
tableId,
sorts,
filters,
diff --git a/apps/studio/components/grid/components/footer/pagination/Pagination.tsx b/apps/studio/components/grid/components/footer/pagination/Pagination.tsx
index 50d3b92f8b9..4a4b2914742 100644
--- a/apps/studio/components/grid/components/footer/pagination/Pagination.tsx
+++ b/apps/studio/components/grid/components/footer/pagination/Pagination.tsx
@@ -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 (
@@ -251,102 +237,107 @@ export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) =
return (
- {isLoading && (
-
-
-
Loading records count...
-
- )}
+
+
}
+ type="outline"
+ className="px-1.5"
+ disabled={page <= 1 || isLoading}
+ onClick={onPreviousPage}
+ />
- {isSuccess && (
- <>
-
+
Page
+
+
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 && (
+
of {totalPages.toLocaleString()}
+ )}
+
+
}
+ type="outline"
+ className="px-1.5"
+ disabled={isLastPage}
+ onClick={onNextPage}
+ />
+
+
+
+
+ {isLoading ? (
+
} />
+ ) : isError ? (
+
+
}
- type="outline"
- className="px-1.5"
- disabled={page <= 1 || isLoading}
- onClick={onPreviousPage}
- />
- Page
- 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={ }
/>
-
- of {totalPages.toLocaleString()}
-
- }
- type="outline"
- className="px-1.5"
- disabled={page >= maxPages || isLoading}
- onClick={onNextPage}
- />
-
-
-
-
- {!isForeignTableSelected && (
-
-
- {`${countString} ${count === 0 || count > 1 ? `records` : 'record'}`}{' '}
- {data.is_estimate ? '(estimated)' : ''}
-
-
- {data.is_estimate && (
-
-
- }
- 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)
- }}
- />
-
-
- This is an estimated value as your table has more than{' '}
- {THRESHOLD_COUNT.toLocaleString()} rows.
-
- Click to retrieve the exact count of the table.
-
-
-
- )}
-
+
+
Failed to retrieve count: {error?.message}
+
+ ) : !isForeignTableSelected ? (
+
+ {hasCountData && (
+
+ {`${countString} ${count === 0 || count > 1 ? `records` : 'record'}`}{' '}
+ {data.is_estimate ? '(estimated)' : ''}
+
)}
- >
- )}
- {isError && (
-
- Error fetching records count. Please refresh the page.
-
- )}
+ {data.is_estimate && (
+
+
+ }
+ 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)
+ }
+ }}
+ />
+
+
+ {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.`}{' '}
+
+ Click to retrieve the exact count of the table.
+
+
+ )}
+
+ ) : null}
{
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,
diff --git a/apps/studio/components/grid/components/header/HeaderNew.tsx b/apps/studio/components/grid/components/header/HeaderNew.tsx
index 3259451354d..9d03a76d271 100644
--- a/apps/studio/components/grid/components/header/HeaderNew.tsx
+++ b/apps/studio/components/grid/components/header/HeaderNew.tsx
@@ -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,
diff --git a/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx b/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx
index bbb8df4e78b..ee26db7e751 100644
--- a/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx
+++ b/apps/studio/components/grid/components/header/sort/SortPopoverPrimitive.tsx
@@ -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,
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx
index d03ce03b372..bdc3d5f8b2b 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/ForeignRowSelector.tsx
@@ -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,
diff --git a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx
index 1e33bfeb284..6ced4961bb4 100644
--- a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx
+++ b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx
@@ -90,7 +90,6 @@ export const EntityListItem = ({
const { data: countData } = useTableRowsCountQuery(
{
projectRef,
- connectionString: project?.connectionString,
tableId: entity.id,
filters,
enforceExactCount: false,
diff --git a/apps/studio/data/database/foreign-key-constraints-query.ts b/apps/studio/data/database/foreign-key-constraints-query.ts
index 797d09ffd20..5b6782a51b9 100644
--- a/apps/studio/data/database/foreign-key-constraints-query.ts
+++ b/apps/studio/data/database/foreign-key-constraints-query.ts
@@ -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(
- { projectRef, connectionString, schema }: ForeignKeyConstraintsVariables,
+ {
+ projectRef,
+ connectionString: connectionStringOverride,
+ schema,
+ }: ForeignKeyConstraintsVariables,
{
enabled = true,
...options
}: UseCustomQueryOptions = {}
-) =>
- useQuery({
- queryKey: databaseKeys.foreignKeyConstraints(projectRef, schema),
+) => {
+ const { connectionString: connectionStringReadOps } = useConnectionStringForReadOps()
+ const connectionString = connectionStringOverride || connectionStringReadOps
+
+ return useQuery({
+ 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,
diff --git a/apps/studio/data/database/keys.ts b/apps/studio/data/database/keys.ts
index 941609fe0f0..e0e1b9bbc53 100644
--- a/apps/studio/data/database/keys.ts
+++ b/apps/studio/data/database/keys.ts
@@ -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) =>
diff --git a/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx b/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx
index 36e8cb25162..254098c9029 100644
--- a/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx
+++ b/apps/studio/data/prefetchers/project.$ref.editor.$id.tsx
@@ -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]
)
}
diff --git a/apps/studio/data/read-replicas/replicas-query.ts b/apps/studio/data/read-replicas/replicas-query.ts
index 30214522541..e4dc14ba655 100644
--- a/apps/studio/data/read-replicas/replicas-query.ts
+++ b/apps/studio/data/read-replicas/replicas-query.ts
@@ -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,
+ }
+}
diff --git a/apps/studio/data/table-rows/table-rows-count-query.ts b/apps/studio/data/table-rows/table-rows-count-query.ts
index 4016f7949b5..481787c29bc 100644
--- a/apps/studio/data/table-rows/table-rows-count-query.ts
+++ b/apps/studio/data/table-rows/table-rows-count-query.ts
@@ -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 = (
- { projectRef, connectionString, tableId, ...args }: Omit,
+ {
+ projectRef,
+ connectionString: connectionStringOverride,
+ tableId,
+ ...args
+ }: Omit,
{
enabled = true,
...options
}: UseCustomQueryOptions = {}
) => {
const queryClient = useQueryClient()
+ const { connectionString: connectionStringReadOps, type } = useConnectionStringForReadOps()
+ const connectionString = connectionStringOverride || connectionStringReadOps
+
return useQuery({
- 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,
})
}
diff --git a/apps/studio/data/table-rows/table-rows-query.ts b/apps/studio/data/table-rows/table-rows-query.ts
index 12e72673096..2b76076672f 100644
--- a/apps/studio/data/table-rows/table-rows-query.ts
+++ b/apps/studio/data/table-rows/table-rows-query.ts
@@ -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 = (
- { projectRef, connectionString, tableId, ...args }: Omit,
+ {
+ projectRef,
+ connectionString: connectionStringOverride,
+ tableId,
+ ...args
+ }: Omit,
{ enabled = true, ...options }: UseCustomQueryOptions = {}
) => {
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 = (
return useQuery({
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 }) =>
diff --git a/apps/studio/data/table-rows/table-rows.sql.ts b/apps/studio/data/table-rows/table-rows.sql.ts
new file mode 100644
index 00000000000..5c5d0947bc5
--- /dev/null
+++ b/apps/studio/data/table-rows/table-rows.sql.ts
@@ -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
+ }
+ }
+}
diff --git a/apps/studio/styles/main.scss b/apps/studio/styles/main.scss
index 76473f85a60..7229a55e252 100644
--- a/apps/studio/styles/main.scss
+++ b/apps/studio/styles/main.scss
@@ -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;
+ }
+}
diff --git a/apps/studio/tailwind.config.js b/apps/studio/tailwind.config.js
index ace71e440be..b5fdcf67abc 100644
--- a/apps/studio/tailwind.config.js
+++ b/apps/studio/tailwind.config.js
@@ -99,12 +99,6 @@ module.exports = config({
transform: 'rotate(10deg) scale(1.5) translateY(2rem)',
},
},
- typewriter: {
- from: { width: '0' },
- },
- 'blink-caret': {
- '50%': { borderColor: 'transparent' },
- },
},
},
},