fix(mssql foreign tables): disallow sort on columns filtered for equality (#40137)

There is an edge case interaction between the Postgres query parser and
MSSQL foreign tables, where the query parser may drop sort clauses that
are redundant with applied filters. This leads to invalid MSSQL syntax,
because the resulting query has a `limit` but no `sort`, and the user
sees a confusing error message.

This PR detects this edge case on MSSQL foreign tables. There are three
cases:
1. The user filters by a column, but there are still other columns
available for sorting. The default search for a sorting column will
leave out the filtered column.
2. The user filters by a column/several columns, and there are no more
columns that can be used for sorting. We stop the query and show an
admonition.
3. The user filters by a column, then tries to sort by the same column.
We stop the query and show an admonition.
This commit is contained in:
Charis authored and GitHub committed 2025-11-06 13:14:30 -05:00
1 parent d2a4175bdb
commit df63ce3658
21 files changed
+267 -60

No files matched your search

@@ -0,0 +1,77 @@
import { isMsSqlForeignTable, type Entity } from 'data/table-editor/table-editor-types'
import type { ComponentType, ReactNode } from 'react'
import { Admonition } from 'ui-patterns'
import type { Filter, Sort } from './types'
type ValidateMsSqlSortingParams = {
filters: Filter[]
sorts: Sort[]
table: Entity
}
type MsSqlWarning = 'ConflictingSort' | 'NoValidSortPossible'
/**
* There is an edge case with the Postgres query planner and MS SQL foreign
* tables, where the Postgres query planner will drop sort clauses that are
* redundant with filters, resulting in invalid MS SQL syntax. We want to
* detect any conflicting or impossible sorts on filtered columns when the
* table is an MS SQL foreign table, and warn the user.
*/
export const validateMsSqlSorting = ({
filters,
sorts,
table,
}: ValidateMsSqlSortingParams):
| { warning: MsSqlWarning; Component: ComponentType }
| { warning: null } => {
const isMsSql = isMsSqlForeignTable(table)
if (!isMsSql) return { warning: null }
const equalityFilterColumns = new Set(
filters
.filter((filter) => filter.operator === '=' || filter.operator === 'is')
.map((filter) => filter.column)
)
const conflictingSort =
sorts.length > 0 && sorts.every((sort) => equalityFilterColumns.has(sort.column))
const showMsSqlSortWarning = equalityFilterColumns.size > 0 && !!conflictingSort
if (showMsSqlSortWarning)
return { warning: 'ConflictingSort', Component: MsSqlSortWarningAdmonition }
const noSortColumnsRemaining =
table.columns.length > 0 &&
table.columns.every((col) => col.data_type === 'json' || equalityFilterColumns.has(col.name))
const showMsSqlNoValidSortWarning = filters.length > 0 && noSortColumnsRemaining
if (showMsSqlNoValidSortWarning)
return { warning: 'NoValidSortPossible', Component: MsSqlNoValidSortAdmonition }
return { warning: null }
}
type MsSqlAdmonitionProps = {
title: string
children: string
}
const MsSqlAdmonition = ({ title, children }: MsSqlAdmonitionProps): ReactNode => (
<div className="mt-2 px-3 pb-2">
<Admonition type="warning" title={title} description={children} />
</div>
)
const MsSqlSortWarningAdmonition = (): ReactNode => (
<MsSqlAdmonition title="Cannot sort by filtered column">
Sorting only by columns filtered with "=" or "is" doesn't work on MSSQL tables. Pick a different
sorting column, or add a column not in your filter.
</MsSqlAdmonition>
)
const MsSqlNoValidSortAdmonition = (): ReactNode => (
<MsSqlAdmonition title="No valid sort column remaining">
All columns that can be sorted have been filtered with "=" or "is", which doesn't work on MSSQL
tables. Remove a column from your filter to continue.
</MsSqlAdmonition>
)
+17 -2
View File
@@ -5,6 +5,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend'
import { createPortal } from 'react-dom'
import { useParams } from 'common'
import { isMsSqlForeignTable } from 'data/table-editor/table-editor-types'
import { useTableRowsQuery } from 'data/table-rows/table-rows-query'
import { RoleImpersonationState } from 'lib/role-impersonation'
import { EMPTY_ARR } from 'lib/void'
@@ -22,6 +23,7 @@ import { GridProps } from './types'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useTableFilter } from './hooks/useTableFilter'
import { useTableSort } from './hooks/useTableSort'
import { validateMsSqlSorting } from './MsSqlValidation'
export const SupabaseGrid = ({
customHeader,
@@ -47,6 +49,11 @@ export const SupabaseGrid = ({
const roleImpersonationState = useRoleImpersonationStateSnapshot()
const msSqlWarning = isMsSqlForeignTable(snap.originalTable)
? validateMsSqlSorting({ filters, sorts, table: snap.originalTable })
: { warning: null }
const tableQueriesEnabled = msSqlWarning.warning === null
const { data, error, isSuccess, isError, isLoading, isRefetching } = useTableRowsQuery(
{
projectRef: project?.ref,
@@ -60,6 +67,7 @@ export const SupabaseGrid = ({
},
{
keepPreviousData: true,
enabled: tableQueriesEnabled,
retry: (_, error: any) => {
const doesNotExistError = error && error.message?.includes('does not exist')
if (doesNotExistError) onApplySorts([])
@@ -77,7 +85,13 @@ export const SupabaseGrid = ({
return (
<DndProvider backend={HTML5Backend} context={window}>
<div className="sb-grid h-full flex flex-col">
<Header customHeader={customHeader} isRefetching={isRefetching} />
<Header
customHeader={customHeader}
isRefetching={isRefetching}
tableQueriesEnabled={tableQueriesEnabled}
/>
{msSqlWarning.warning !== null && <msSqlWarning.Component />}
{children || (
<>
@@ -86,13 +100,14 @@ export const SupabaseGrid = ({
{...gridProps}
rows={rows}
error={error}
isDisabled={!tableQueriesEnabled}
isLoading={isLoading}
isSuccess={isSuccess}
isError={isError}
filters={filters}
onApplyFilters={onApplyFilters}
/>
<Footer />
<Footer enableForeignRowsQuery={tableQueriesEnabled} />
<Shortcuts gridRef={gridRef} rows={rows} />
</>
)}
@@ -7,7 +7,11 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useUrlState } from 'hooks/ui/useUrlState'
import { Pagination } from './pagination/Pagination'
export const Footer = () => {
type FooterProps = {
enableForeignRowsQuery?: boolean
}
export const Footer: React.FC<FooterProps> = ({ enableForeignRowsQuery = true }: FooterProps) => {
const { id: _id } = useParams()
const id = _id ? Number(_id) : undefined
const { data: project } = useSelectedProjectQuery()
@@ -33,7 +37,7 @@ export const Footer = () => {
return (
<GridFooter>
{selectedView === 'data' && <Pagination />}
{selectedView === 'data' && <Pagination enableForeignRowsQuery={enableForeignRowsQuery} />}
<div className="ml-auto flex items-center gap-x-2">
{(isViewSelected || isTableSelected) && (
@@ -47,7 +47,11 @@ const RowCountSelector = ({
)
}
export const Pagination = () => {
type PaginationProps = {
enableForeignRowsQuery?: boolean
}
export const Pagination = ({ enableForeignRowsQuery = true }: PaginationProps) => {
const { id: _id } = useParams()
const id = _id ? Number(_id) : undefined
@@ -108,7 +112,7 @@ export const Pagination = () => {
roleImpersonationState: roleImpersonationState as RoleImpersonationState,
},
{
enabled: isForeignTableSelected,
enabled: isForeignTableSelected && enableForeignRowsQuery,
}
)
const isLastPage = (rowsData?.rows ?? []).length < tableEditorSnap.rowsPerPage
@@ -223,8 +227,8 @@ export const Pagination = () => {
icon={<ArrowRight />}
type="outline"
className="px-1.5"
disabled={isLastPage}
loading={isLoadingRows}
disabled={isLastPage || !enableForeignRowsQuery}
loading={isLoadingRows && enableForeignRowsQuery}
onClick={goToNextPage}
/>
<RowCountSelector onRowsPerPageChange={onRowsPerPageChange} />
@@ -26,6 +26,7 @@ const rowKeyGetter = (row: SupaRow) => {
interface IGrid extends GridProps {
rows: any[]
error: any
isDisabled?: boolean
isLoading: boolean
isSuccess: boolean
isError: boolean
@@ -45,6 +46,7 @@ export const Grid = memo(
rowClass,
rows,
error,
isDisabled = false,
isLoading,
isSuccess,
isError,
@@ -154,7 +156,7 @@ export const Grid = memo(
onDragLeave={onDragOver}
onDrop={onFileDrop}
>
{isLoading && <GenericSkeletonLoader />}
{isLoading && !isDisabled && <GenericSkeletonLoader />}
{isError && <GridError error={error} />}
@@ -58,9 +58,10 @@ export const MAX_EXPORT_ROW_COUNT_MESSAGE = (
export type HeaderProps = {
customHeader: ReactNode
isRefetching: boolean
tableQueriesEnabled?: boolean
}
export const Header = ({ customHeader, isRefetching }: HeaderProps) => {
export const Header = ({ customHeader, isRefetching, tableQueriesEnabled = true }: HeaderProps) => {
const snap = useTableEditorTableStateSnapshot()
return (
@@ -69,7 +70,7 @@ export const Header = ({ customHeader, isRefetching }: HeaderProps) => {
{customHeader ? (
customHeader
) : snap.selectedRows.size > 0 ? (
<RowHeader />
<RowHeader tableQueriesEnabled={tableQueriesEnabled} />
) : (
<DefaultHeader />
)}
@@ -224,7 +225,12 @@ const DefaultHeader = () => {
)
}
const RowHeader = () => {
type RowHeaderProps = {
tableQueriesEnabled?: boolean
}
const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
debugger
const { data: project } = useSelectedProjectQuery()
const tableEditorSnap = useTableEditorStateSnapshot()
const snap = useTableEditorTableStateSnapshot()
@@ -238,16 +244,19 @@ const RowHeader = () => {
const [isExporting, setIsExporting] = useState(false)
const [showExportModal, setShowExportModal] = useState(false)
const { data } = useTableRowsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
tableId: snap.table.id,
sorts,
filters,
page: snap.page,
limit: tableEditorSnap.rowsPerPage,
roleImpersonationState: roleImpersonationState as RoleImpersonationState,
})
const { data } = useTableRowsQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
tableId: snap.table.id,
sorts,
filters,
page: snap.page,
limit: tableEditorSnap.rowsPerPage,
roleImpersonationState: roleImpersonationState as RoleImpersonationState,
},
{ enabled: tableQueriesEnabled }
)
const { data: countData } = useTableRowsCountQuery(
{
@@ -258,7 +267,7 @@ const RowHeader = () => {
enforceExactCount: snap.enforceExactCount,
roleImpersonationState: roleImpersonationState as RoleImpersonationState,
},
{ keepPreviousData: true }
{ keepPreviousData: true, enabled: tableQueriesEnabled }
)
const allRows = data?.rows ?? []
@@ -391,7 +391,7 @@ export const SidePanelEditor = ({
payload: {
name: string
schema: string
comment?: string | undefined
comment?: string | null
},
columns: ColumnField[],
foreignKeyRelations: ForeignKey[],
@@ -374,7 +374,7 @@ export const updateColumn = async ({
export const duplicateTable = async (
projectRef: string,
connectionString: string | undefined | null,
payload: { name: string; comment?: string },
payload: { name: string; comment?: string | null },
metadata: {
duplicateTable: RetrieveTableResult
isRLSEnabled: boolean
@@ -470,7 +470,7 @@ export const createTable = async ({
payload: {
name: string
schema: string
comment?: string | undefined
comment?: string | null
}
columns: ColumnField[]
foreignKeyRelations: ForeignKey[]
@@ -56,7 +56,7 @@ export interface TableEditorProps {
payload: {
name: string
schema: string
comment?: string | undefined
comment?: string | null
},
columns: ColumnField[],
foreignKeyRelations: ForeignKey[],
@@ -14,9 +14,16 @@ export function getTableEditorSql(id?: number) {
c.relforcerowsecurity as rls_forced,
c.relreplident,
c.relowner,
obj_description(c.oid) as comment
obj_description(c.oid) as comment,
fs.srvname as foreign_server_name,
fdw.fdwname as foreign_data_wrapper_name,
fdw_handler.proname as foreign_data_wrapper_handler
from pg_class c
join pg_namespace nc on nc.oid = c.relnamespace
left join pg_foreign_table ft on ft.ftrelid = c.oid
left join pg_foreign_server fs on fs.oid = ft.ftserver
left join pg_foreign_data_wrapper fdw on fdw.oid = fs.srvfdw
left join pg_proc fdw_handler on fdw.fdwhandler = fdw_handler.oid
where c.oid = ${id}
and not pg_is_other_temp_schema(nc.oid)
and (
@@ -252,6 +259,9 @@ export function getTableEditorSql(id?: number) {
'schema', b.schema,
'name', b.name,
'comment', b.comment,
'foreign_server_name', b.foreign_server_name,
'foreign_data_wrapper_name', b.foreign_data_wrapper_name,
'foreign_data_wrapper_handler', b.foreign_data_wrapper_handler,
'columns', coalesce(c.columns, '[]'::jsonb)
)
end as entity
@@ -5,6 +5,7 @@ import type {
PostgresTable,
PostgresView,
} from '@supabase/postgres-meta'
import { WRAPPER_HANDLERS } from 'components/interfaces/Integrations/Wrappers/Wrappers.constants'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
interface TableRelationship extends PostgresRelationship {
@@ -40,6 +41,9 @@ export interface ForeignTable {
schema: string
name: string
comment: string | null
foreign_server_name: string
foreign_data_wrapper_name: string
foreign_data_wrapper_handler: string
columns: PostgresColumn[]
}
@@ -67,6 +71,10 @@ export function isForeignTable(entity?: Entity): entity is ForeignTable {
return entity?.entity_type === ENTITY_TYPE.FOREIGN_TABLE
}
export function isMsSqlForeignTable(entity?: Entity): entity is ForeignTable {
return isForeignTable(entity) && entity.foreign_data_wrapper_handler === WRAPPER_HANDLERS.MSSQL
}
export function isView(entity?: Entity): entity is View {
return entity?.entity_type === ENTITY_TYPE.VIEW
}
@@ -6,6 +6,7 @@ import { IS_PLATFORM } from 'common'
import { parseSupaTable } from 'components/grid/SupabaseGrid.utils'
import { Filter, Sort, SupaRow, SupaTable } from 'components/grid/types'
import { prefetchTableEditor } from 'data/table-editor/table-editor-query'
import { isMsSqlForeignTable } from 'data/table-editor/table-editor-types'
import {
ROLE_IMPERSONATION_NO_RESULTS,
RoleImpersonationState,
@@ -248,8 +249,27 @@ export async function getTableRows(
const table = parseSupaTable(entity)
const equalityFilterColumns = filters
?.filter((filter) => filter.operator === '=' || filter.operator === 'is')
.flatMap((filter) => filter.column)
// There is an edge case for MS SQL foreign tables, where the Postgres query
// planner may drop sorts that are redundant with filters, resulting in
// invalid MS SQL syntax. To prevent this, we exclude potentially conflicting
// columns from potential default sort columns.
const excludedColumns = isMsSqlForeignTable(entity)
? Array.from(new Set(equalityFilterColumns))
: undefined
const sql = wrapWithRoleImpersonation(
getTableRowsSql({ table: entity, filters, sorts, limit, page }),
getTableRowsSql({
table: entity,
filters,
sorts,
limit,
page,
sortExcludedColumns: excludedColumns,
}),
roleImpersonationState
)
@@ -9,7 +9,7 @@ import { tableKeys } from './keys'
export type CreateTableBody = {
name: string
schema?: string
comment?: string
comment?: string | null
}
export type TableCreateVariables = {
@@ -1,16 +1,19 @@
import { literal, ident } from './pg-format'
import { z } from 'zod'
import { DEFAULT_SYSTEM_SCHEMAS } from './constants'
import { filterByList, coalesceRowsToArray } from './helpers'
import { FOREIGN_TABLES_SQL } from './sql/foreign-tables'
import { COLUMNS_SQL } from './sql/columns'
import { coalesceRowsToArray, filterByList } from './helpers'
import { ident, literal } from './pg-format'
import { pgColumnArrayZod } from './pg-meta-columns'
import { COLUMNS_SQL } from './sql/columns'
import { FOREIGN_TABLES_SQL } from './sql/foreign-tables'
export const pgForeignTableZod = z.object({
id: z.number(),
schema: z.string(),
name: z.string(),
comment: z.string().nullable(),
foreign_server_name: z.string(),
foreign_data_wrapper_name: z.string(),
foreign_data_wrapper_handler: z.string(),
columns: pgColumnArrayZod.optional(),
})
+12 -14
View File
@@ -1,10 +1,10 @@
import { z } from 'zod'
import { coalesceRowsToArray, filterByList } from './helpers'
import { TABLES_SQL } from './sql/tables'
import { COLUMNS_SQL } from './sql/columns'
import { DEFAULT_SYSTEM_SCHEMAS } from './constants'
import { coalesceRowsToArray, filterByList } from './helpers'
import { ident, literal } from './pg-format'
import { pgColumnArrayZod } from './pg-meta-columns'
import { COLUMNS_SQL } from './sql/columns'
import { TABLES_SQL } from './sql/tables'
const pgTablePrimaryKeyZod = z.object({
table_id: z.number(),
@@ -139,15 +139,14 @@ const generateEnrichedTablesSql = ({ includeColumns }: { includeColumns?: boolea
type TableCreateParams = {
name: string
schema?: string
comment?: string
comment?: string | null
}
function create({ name, schema = 'public', comment }: TableCreateParams): { sql: string } {
const tableSql = `CREATE TABLE ${ident(schema)}.${ident(name)} ();`
const commentSql =
comment === undefined
? ''
: `COMMENT ON TABLE ${ident(schema)}.${ident(name)} IS ${literal(comment)};`
const commentSql = comment
? `COMMENT ON TABLE ${ident(schema)}.${ident(name)} IS ${literal(comment)};`
: ''
const sql = `BEGIN; ${tableSql} ${commentSql} COMMIT;`
return { sql }
}
@@ -160,7 +159,7 @@ type TableUpdateParams = {
replica_identity?: 'DEFAULT' | 'INDEX' | 'FULL' | 'NOTHING'
replica_identity_index?: string
primary_keys?: Array<{ name: string }>
comment?: string
comment?: string | null
}
function update(
@@ -231,10 +230,9 @@ $$;
.join(',')});`
}
}
const commentSql =
comment === undefined
? ''
: `COMMENT ON TABLE ${ident(old.schema)}.${ident(old.name)} IS ${literal(comment)};`
const commentSql = !!comment
? `COMMENT ON TABLE ${ident(old.schema)}.${ident(old.name)} IS ${literal(comment)};`
: ''
// nameSql must be last, right below schemaSql
const sql = `
BEGIN;
@@ -250,4 +248,4 @@ COMMIT;`
return { sql }
}
export { list, retrieve, remove, create, update }
export { create, list, remove, retrieve, update }
+21 -5
View File
@@ -21,6 +21,10 @@ export interface BuildTableRowsQueryArgs {
page?: number
maxCharacters?: number
maxArraySize?: number
/**
* Columns that should not be used for default sorting
*/
sortExcludedColumns?: string[]
}
// Text and JSON types that should be truncated
@@ -71,13 +75,22 @@ const LARGE_COLUMNS_TYPES_SET = new Set(LARGE_COLUMNS_TYPES)
export const THRESHOLD_COUNT = 100000
// Return the primary key columns if exists, otherwise return the first column to use as a default sort
export const getDefaultOrderByColumns = (table: Pick<PGTable, 'primary_keys' | 'columns'>) => {
export const getDefaultOrderByColumns = (
table: Pick<PGTable, 'primary_keys' | 'columns'>,
{ excludedColumns = [] }: { excludedColumns?: string[] } = {}
) => {
const primaryKeyColumns = table.primary_keys?.map((pk) => pk.name)
if (primaryKeyColumns && primaryKeyColumns.length > 0) {
if (
primaryKeyColumns &&
primaryKeyColumns.length > 0 &&
!primaryKeyColumns.every((col) => excludedColumns.includes(col))
) {
return primaryKeyColumns
}
if (table.columns && table.columns.length > 0) {
const eligibleColumnsForSorting = table.columns.filter((x) => !x.data_type.includes('json'))
const eligibleColumnsForSorting = table.columns.filter(
(x) => !x.data_type.includes('json') && !excludedColumns.includes(x.name)
)
if (eligibleColumnsForSorting.length > 0) return [eligibleColumnsForSorting[0].name]
else return []
}
@@ -111,6 +124,7 @@ export const getTableRowsSql = ({
limit,
maxCharacters = MAX_CHARACTERS,
maxArraySize = MAX_ARRAY_SIZE,
sortExcludedColumns = [],
}: BuildTableRowsQueryArgs) => {
if (!table || !table.columns) return ``
@@ -129,11 +143,13 @@ export const getTableRowsSql = ({
)
})
// If sorts is empty and table row count is within threshold, use the primary key as the default sort
// If sorts is empty and table row count is within threshold, use the primary key as the default sort.
// Only apply for selections over a Table, not View, MaterializedViews, ...
const liveRowCount = (table as PGTable).live_rows_estimate || 0
if (sorts.length === 0 && liveRowCount <= THRESHOLD_COUNT && table.columns.length > 0) {
const defaultOrderByColumns = getDefaultOrderByColumns(table as PGTable)
const defaultOrderByColumns = getDefaultOrderByColumns(table as PGTable, {
excludedColumns: sortExcludedColumns,
})
if (defaultOrderByColumns.length > 0) {
defaultOrderByColumns.forEach((col) => {
queryChains = queryChains.order(table.name, col)
+8 -1
View File
@@ -3,10 +3,17 @@ select
c.oid::int8 as id,
n.nspname as schema,
c.relname as name,
obj_description(c.oid) as comment
obj_description(c.oid) as comment,
fs.srvname as foreign_server_name,
fdw.fdwname as foreign_data_wrapper_name,
handler.proname as foreign_data_wrapper_handler
from
pg_class c
join pg_namespace n on n.oid = c.relnamespace
inner join pg_foreign_table ft on ft.ftrelid = c.oid
inner join pg_foreign_server fs on fs.oid = ft.ftserver
inner join pg_foreign_data_wrapper fdw on fdw.oid = fs.srvfdw
inner join pg_proc handler on handler.oid = fdw.fdwhandler
where
c.relkind = 'f'
`
+1 -1
View File
@@ -43,7 +43,7 @@ FROM
join pg_class c on i.indrelid = c.oid
join pg_namespace n on c.relnamespace = n.oid
join pg_attribute a on a.attrelid = c.oid and a.attnum = any(i.indkey)
where
where
i.indisprimary
group by c.oid
) as pk
+14 -2
View File
@@ -1,6 +1,6 @@
import { expect, test, beforeAll, afterAll } from 'vitest'
import { afterAll, beforeAll, expect, test } from 'vitest'
import pgMeta from '../src/index'
import { createTestDatabase, cleanupRoot } from './db/utils'
import { cleanupRoot, createTestDatabase } from './db/utils'
beforeAll(async () => {
// Any global setup if needed
@@ -98,6 +98,9 @@ withTestDatabase('list foreign tables', async ({ executeQuery }) => {
},
],
"comment": null,
"foreign_data_wrapper_handler": "postgres_fdw_handler",
"foreign_data_wrapper_name": "postgres_fdw",
"foreign_server_name": "foreign_server",
"id": Any<Number>,
"name": "foreign_table",
"schema": "public",
@@ -115,6 +118,9 @@ withTestDatabase('list foreign tables without columns', async ({ executeQuery })
`
{
"comment": null,
"foreign_data_wrapper_handler": "postgres_fdw_handler",
"foreign_data_wrapper_name": "postgres_fdw",
"foreign_server_name": "foreign_server",
"id": Any<Number>,
"name": "foreign_table",
"schema": "public",
@@ -199,6 +205,9 @@ withTestDatabase('retrieve foreign table by name', async ({ executeQuery }) => {
},
],
"comment": null,
"foreign_data_wrapper_handler": "postgres_fdw_handler",
"foreign_data_wrapper_name": "postgres_fdw",
"foreign_server_name": "foreign_server",
"id": Any<Number>,
"name": "foreign_table",
"schema": "public",
@@ -282,6 +291,9 @@ withTestDatabase('retrieve foreign table by id', async ({ executeQuery }) => {
},
],
"comment": null,
"foreign_data_wrapper_handler": "postgres_fdw_handler",
"foreign_data_wrapper_name": "postgres_fdw",
"foreign_server_name": "foreign_server",
"id": Any<Number>,
"name": "foreign_table",
"schema": "public",
@@ -34,6 +34,29 @@ describe('Table Row Query', () => {
})
expect(result).toEqual([])
})
test('should exclude specified columns when determining default sort', () => {
const table = {
primary_keys: [{ name: 'id' }],
columns: [
{
name: 'id',
data_type: 'integer',
format: 'int4',
ordinal_position: 1,
},
{
name: 'name',
data_type: 'text',
format: 'text',
ordinal_position: 2,
},
],
} as any
const result = getDefaultOrderByColumns(table, { excludedColumns: ['id'] })
expect(result).toEqual(['name'])
})
})
describe('getTableRowsSql', () => {
+2 -3
View File
@@ -1,6 +1,6 @@
import { expect, test, beforeAll, afterAll } from 'vitest'
import { afterAll, beforeAll, expect, test } from 'vitest'
import pgMeta from '../src/index'
import { createTestDatabase, cleanupRoot } from './db/utils'
import { cleanupRoot, createTestDatabase } from './db/utils'
beforeAll(async () => {
// Any global setup if needed
@@ -1427,7 +1427,6 @@ for (const testCase of tableCreationTests) {
await testCase.beforeTest(executeQuery)
}
//@ts-expect-error
const { sql } = pgMeta.tables.create(testCase.input)
await executeQuery(sql)