From dfd5461ef99aa98c32ae2546d1d3de24984fe58d Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Wed, 4 Mar 2026 16:34:36 +0800 Subject: [PATCH] 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 --- apps/studio/components/grid/SupabaseGrid.tsx | 1 - .../footer/pagination/Pagination.tsx | 217 +++++++++--------- .../formatter/ReferenceRecordPeek.tsx | 1 - .../components/grid/components/grid/Grid.tsx | 1 - .../grid/components/header/Header.tsx | 2 - .../grid/components/header/HeaderNew.tsx | 2 - .../header/sort/SortPopoverPrimitive.tsx | 5 +- .../ForeignRowSelector/ForeignRowSelector.tsx | 17 +- .../TableEditorLayout/EntityListItem.tsx | 1 - .../database/foreign-key-constraints-query.ts | 21 +- apps/studio/data/database/keys.ts | 4 +- .../prefetchers/project.$ref.editor.$id.tsx | 12 +- .../data/read-replicas/replicas-query.ts | 45 +++- .../data/table-rows/table-rows-count-query.ts | 116 ++++------ .../data/table-rows/table-rows-query.ts | 18 +- apps/studio/data/table-rows/table-rows.sql.ts | 93 ++++++++ apps/studio/styles/main.scss | 12 + apps/studio/tailwind.config.js | 6 - 18 files changed, 344 insertions(+), 230 deletions(-) create mode 100644 apps/studio/data/table-rows/table-rows.sql.ts 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...

-
- )} +
+
+ + {isLoading ? ( +
- - {!isForeignTableSelected && ( -
-

- {`${countString} ${count === 0 || count > 1 ? `records` : 'record'}`}{' '} - {data.is_estimate ? '(estimated)' : ''} -

- - {data.is_estimate && ( - - -
+ + 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 && ( + + +
+ ) : null} ( - { 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' }, - }, }, }, },