Skip to content

Commit ef738e5

Browse files
letstriKavish022
andauthored
feat: estimates counts implementation (#311)
Co-authored-by: Kavish002 <kavishbhatt022@gmail.com>
1 parent 7b71de0 commit ef738e5

13 files changed

Lines changed: 144 additions & 47 deletions

File tree

apps/desktop/src/entities/connection/dialects/clickhouse/schema/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ import type { WithSchema } from '../../../utils/types'
22
import type { InformationSchema } from './information'
33
import type { System } from './system'
44

5-
export type Database = WithSchema<InformationSchema, 'information_schema'> & WithSchema<System, 'system'>
5+
export type Database = WithSchema<InformationSchema, 'information_schema'>
6+
& WithSchema<System, 'system'>

apps/desktop/src/entities/connection/dialects/clickhouse/schema/system.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
export interface System {
66
columns: Columns
7+
parts: Parts
78
}
89

910
/**
@@ -16,3 +17,14 @@ interface Columns {
1617
name: string
1718
is_in_primary_key: number
1819
}
20+
21+
/**
22+
* @name parts
23+
* @type table
24+
*/
25+
interface Parts {
26+
database: string
27+
table: string
28+
rows: number
29+
active: number
30+
}

apps/desktop/src/entities/connection/dialects/postgres/schema/catalog.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ interface PgClass {
2929
relname: string
3030
relnamespace: number
3131
relkind: string
32+
reltuples: number
3233
}
3334

3435
/**

apps/desktop/src/entities/connection/queries/total.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ export function connectionTableTotalQuery({
1212
connection: typeof connections.$inferSelect
1313
table: string
1414
schema: string
15-
query: { filters: ActiveFilter[] }
15+
query: {
16+
filters: ActiveFilter[]
17+
exact: boolean
18+
}
1619
}) {
1720
return queryOptions({
1821
queryKey: [
@@ -25,9 +28,10 @@ export function connectionTableTotalQuery({
2528
'total',
2629
{
2730
filters: query.filters,
31+
exact: query.exact,
2832
},
2933
],
30-
queryFn: () => totalQuery(connection, { schema, table, filters: query.filters }),
34+
queryFn: () => totalQuery(connection, { schema, table, filters: query.filters, exact: query.exact }),
3135
throwOnError: false,
3236
})
3337
}
Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,120 @@
11
import type { ActiveFilter } from '@conar/shared/filters'
22
import { type } from 'arktype'
3+
import { sql } from 'kysely'
34
import { createQuery } from '../query'
45
import { buildWhere } from './rows'
56

67
export const totalQuery = createQuery({
7-
type: type('string | number | bigint | undefined').pipe(v => v !== undefined ? Number(v) : undefined),
8+
type: type({
9+
count: 'number',
10+
isEstimated: 'boolean',
11+
}),
812
query: ({
913
schema,
1014
table,
1115
filters,
12-
}: { schema: string, table: string, filters?: ActiveFilter[] }) => ({
16+
exact,
17+
}: {
18+
schema: string
19+
table: string
20+
filters?: ActiveFilter[]
21+
exact?: boolean
22+
}) => ({
1323
postgres: async (db) => {
24+
if (!exact && !filters?.length) {
25+
const estimate = await db
26+
.withSchema('pg_catalog')
27+
.selectFrom('pg_catalog.pg_class')
28+
.innerJoin('pg_catalog.pg_namespace', 'pg_catalog.pg_namespace.oid', 'pg_catalog.pg_class.relnamespace')
29+
.select('pg_catalog.pg_class.reltuples as count')
30+
.where('pg_catalog.pg_namespace.nspname', '=', schema)
31+
.where('pg_catalog.pg_class.relname', '=', table)
32+
.executeTakeFirst()
33+
34+
if (estimate && estimate.count !== null && estimate.count >= 0) {
35+
return {
36+
count: Math.round(estimate.count),
37+
isEstimated: true,
38+
}
39+
}
40+
}
41+
1442
const query = await db
1543
.withSchema(schema)
1644
.withTables<{ [table]: Record<string, unknown> }>()
1745
.selectFrom(table)
1846
.select(db.fn.countAll().as('total'))
1947
.$if(filters !== undefined, qb => qb.where(eb => buildWhere(eb, filters!)))
20-
.execute()
48+
.executeTakeFirst()
2149

22-
return query[0]?.total
50+
return { count: Number(query?.total ?? 0), isEstimated: false }
2351
},
2452
mysql: async (db) => {
53+
if (!exact && !filters?.length) {
54+
const estimate = await db
55+
.withSchema('information_schema')
56+
.selectFrom('information_schema.TABLES')
57+
.select('TABLE_ROWS as count')
58+
.where('TABLE_SCHEMA', '=', schema)
59+
.where('TABLE_NAME', '=', table)
60+
.executeTakeFirst()
61+
62+
if (estimate && estimate.count !== null && estimate.count >= 0) {
63+
return { count: estimate.count, isEstimated: true }
64+
}
65+
}
66+
2567
const query = await db
2668
.withSchema(schema)
2769
.withTables<{ [table]: Record<string, unknown> }>()
2870
.selectFrom(table)
2971
.select(db.fn.countAll().as('total'))
3072
.$if(filters !== undefined, qb => qb.where(eb => buildWhere(eb, filters!)))
31-
.execute()
73+
.executeTakeFirst()
3274

33-
return query[0]?.total
75+
return { count: Number(query?.total ?? 0), isEstimated: false }
3476
},
77+
3578
mssql: async (db) => {
3679
const query = await db
3780
.withSchema(schema)
3881
.withTables<{ [table]: Record<string, unknown> }>()
3982
.selectFrom(table)
4083
.select(db.fn.countAll().as('total'))
4184
.$if(filters !== undefined, qb => qb.where(eb => buildWhere(eb, filters!)))
42-
.execute()
85+
.executeTakeFirst()
4386

44-
return query[0]?.total
87+
return {
88+
count: Number(query?.total ?? 0),
89+
isEstimated: false,
90+
}
4591
},
92+
4693
clickhouse: async (db) => {
94+
if (!exact && !filters?.length) {
95+
const estimate = await db
96+
.withSchema('system')
97+
.selectFrom('system.parts')
98+
.select(db.fn.sum(sql.ref('rows')).as('count'))
99+
.where('database', '=', schema)
100+
.where('table', '=', table)
101+
.where('active', '=', 1)
102+
.executeTakeFirst()
103+
104+
if (estimate && Number(estimate.count) >= 0) {
105+
return { count: Number(estimate.count), isEstimated: true }
106+
}
107+
}
108+
47109
const query = await db
48110
.withSchema(schema)
49111
.withTables<{ [table]: Record<string, unknown> }>()
50112
.selectFrom(table)
51113
.select(db.fn.countAll().as('total'))
52114
.$if(filters !== undefined, qb => qb.where(eb => buildWhere(eb, filters!)))
53-
.execute()
115+
.executeTakeFirst()
54116

55-
return query[0]?.total
117+
return { count: Number(query?.total ?? 0), isEstimated: false }
56118
},
57119
}),
58120
})

apps/desktop/src/entities/connection/store/index.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ const tabType = type({
1111
preview: 'boolean',
1212
})
1313

14-
const definitionTabType = type({
15-
type: '"enums" | "constraints" | "indexes"',
16-
})
17-
1814
const queryToRunType = type({
1915
startLineNumber: 'number',
2016
endLineNumber: 'number',
@@ -30,7 +26,6 @@ const layoutSettingsType = type({
3026
export const connectionStoreType = type({
3127
lastOpenedPage: 'string | null' as type.cast<(Extract<keyof FileRoutesById, `/_protected/database/$id/${string}`> | null)>,
3228
lastOpenedChatId: 'string | null',
33-
definitionTabs: definitionTabType.array(),
3429
lastOpenedTable: type({
3530
schema: 'string',
3631
table: 'string',
@@ -60,7 +55,6 @@ export const connectionStoreType = type({
6055
const defaultState: typeof connectionStoreType.infer = {
6156
lastOpenedPage: null,
6257
lastOpenedChatId: null,
63-
definitionTabs: [],
6458
lastOpenedTable: null,
6559
sql: [
6660
'-- Write your SQL query here based on your database schema',
@@ -139,7 +133,6 @@ export function connectionStore(id: string) {
139133
lastOpenedPage: currentVal.lastOpenedPage,
140134
lastOpenedChatId: currentVal.lastOpenedChatId,
141135
lastOpenedTable: currentVal.lastOpenedTable,
142-
definitionTabs: currentVal.definitionTabs,
143136
sql: currentVal.sql,
144137
selectedLines: currentVal.selectedLines,
145138
loggerOpened: currentVal.loggerOpened,

apps/desktop/src/entities/connection/utils/fetching.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export async function prefetchConnectionTableCore({ connection, schema, table, q
2727
query: {
2828
filters: ActiveFilter[]
2929
orderBy: Record<string, 'ASC' | 'DESC'>
30+
exact: boolean
3031
}
3132
}) {
3233
await Promise.all([

apps/desktop/src/routes/_protected/database/$id/table/-components/header/header-actions-delete.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function HeaderActionsDelete({ table, schema, connection }: { table: stri
2626
onSuccess: () => {
2727
toast.success(`${selected.length} row${selected.length === 1 ? '' : 's'} successfully deleted`)
2828
queryClient.invalidateQueries(connectionRowsQuery({ connection, table, schema, query: { filters: store.state.filters, orderBy: store.state.orderBy } }))
29-
queryClient.invalidateQueries(connectionTableTotalQuery({ connection, table, schema, query: { filters: store.state.filters } }))
29+
queryClient.invalidateQueries(connectionTableTotalQuery({ connection, table, schema, query: { filters: store.state.filters, exact: store.state.exact } }))
3030
store.setState(state => ({
3131
...state,
3232
selected: [],

apps/desktop/src/routes/_protected/database/$id/table/-components/header/header-actions.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ import { HeaderActionsOrder } from './header-actions-order'
2020
export function HeaderActions({ table, schema }: { table: string, schema: string }) {
2121
const { connection } = Route.useLoaderData()
2222
const store = usePageStoreContext()
23-
const [filters, orderBy] = useStore(store, state => [state.filters, state.orderBy])
23+
const [filters, orderBy, exact] = useStore(store, state => [state.filters, state.orderBy, state.exact])
2424
const { isFetching, dataUpdatedAt, refetch, data: rows, isPending } = useInfiniteQuery(
2525
connectionRowsQuery({ connection, table, schema, query: { filters, orderBy } }),
2626
)
2727

2828
async function handleRefresh() {
2929
refetch()
3030
queryClient.invalidateQueries(connectionTableColumnsQuery({ connection, table, schema }))
31-
queryClient.invalidateQueries(connectionTableTotalQuery({ connection, table, schema, query: { filters } }))
31+
queryClient.invalidateQueries(connectionTableTotalQuery({ connection, table, schema, query: { filters, exact } }))
3232
queryClient.invalidateQueries(connectionConstraintsQuery({ connection }))
3333
}
3434

apps/desktop/src/routes/_protected/database/$id/table/-components/header/header.tsx

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { Separator } from '@conar/ui/components/separator'
2+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@conar/ui/components/tooltip'
3+
import { cn } from '@conar/ui/lib/utils'
24
import NumberFlow from '@number-flow/react'
35
import { useStore } from '@tanstack/react-store'
46
import { useConnectionTableTotal } from '~/entities/connection/queries'
@@ -13,7 +15,8 @@ export function Header({ table, schema }: { table: string, schema: string }) {
1315
const columns = useTableColumns({ connection, table, schema })
1416
const store = usePageStoreContext()
1517
const filters = useStore(store, state => state.filters)
16-
const { data: total } = useConnectionTableTotal({ connection, table, schema, query: { filters } })
18+
const exact = useStore(store, state => state.exact)
19+
const { data: total, isLoading } = useConnectionTableTotal({ connection, table, schema, query: { filters, exact } })
1720

1821
const columnsCount = columns?.length ?? 0
1922

@@ -30,29 +33,44 @@ export function Header({ table, schema }: { table: string, schema: string }) {
3033
{' '}
3134
<span data-mask>{table}</span>
3235
</h2>
33-
<p className="text-xs text-muted-foreground">
34-
<span className="tabular-nums">{columnsCount}</span>
35-
{' '}
36-
column
37-
{columnsCount === 1 ? '' : 's'}
38-
{' '}
39-
40-
{' '}
41-
{total !== undefined
42-
? (
43-
<NumberFlow
44-
className="tabular-nums"
45-
value={total}
46-
style={{
47-
'--number-flow-mask-height': '0px',
48-
}}
49-
/>
50-
)
51-
: <span className="animate-pulse">...</span>}
52-
{' '}
53-
row
54-
{total !== undefined && total !== 1 && 's'}
55-
</p>
36+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
37+
<span>
38+
<span className="tabular-nums">{columnsCount}</span>
39+
{' '}
40+
column
41+
{columnsCount === 1 ? '' : 's'}
42+
</span>
43+
<Separator orientation="vertical" className="h-3!" />
44+
{total?.count === undefined
45+
? <>...</>
46+
: (
47+
<TooltipProvider>
48+
<Tooltip>
49+
<TooltipTrigger
50+
className={cn('inline-flex items-center gap-1', !exact && total.isEstimated && `
51+
cursor-pointer
52+
`)}
53+
onClick={() => store.setState(state => ({ ...state, exact: true } satisfies typeof state))}
54+
>
55+
<NumberFlow
56+
value={total.count}
57+
format={{ notation: 'compact', compactDisplay: 'short', maximumFractionDigits: 1 }}
58+
className={cn('text-muted-foreground tabular-nums', isLoading && `
59+
animate-pulse text-muted-foreground/50
60+
`)}
61+
prefix={total.isEstimated ? '~' : ''}
62+
suffix={total.count === 1 ? ' row' : ' rows'}
63+
/>
64+
</TooltipTrigger>
65+
{!exact && total.isEstimated && (
66+
<TooltipContent side="bottom">
67+
Click to get the exact count.
68+
</TooltipContent>
69+
)}
70+
</Tooltip>
71+
</TooltipProvider>
72+
)}
73+
</div>
5674
</div>
5775
<Separator orientation="vertical" className="h-6!" />
5876
<HeaderSearch table={table} schema={schema} />

0 commit comments

Comments
 (0)