From a4334a2cc75b85e3c569e24c9e33be94b37064d7 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Fri, 29 May 2026 15:12:22 -0400 Subject: [PATCH] feat(studio): paginate Schema Designer via useInfiniteTablesQuery (#46402) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Performance improvement / feature ## What is the current behavior? The Schema Designer fetches all tables in a single request via `useTablesQuery`. For schemas with 400+ tables this blocks first paint on a large payload. ## What is the new behavior? `SchemaGraph` uses `useInfiniteTablesQuery` (pageSize: 100) so the first 100 tables paint immediately. A "Load more tables" button appears above the legend whenever more pages remain, letting users load the rest on demand. ## Additional context ## Summary by CodeRabbit * **New Features** * Added a "Find table…" selector and keyboard shortcut to quickly locate and focus tables; supports incremental loading and debounced name search (with literal wildcard handling). * Schema Graph shows a bottom "Load more tables" control with loading state and preserves view after loading more. * **Refactor** * Table listing switched to infinite/paginated retrieval and improved "no tables" logic; server-side name filtering supported. * **Tests** * E2E tests add a schema-visualizer wait helper and update flows to support the paginated visualizer. [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46402?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) --- .../Database/Schemas/FindTableSelector.tsx | 144 ++++++++++++++++++ .../Database/Schemas/SchemaGraph.tsx | 132 +++++++++++++--- .../SidePanelEditor/SidePanelEditor.tsx | 3 + .../SidePanelEditor/SidePanelEditor.utils.tsx | 8 + apps/studio/data/tables/keys.ts | 14 +- apps/studio/data/tables/tables-query.ts | 29 +++- .../shortcuts/registry/schema-visualizer.ts | 8 + e2e/studio/features/database.spec.ts | 35 ++++- e2e/studio/utils/wait-for-response.ts | 10 ++ .../sql/studio/database/tables-paginated.ts | 12 ++ .../test/sql/studio/tables-paginated.test.ts | 42 +++++ 11 files changed, 394 insertions(+), 43 deletions(-) create mode 100644 apps/studio/components/interfaces/Database/Schemas/FindTableSelector.tsx diff --git a/apps/studio/components/interfaces/Database/Schemas/FindTableSelector.tsx b/apps/studio/components/interfaces/Database/Schemas/FindTableSelector.tsx new file mode 100644 index 00000000000..b061c444829 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Schemas/FindTableSelector.tsx @@ -0,0 +1,144 @@ +import { Loader2, Search } from 'lucide-react' +import { ComponentPropsWithoutRef, forwardRef, useMemo, useState } from 'react' +import { + Button, + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + Popover, + PopoverContent, + PopoverTrigger, + ScrollArea, +} from 'ui' + +import { useInfiniteTablesQuery } from '@/data/tables/tables-query' +import { useDebouncedValue } from '@/hooks/misc/useDebouncedValue' +import type { SafePostgresTable } from '@/lib/postgres-types' + +type FindTableSelectorProps = Omit, 'onSelect'> & { + projectRef?: string + connectionString?: string | null + schema?: string + disabled?: boolean + size?: 'tiny' | 'small' + open: boolean + onOpenChange: (open: boolean) => void + onSelect: (table: SafePostgresTable) => void +} + +export const FindTableSelector = forwardRef( + ( + { + className, + projectRef, + connectionString, + schema, + disabled = false, + size = 'tiny', + open, + onOpenChange, + onSelect, + ...rest + }, + ref + ) => { + const [search, setSearch] = useState('') + const debouncedSearch = useDebouncedValue(search, 300) + const nameFilter = debouncedSearch.trim() || undefined + + const { data, isFetching, hasNextPage, isFetchingNextPage, fetchNextPage } = + useInfiniteTablesQuery( + { + projectRef, + connectionString, + schema, + includeColumns: false, + pageSize: 50, + nameFilter, + }, + { enabled: open } + ) + + const tables = useMemo(() => data?.pages.flat() ?? [], [data]) + + const handleOpenChange = (next: boolean) => { + if (!next) setSearch('') + onOpenChange(next) + } + + return ( +
+ + + + + + + + + {isFetching && tables.length === 0 ? ( +
+ + Loading tables +
+ ) : ( + <> + No tables found + + 7 ? 'h-[210px]' : ''}> + {tables.map((table) => ( + { + onSelect(table) + handleOpenChange(false) + }} + > + {table.name} + + ))} + {hasNextPage && ( +
+ +
+ )} +
+
+ + )} +
+
+
+
+
+ ) + } +) + +FindTableSelector.displayName = 'FindTableSelector' diff --git a/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx b/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx index e15a05f61ef..4293e97eed1 100644 --- a/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx +++ b/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx @@ -8,6 +8,7 @@ import { MiniMap, Node, OnSelectionChangeParams, + Panel, ReactFlow, useReactFlow, } from '@xyflow/react' @@ -41,6 +42,7 @@ import { Admonition } from 'ui-patterns/admonition' import { SidePanelEditor } from '../../TableGridEditor/SidePanelEditor/SidePanelEditor' import { DefaultEdge } from './DefaultEdge' +import { FindTableSelector } from './FindTableSelector' import { SchemaGraphContextProvider, SchemaGraphContextType } from './SchemaGraphContext' import { SchemaGraphLegend } from './SchemaGraphLegend' import { EdgeData, TableNodeData } from './Schemas.constants' @@ -56,7 +58,7 @@ import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import SchemaSelector from '@/components/ui/SchemaSelector' import { Shortcut } from '@/components/ui/Shortcut' import { useSchemasQuery } from '@/data/database/schemas-query' -import { useTablesQuery } from '@/data/tables/tables-query' +import { useInfiniteTablesQuery } from '@/data/tables/tables-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useLocalStorage } from '@/hooks/misc/useLocalStorage' import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState' @@ -117,18 +119,23 @@ export const SchemaGraph = () => { }) const { - data: tables = [], + data: tablesData, error: errorTables, isSuccess: isSuccessTables, isPending: isLoadingTables, isError: isErrorTables, - } = useTablesQuery({ + hasNextPage, + isFetchingNextPage, + fetchNextPage, + } = useInfiniteTablesQuery({ projectRef: project?.ref, connectionString: project?.connectionString, schema: selectedSchema, includeColumns: true, + pageSize: 100, }) - const hasNoTables = isSuccessSchemas && tables.length === 0 + const tables = useMemo(() => tablesData?.pages.flat() ?? [], [tablesData]) + const hasNoTables = isSuccessTables && isSuccessSchemas && tables.length === 0 && !hasNextPage const schema = (schemas ?? []).find((s) => s.name === selectedSchema) const [, setStoredPositions] = useLocalStorage( @@ -227,8 +234,14 @@ export const SchemaGraph = () => { } const [schemaSelectorOpen, setSchemaSelectorOpen] = useState(false) + const [findTableOpen, setFindTableOpen] = useState(false) const [autoLayoutDialogOpen, setAutoLayoutDialogOpen] = useState(false) + const handleSelectSchema = (name: string) => { + setFindTableOpen(false) + setSelectedSchema(name) + } + const shortcutsEnabled = isSuccessSchemas && !hasNoTables useShortcut(SHORTCUT_IDS.SCHEMA_VISUALIZER_COPY_SQL, copyAsSQL, { enabled: shortcutsEnabled }) @@ -241,8 +254,13 @@ export const SchemaGraph = () => { useShortcut(SHORTCUT_IDS.SCHEMA_VISUALIZER_DOWNLOAD_SVG, () => downloadImage('svg'), { enabled: shortcutsEnabled, }) + useShortcut(SHORTCUT_IDS.SCHEMA_VISUALIZER_FIND_TABLE, () => setFindTableOpen(true), { + enabled: shortcutsEnabled, + }) const isFirstLoad = useRef(true) + const fitViewOnNextLayout = useRef(false) + const pendingFocusTableIdRef = useRef(null) useEffect(() => { if (isSuccessTables && isSuccessSchemas && tables.length > 0) { const schema = schemas.find((s) => s.name === selectedSchema) as PGSchema @@ -250,14 +268,49 @@ export const SchemaGraph = () => { reactFlowInstance.setNodes(nodes) reactFlowInstance.setEdges(edges) // Prevent resetting a view after first load to avoid layout changes after editing a column - if (isFirstLoad.current) { + if (isFirstLoad.current || fitViewOnNextLayout.current) { isFirstLoad.current = false + fitViewOnNextLayout.current = false setTimeout(() => reactFlowInstance.fitView({})) // it needs to happen during next event tick } + const pendingId = pendingFocusTableIdRef.current + if (pendingId !== null && nodes.some((n) => n.id === pendingId)) { + pendingFocusTableIdRef.current = null + setTimeout(() => + reactFlowInstance.fitView({ + nodes: [{ id: pendingId }], + duration: 300, + maxZoom: 1.5, + }) + ) + } }) } }, [isSuccessTables, isSuccessSchemas, tables, reactFlowInstance, ref, schemas, selectedSchema]) + const handleFindTableSelect = async (table: SafePostgresTable) => { + const targetId = String(table.id) + if (reactFlowInstance.getNode(targetId)) { + reactFlowInstance.fitView({ + nodes: [{ id: targetId }], + duration: 300, + maxZoom: 1.5, + }) + return + } + // Selected table isn't loaded yet — queue the fitView and pull pages until + // it shows up. The build-effect above will consume the pending id once the + // node is mounted. + pendingFocusTableIdRef.current = targetId + let result = await fetchNextPage() + while ( + result.hasNextPage && + !result.data?.pages.some((page) => page.some((t) => t.id === table.id)) + ) { + result = await fetchNextPage() + } + } + const schemaGraphContext = useMemo( () => ({ selectedEdge, @@ -294,23 +347,43 @@ export const SchemaGraph = () => { {isSuccessSchemas && ( <> - setSchemaSelectorOpen(true)} - options={{ enabled: isSuccessSchemas }} - side="bottom" - tooltipOpen={schemaSelectorOpen ? false : undefined} - > - - +
+ setSchemaSelectorOpen(true)} + options={{ enabled: isSuccessSchemas }} + side="bottom" + tooltipOpen={schemaSelectorOpen ? false : undefined} + > + + + {!hasNoTables && ( + setFindTableOpen(true)} + options={{ enabled: shortcutsEnabled }} + side="bottom" + tooltipOpen={findTableOpen ? false : undefined} + > + + + )} +
{!hasNoTables && (
@@ -483,6 +556,21 @@ export const SchemaGraph = () => { className="border rounded-md shadow-xs" /> + {hasNextPage && ( + + + + )}
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx index 758dc85397e..481f4a511b3 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.tsx @@ -439,6 +439,9 @@ export const SidePanelEditor = ({ queryClient.invalidateQueries({ queryKey: tableKeys.list(project?.ref, selectedTable?.schema, includeColumns), }), + queryClient.invalidateQueries({ + queryKey: tableKeys.infiniteListPrefix(project?.ref, selectedTable?.schema), + }), ]) // We need to invalidate tableRowsAndCount after tableEditor diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx index f45fb92b94e..a654f7f0048 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.utils.tsx @@ -781,6 +781,14 @@ export const updateTable = async ({ schema: table.schema, payload, }) + await queryClient.invalidateQueries({ + queryKey: tableKeys.infiniteListPrefix(projectRef, table.schema), + }) + if (payload.schema && payload.schema !== table.schema) { + await queryClient.invalidateQueries({ + queryKey: tableKeys.infiniteListPrefix(projectRef, payload.schema), + }) + } } if (payload.rls_enabled === true) { diff --git a/apps/studio/data/tables/keys.ts b/apps/studio/data/tables/keys.ts index 87c5603922e..51440a268cc 100644 --- a/apps/studio/data/tables/keys.ts +++ b/apps/studio/data/tables/keys.ts @@ -2,15 +2,15 @@ export const tableKeys = { names: (projectRef: string | undefined) => ['projects', projectRef, 'table-names'] as const, list: (projectRef: string | undefined, schema?: string, includeColumns?: boolean) => ['projects', projectRef, 'tables', schema, includeColumns].filter(Boolean), - infiniteList: ( - projectRef: string | undefined, - schema?: string, - includeColumns?: boolean, - pageSize?: number - ) => - ['projects', projectRef, 'tables', 'infinite', schema, includeColumns, pageSize].filter( + infiniteListPrefix: (projectRef: string | undefined, schema?: string) => + ['projects', projectRef, 'tables', 'infinite', schema].filter( (part) => part !== undefined && part !== null && part !== '' ), + infiniteList: ( + projectRef: string | undefined, + schema: string | undefined, + options: { includeColumns?: boolean; pageSize?: number; nameFilter?: string } + ) => [...tableKeys.infiniteListPrefix(projectRef, schema), options], retrieve: (projectRef: string | undefined, name: string, schema: string) => ['projects', projectRef, 'table', schema, name].filter(Boolean), } diff --git a/apps/studio/data/tables/tables-query.ts b/apps/studio/data/tables/tables-query.ts index 3d6aa93fdb3..d98ed36969e 100644 --- a/apps/studio/data/tables/tables-query.ts +++ b/apps/studio/data/tables/tables-query.ts @@ -130,6 +130,7 @@ export type InfiniteTablesVariables = Pick< 'projectRef' | 'connectionString' | 'schema' | 'includeColumns' > & { pageSize?: number + nameFilter?: string } export async function getTablesPage( @@ -140,9 +141,11 @@ export async function getTablesPage( includeColumns = false, limit, afterOid, + nameFilter, }: Pick & { limit: number afterOid: number + nameFilter?: string }, signal?: AbortSignal ) { @@ -150,16 +153,22 @@ export async function getTablesPage( throw new Error('projectRef is required') } - const sql = getTablesPaginatedSql({ schema, includeColumns, limit, afterOid }) + const sql = getTablesPaginatedSql({ schema, includeColumns, limit, afterOid, nameFilter }) const { result } = await executeSql( { projectRef, connectionString, sql, - queryKey: tableKeys - .infiniteList(projectRef, schema, includeColumns, limit) - .concat([afterOid]), + queryKey: [ + `project:${projectRef}`, + `schema:${schema}`, + `infinite_tables`, + includeColumns ? 'with_columns' : null, + nameFilter ? `search:${nameFilter}` : null, + limit ? `page_size:${limit}` : null, + afterOid ? `after:${afterOid}` : null, + ], }, signal ) @@ -168,14 +177,21 @@ export async function getTablesPage( } export const useInfiniteTablesQuery = >( - { projectRef, connectionString, schema, includeColumns, pageSize = 50 }: InfiniteTablesVariables, + { + projectRef, + connectionString, + schema, + includeColumns, + pageSize = 50, + nameFilter, + }: InfiniteTablesVariables, { enabled = true, ...options }: UseCustomInfiniteQueryOptions = {} ) => { return useInfiniteQuery({ - queryKey: tableKeys.infiniteList(projectRef, schema, includeColumns, pageSize), + queryKey: tableKeys.infiniteList(projectRef, schema, { includeColumns, pageSize, nameFilter }), queryFn: ({ signal, pageParam }) => getTablesPage( { @@ -185,6 +201,7 @@ export const useInfiniteTablesQuery = >( includeColumns, limit: pageSize, afterOid: pageParam, + nameFilter, }, signal ), diff --git a/apps/studio/state/shortcuts/registry/schema-visualizer.ts b/apps/studio/state/shortcuts/registry/schema-visualizer.ts index 80b8e5d40a0..f64c5e3033b 100644 --- a/apps/studio/state/shortcuts/registry/schema-visualizer.ts +++ b/apps/studio/state/shortcuts/registry/schema-visualizer.ts @@ -7,6 +7,7 @@ export const SCHEMA_VISUALIZER_SHORTCUT_IDS = { SCHEMA_VISUALIZER_DOWNLOAD_SVG: 'schema-visualizer.download-svg', SCHEMA_VISUALIZER_AUTO_LAYOUT: 'schema-visualizer.auto-layout', SCHEMA_VISUALIZER_FOCUS_SCHEMA: 'schema-visualizer.focus-schema', + SCHEMA_VISUALIZER_FIND_TABLE: 'schema-visualizer.find-table', } export type SchemaVisualizerShortcutId = @@ -55,4 +56,11 @@ export const schemaVisualizerRegistry: RegistryDefinations { test.describe('Schema Visualizer', () => { test('actions works as expected', async ({ page, ref }) => { @@ -25,15 +32,19 @@ test.describe('Database', () => { await dropTable(databaseTableName) } ) + const wait = createApiResponseWaiter( page, 'pg-meta', ref, - 'tables?include_columns=true&included_schemas=public' + 'query?key=project:default-schema:public-infinite_tables' ) await page.goto(toUrl(`/project/${env.PROJECT_REF}/database/schemas?schema=public`)) await wait + // focus the test table so it mounts inside the viewport + await focusTableInVisualizer(page, databaseTableName) + // validates table and column exists await expect(page.getByText(databaseTableName, { exact: true })).toBeVisible() await expect(page.getByText(databaseColumnName, { exact: true })).toBeVisible() @@ -69,10 +80,12 @@ test.describe('Database', () => { // changing schema -> auth await page.getByTestId('schema-selector').click() await page.getByRole('option', { name: 'auth' }).click() - await waitForDatabaseToLoad(page, ref, 'auth') - await expect(page.getByText('users', { exact: true })).toBeVisible() - await expect(page.getByText('sso_providers', { exact: true })).toBeVisible() - await expect(page.getByText('saml_providers', { exact: true })).toBeVisible() + await waitForSchemaVisualizerToLoad(page, ref, 'auth') + + for (const tableName of ['users', 'sso_providers', 'saml_providers']) { + await focusTableInVisualizer(page, tableName) + await expect(page.getByText(tableName, { exact: true })).toBeVisible() + } }) test('table actions work as expected', async ({ page, ref }) => { @@ -90,11 +103,14 @@ test.describe('Database', () => { page, 'pg-meta', ref, - 'tables?include_columns=true&included_schemas=public' + 'query?key=project:default-schema:public-infinite_tables' ) await page.goto(toUrl(`/project/${env.PROJECT_REF}/database/schemas?schema=public`)) await wait + // focus the test table so it mounts inside the viewport + await focusTableInVisualizer(page, databaseTableName) + // validates table and column exists await expect(page.getByText(databaseTableName, { exact: true })).toBeVisible() // test we can edit the column @@ -158,11 +174,14 @@ test.describe('Database', () => { page, 'pg-meta', ref, - 'tables?include_columns=true&included_schemas=public' + 'query?key=project:default-schema:public-infinite_tables' ) await page.goto(toUrl(`/project/${env.PROJECT_REF}/database/schemas?schema=public`)) await wait + // focus the test table so it mounts inside the viewport + await focusTableInVisualizer(page, databaseTableName) + // validates table and column exists await expect(page.getByText(databaseTableName, { exact: true })).toBeVisible() await expect(page.getByText(databaseColumnName, { exact: true })).toBeVisible() diff --git a/e2e/studio/utils/wait-for-response.ts b/e2e/studio/utils/wait-for-response.ts index 0a0ab48ad4e..c812aa26c9e 100644 --- a/e2e/studio/utils/wait-for-response.ts +++ b/e2e/studio/utils/wait-for-response.ts @@ -112,3 +112,13 @@ export async function waitForDatabaseToLoad(page: Page, ref: string, schema?: st `tables?include_columns=true&included_schemas=${databaseSchema}` ) } + +export async function waitForSchemaVisualizerToLoad(page: Page, ref: string, schema?: string) { + const databaseSchema = schema || 'public' + return await waitForApiResponse( + page, + 'pg-meta', + ref, + `query?key=project:default-schema:${schema ?? 'public'}-infinite_tables` + ) +} diff --git a/packages/pg-meta/src/sql/studio/database/tables-paginated.ts b/packages/pg-meta/src/sql/studio/database/tables-paginated.ts index de5d986e7b0..d18dc71867d 100644 --- a/packages/pg-meta/src/sql/studio/database/tables-paginated.ts +++ b/packages/pg-meta/src/sql/studio/database/tables-paginated.ts @@ -21,11 +21,13 @@ export const getTablesPaginatedSql = ({ includeColumns = false, limit, afterOid, + nameFilter, }: { schema?: string includeColumns?: boolean limit: number afterOid: number + nameFilter?: string }): SafeSqlFragment => { const filter = filterByList( schema ? [schema] : undefined, @@ -33,6 +35,10 @@ export const getTablesPaginatedSql = ({ schema ? undefined : DEFAULT_SYSTEM_SCHEMAS ) const schemaFilter = filter ? safeSql`and nc.nspname ${filter}` : safeSql`` + const nameFilterClause = + nameFilter && nameFilter.length > 0 + ? safeSql`and c.relname ilike ${literal(`%${escapeIlikeLiteral(nameFilter)}%`)}` + : safeSql`` const columnsCte = includeColumns ? safeSql`, columns as (${getColumnsSql({ @@ -70,6 +76,7 @@ export const getTablesPaginatedSql = ({ or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') ) ${schemaFilter} + ${nameFilterClause} order by c.oid limit ${literal(limit)} ), @@ -196,3 +203,8 @@ export const getTablesPaginatedSql = ({ order by tables.id ` } + +// Escape LIKE/ILIKE wildcards so user input is matched literally. ILIKE's +// default escape character is `\`, so we escape `\`, `%`, and `_` and let the +// pattern carry its own surrounding `%`s as wildcards. +const escapeIlikeLiteral = (value: string) => value.replace(/([\\%_])/g, '\\$1') diff --git a/packages/pg-meta/test/sql/studio/tables-paginated.test.ts b/packages/pg-meta/test/sql/studio/tables-paginated.test.ts index bacde07b0bf..6d7aadbdc86 100644 --- a/packages/pg-meta/test/sql/studio/tables-paginated.test.ts +++ b/packages/pg-meta/test/sql/studio/tables-paginated.test.ts @@ -287,6 +287,48 @@ withTestDatabase('includeColumns: true returns columns per row', async ({ execut expect(empty.columns).toEqual([]) }) +withTestDatabase( + 'nameFilter restricts results to tables whose name matches (case-insensitive)', + async ({ executeQuery }) => { + await executeQuery(` + create table public.users_archive_2024 (id bigint primary key); + create table public.users_archive_2025 (id bigint primary key); + `) + + const sql = getTablesPaginatedSql({ + schema: 'public', + limit: 100, + afterOid: 0, + nameFilter: 'USERS_ARCHIVE', + }) + const rows = await executeQuery(sql) + const names = rows.map((r) => r.name).sort() + expect(names).toEqual(['users_archive_2024', 'users_archive_2025']) + } +) + +withTestDatabase( + 'nameFilter escapes LIKE wildcards so they match literally', + async ({ executeQuery }) => { + await executeQuery(` + create table public."weird_name" (id bigint primary key); + create table public."weird%name" (id bigint primary key); + `) + + const sql = getTablesPaginatedSql({ + schema: 'public', + limit: 100, + afterOid: 0, + nameFilter: 'weird%', + }) + const rows = await executeQuery(sql) + const names = rows.map((r) => r.name) + // Only the table with a literal `%` in its name should match. + expect(names).toContain('weird%name') + expect(names).not.toContain('weird_name') + } +) + withTestDatabase('afterOid skips tables with smaller oids', async ({ executeQuery }) => { const fullSql = getTablesPaginatedSql({ schema: 'public', limit: 1000, afterOid: 0 }) const all = await executeQuery(fullSql)