-
-
Notifications
You must be signed in to change notification settings - Fork 108
feat: estimates counts implementation #311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
1d45d19
b47ff18
2b72758
03f21f2
898db59
ade1c55
8c11f14
59e1db8
3f80df0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,7 @@ interface PgClass { | |
| relname: string | ||
| relnamespace: number | ||
| relkind: string | ||
| reltuples: number | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| 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 { | ||
| 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) { | ||
|
letstri marked this conversation as resolved.
Outdated
|
||
| return { count: estimate.count, isEstimated: true } | ||
|
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) { | ||
|
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 } | ||
| }, | ||
| }), | ||
| }) | ||
| 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' | ||||||||||||||
|
|
@@ -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 } }) | ||||||||||||||
|
||||||||||||||
|
|
||||||||||||||
| const columnsCount = columns?.length ?? 0 | ||||||||||||||
| const count = Number(total?.count) | ||||||||||||||
|
||||||||||||||
| const count = Number(total?.count) | |
| const count = Number(total?.count ?? 0) |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
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
AI
Jan 26, 2026
There was a problem hiding this comment.
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.
| onClick={() => setExact(true)} | |
| onClick={() => { | |
| if (!exact && total?.isEstimated) { | |
| setExact(true) | |
| } | |
| }} |
Copilot
AI
Jan 26, 2026
There was a problem hiding this comment.
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
AI
Jan 26, 2026
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In PostgreSQL,
reltuplescan be -1 for tables that have never been analyzed. While the checkestimate && estimate.count >= 0correctly handles negative values, it would be clearer to add an explicit null check as well:estimate && estimate.count != null && estimate.count >= 0for defensive programming, especially since the type system might not catch all edge cases.