Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import type { WithSchema } from '../../../utils/types'
import type { InformationSchema } from './information'
import type { System } from './system'

export type Database = WithSchema<InformationSchema, 'information_schema'> & WithSchema<System, 'system'>
export type Database = WithSchema<InformationSchema, 'information_schema'>
& WithSchema<System, 'system'>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
export interface System {
columns: Columns
parts: Parts
}

/**
Expand All @@ -16,3 +17,14 @@ interface Columns {
name: string
is_in_primary_key: number
}

/**
* @name parts
* @type table
*/
interface Parts {
database: string
table: string
rows: number
active: number
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface PgClass {
relname: string
relnamespace: number
relkind: string
reltuples: number
}

/**
Expand Down
8 changes: 6 additions & 2 deletions apps/desktop/src/entities/connection/queries/total.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export function connectionTableTotalQuery({
connection: typeof connections.$inferSelect
table: string
schema: string
query: { filters: ActiveFilter[] }
query: {
filters: ActiveFilter[]
exact: boolean
}
}) {
return queryOptions({
queryKey: [
Expand All @@ -25,9 +28,10 @@ export function connectionTableTotalQuery({
'total',
{
filters: query.filters,
exact: query.exact,
},
],
queryFn: () => totalQuery(connection, { schema, table, filters: query.filters }),
queryFn: () => totalQuery(connection, { schema, table, filters: query.filters, exact: query.exact }),
throwOnError: false,
})
}
Expand Down
82 changes: 72 additions & 10 deletions apps/desktop/src/entities/connection/sql/total.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,120 @@
import type { ActiveFilter } from '@conar/shared/filters'
import { type } from 'arktype'
import { sql } from 'kysely'
import { createQuery } from '../query'
import { buildWhere } from './rows'

export const totalQuery = createQuery({
type: type('string | number | bigint | undefined').pipe(v => v !== undefined ? Number(v) : undefined),
type: type({
count: 'number',
isEstimated: 'boolean',
}),
query: ({
schema,
table,
filters,
}: { schema: string, table: string, filters?: ActiveFilter[] }) => ({
exact,
}: {
schema: string
table: string
filters?: ActiveFilter[]
exact?: boolean
}) => ({
postgres: async (db) => {
if (!exact && !filters?.length) {
const estimate = await db
.withSchema('pg_catalog')
.selectFrom('pg_catalog.pg_class')
.innerJoin('pg_catalog.pg_namespace', 'pg_catalog.pg_namespace.oid', 'pg_catalog.pg_class.relnamespace')
.select('pg_catalog.pg_class.reltuples as count')
.where('pg_catalog.pg_namespace.nspname', '=', schema)
.where('pg_catalog.pg_class.relname', '=', table)
.executeTakeFirst()

if (estimate && estimate.count >= 0) {
return {

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In PostgreSQL, reltuples can be -1 for tables that have never been analyzed. While the check estimate && estimate.count >= 0 correctly handles negative values, it would be clearer to add an explicit null check as well: estimate && estimate.count != null && estimate.count >= 0 for defensive programming, especially since the type system might not catch all edge cases.

Copilot uses AI. Check for mistakes.
count: Math.round(estimate.count),
isEstimated: true,
}
}
}

const query = await db
.withSchema(schema)
.withTables<{ [table]: Record<string, unknown> }>()
.selectFrom(table)
.select(db.fn.countAll().as('total'))
.$if(filters !== undefined, qb => qb.where(eb => buildWhere(eb, filters!)))
.execute()
.executeTakeFirst()

return query[0]?.total
return { count: Number(query?.total ?? 0), isEstimated: false }
},
mysql: async (db) => {
if (!exact && !filters?.length) {
const estimate = await db
.withSchema('information_schema')
.selectFrom('information_schema.TABLES')
.select('TABLE_ROWS as count')
.where('TABLE_SCHEMA', '=', schema)
.where('TABLE_NAME', '=', table)
.executeTakeFirst()

if (estimate && estimate.count >= 0) {
Comment thread
letstri marked this conversation as resolved.
Outdated
return { count: estimate.count, isEstimated: true }
Comment thread
letstri marked this conversation as resolved.
}
}

const query = await db
.withSchema(schema)
.withTables<{ [table]: Record<string, unknown> }>()
.selectFrom(table)
.select(db.fn.countAll().as('total'))
.$if(filters !== undefined, qb => qb.where(eb => buildWhere(eb, filters!)))
.execute()
.executeTakeFirst()

return query[0]?.total
return { count: Number(query?.total ?? 0), isEstimated: false }
},

mssql: async (db) => {
const query = await db
.withSchema(schema)
.withTables<{ [table]: Record<string, unknown> }>()
.selectFrom(table)
.select(db.fn.countAll().as('total'))
.$if(filters !== undefined, qb => qb.where(eb => buildWhere(eb, filters!)))
.execute()
.executeTakeFirst()

return query[0]?.total
return {
count: Number(query?.total ?? 0),
isEstimated: false,
}
},

clickhouse: async (db) => {
if (!exact && !filters?.length) {
const estimate = await db
.withSchema('system')
.selectFrom('system.parts')
.select(db.fn.sum(sql.ref('rows')).as('count'))
.where('database', '=', schema)
.where('table', '=', table)
.where('active', '=', 1)
.executeTakeFirst()

if (estimate && Number(estimate.count) >= 0) {
Comment thread
letstri marked this conversation as resolved.
return { count: Number(estimate.count), isEstimated: true }
}
}

const query = await db
.withSchema(schema)
.withTables<{ [table]: Record<string, unknown> }>()
.selectFrom(table)
.select(db.fn.countAll().as('total'))
.$if(filters !== undefined, qb => qb.where(eb => buildWhere(eb, filters!)))
.execute()
.executeTakeFirst()

return query[0]?.total
return { count: Number(query?.total ?? 0), isEstimated: false }
},
}),
})
2 changes: 1 addition & 1 deletion apps/desktop/src/entities/connection/utils/fetching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export async function prefetchConnectionTableCore({ connection, schema, table, q
}) {
await Promise.all([
queryClient.prefetchInfiniteQuery(connectionRowsQuery({ connection, table, schema, query })),
queryClient.prefetchQuery(connectionTableTotalQuery({ connection, table, schema, query })),
queryClient.prefetchQuery(connectionTableTotalQuery({ connection, table, schema, query: { filters: query.filters, exact: false } })),
queryClient.prefetchQuery(connectionTableColumnsQuery({ connection, table, schema })),
])
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function HeaderActionsDelete({ table, schema, connection }: { table: stri
onSuccess: () => {
toast.success(`${selected.length} row${selected.length === 1 ? '' : 's'} successfully deleted`)
queryClient.invalidateQueries(connectionRowsQuery({ connection, table, schema, query: { filters: store.state.filters, orderBy: store.state.orderBy } }))
queryClient.invalidateQueries(connectionTableTotalQuery({ connection, table, schema, query: { filters: store.state.filters } }))
queryClient.invalidateQueries(connectionTableTotalQuery({ connection, table, schema, query: { filters: store.state.filters, exact: store.state.exact } }))
store.setState(state => ({
...state,
selected: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ import { HeaderActionsOrder } from './header-actions-order'
export function HeaderActions({ table, schema }: { table: string, schema: string }) {
const { connection } = Route.useLoaderData()
const store = usePageStoreContext()
const [filters, orderBy] = useStore(store, state => [state.filters, state.orderBy])
const [filters, orderBy, exact] = useStore(store, state => [state.filters, state.orderBy, state.exact])
const { isFetching, dataUpdatedAt, refetch, data: rows, isPending } = useInfiniteQuery(
connectionRowsQuery({ connection, table, schema, query: { filters, orderBy } }),
)

async function handleRefresh() {
refetch()
queryClient.invalidateQueries(connectionTableColumnsQuery({ connection, table, schema }))
queryClient.invalidateQueries(connectionTableTotalQuery({ connection, table, schema, query: { filters } }))
queryClient.invalidateQueries(connectionTableTotalQuery({ connection, table, schema, query: { filters, exact } }))
queryClient.invalidateQueries(connectionConstraintsQuery({ connection }))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Separator } from '@conar/ui/components/separator'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@conar/ui/components/tooltip'
import { cn } from '@conar/ui/lib/utils'
import NumberFlow from '@number-flow/react'
import { useStore } from '@tanstack/react-store'
import { useState } from 'react'
import { useConnectionTableTotal } from '~/entities/connection/queries'
import { Route } from '../..'
import { useTableColumns } from '../../-queries/use-columns-query'
Expand All @@ -13,9 +16,11 @@ export function Header({ table, schema }: { table: string, schema: string }) {
const columns = useTableColumns({ connection, table, schema })
const store = usePageStoreContext()
const filters = useStore(store, state => state.filters)
const { data: total } = useConnectionTableTotal({ connection, table, schema, query: { filters } })
const [exact, setExact] = useState(false)
const { data: total, isLoading } = useConnectionTableTotal({ connection, table, schema, query: { filters, exact } })

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exact state is managed as local component state using useState, but other parts of the application (like header-actions.tsx at line 23 and header-actions-delete.tsx at line 29) read this value from the global store using store.state.exact. This creates a state synchronization issue where clicking to get exact counts in the header won't update the store, causing query invalidations to use stale exact values.

The exact state should be read from and written to the store instead of using local state. Replace the useState with useStore to read from the store, and use store.setState to update it when clicking.

Copilot uses AI. Check for mistakes.

const columnsCount = columns?.length ?? 0
const count = Number(total?.count)

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expression Number(total?.count) will return NaN when total is undefined during the initial loading state. This will cause the NumberFlow component to display NaN rows, which is not user-friendly. Consider using a fallback value like Number(total?.count ?? 0) or conditionally rendering based on whether total exists.

Suggested change
const count = Number(total?.count)
const count = Number(total?.count ?? 0)

Copilot uses AI. Check for mistakes.

return (
<div className="flex w-full items-center justify-between gap-6">
Expand All @@ -30,29 +35,40 @@ export function Header({ table, schema }: { table: string, schema: string }) {
{' '}
<span data-mask>{table}</span>
</h2>
<p className="text-xs text-muted-foreground">
<span className="tabular-nums">{columnsCount}</span>
{' '}
column
{columnsCount === 1 ? '' : 's'}
{' '}
{' '}
{total !== undefined
? (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>
<span className="tabular-nums">{columnsCount}</span>
{' '}
column
{columnsCount === 1 ? '' : 's'}
</span>
<Separator orientation="vertical" className="h-3!" />
<TooltipProvider>
<Tooltip>
<TooltipTrigger
className={cn('inline-flex items-center gap-1', !exact && total?.isEstimated && `
cursor-pointer
`)}

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The template literal with embedded newlines and indentation in the className creates unnecessary whitespace in the resulting class string. While cn() may handle this, it's cleaner to use an array or keep the condition inline. Consider refactoring to: className={cn('inline-flex items-center gap-1', !exact && total?.isEstimated && 'cursor-pointer')}

Copilot uses AI. Check for mistakes.
onClick={() => setExact(true)}

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onClick handler unconditionally sets exact to true, but it should only be clickable when the count is estimated. When exact is already true or the count is not estimated, clicking should have no effect. Consider conditionally calling setExact(true) only when !exact && total?.isEstimated to prevent unnecessary state updates and re-renders.

Suggested change
onClick={() => setExact(true)}
onClick={() => {
if (!exact && total?.isEstimated) {
setExact(true)
}
}}

Copilot uses AI. Check for mistakes.
>
<NumberFlow
className="tabular-nums"
value={total}
style={{
'--number-flow-mask-height': '0px',
}}
value={count}
format={{ notation: 'compact', compactDisplay: 'short', maximumFractionDigits: 1 }}
className={cn('text-muted-foreground tabular-nums', isLoading && `
animate-pulse
`)}

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar formatting issue: the template literal with embedded newlines creates unnecessary whitespace. Consider simplifying to: className={cn('text-muted-foreground tabular-nums', isLoading && 'animate-pulse')}

Copilot uses AI. Check for mistakes.
prefix={total?.isEstimated ? '~' : ''}
suffix={count === 1 ? ' row' : ' rows'}
/>
)
: <span className="animate-pulse">...</span>}
{' '}
row
{total !== undefined && total !== 1 && 's'}
</p>
</TooltipTrigger>
{!exact && total?.isEstimated && (
<TooltipContent side="bottom">
Click to get the exact count.
</TooltipContent>
)}

Copilot AI Jan 26, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When filters are applied, the code falls back to exact counts (because estimates are only used when !filters?.length). However, the UI state doesn't reflect this - the user might still see the tooltip "Click to get the exact count" even though an exact count is already being fetched due to filters. Consider updating the UI logic to show that counts are always exact when filters are active.

Copilot uses AI. Check for mistakes.
</Tooltip>
</TooltipProvider>
</div>
</div>
<Separator orientation="vertical" className="h-6!" />
<HeaderSearch table={table} schema={schema} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const storeState = type({
ref: type('object') as type.cast<Filter>,
values: 'string[]',
}).array() as type.cast<ActiveFilter[]>,
exact: 'boolean',
hiddenColumns: 'string[]',
orderBy: {
'[string]': '"ASC" | "DESC"',
Expand Down Expand Up @@ -45,6 +46,7 @@ export function createPageStore({ id, schema, table }: { id: string, schema: str
?? {
selected: [],
filters: [],
exact: false,
prompt: '',
hiddenColumns: [],
orderBy: {},
Expand Down
Loading