From ef9cb42fdb81427a2130da6569e6aa9709924f4e Mon Sep 17 00:00:00 2001 From: Vignesh Date: Fri, 12 Jun 2026 01:37:55 +0530 Subject: [PATCH 1/3] fix(postgrest): add typed column inference for order() with referencedTable When referencedTable or foreignTable is a known table/view, the column param is now constrained to that table's Row. Provides autocomplete and compile-time error checking. Closes #971 --- .../src/PostgrestTransformBuilder.ts | 36 ++++++- .../core/postgrest-js/test/index.test-d.ts | 94 ++++++++++++++----- 2 files changed, 105 insertions(+), 25 deletions(-) diff --git a/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts b/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts index e37bd16662..ca6579a8ae 100644 --- a/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts +++ b/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts @@ -4,6 +4,7 @@ import { GetResult } from './select-query-parser/result' import { CheckMatchingArrayTypes } from './types/types' import { ClientServerOptions, GenericSchema } from './types/common/common' import type { MaxAffectedEnabled } from './types/feature-flags' +import type { TablesAndViews } from './select-query-parser/types' export default class PostgrestTransformBuilder< ClientOptions extends ClientServerOptions, @@ -156,9 +157,22 @@ export default class PostgrestTransformBuilder< column: ColumnName, options?: { ascending?: boolean; nullsFirst?: boolean; referencedTable?: undefined } ): this - order( + order< + ReferencedTable extends string & keyof TablesAndViews, + ColumnName extends string & keyof TablesAndViews[ReferencedTable]['Row'], + >( + column: ColumnName, + options: { ascending?: boolean; nullsFirst?: boolean; referencedTable: ReferencedTable } + ): this + order( column: string, - options?: { ascending?: boolean; nullsFirst?: boolean; referencedTable?: string } + options?: { + ascending?: boolean + nullsFirst?: boolean + referencedTable?: ReferencedTable extends keyof TablesAndViews + ? never + : ReferencedTable + } ): this /** * @deprecated Use `options.referencedTable` instead of `options.foreignTable` @@ -170,9 +184,23 @@ export default class PostgrestTransformBuilder< /** * @deprecated Use `options.referencedTable` instead of `options.foreignTable` */ - order( + order< + ReferencedTable extends string & keyof TablesAndViews, + ColumnName extends string & keyof TablesAndViews[ReferencedTable]['Row'], + >( + column: ColumnName, + options: { ascending?: boolean; nullsFirst?: boolean; foreignTable: ReferencedTable } + ): this + /** + * @deprecated Use `options.referencedTable` instead of `options.foreignTable` + */ + order( column: string, - options?: { ascending?: boolean; nullsFirst?: boolean; foreignTable?: string } + options?: { + ascending?: boolean + nullsFirst?: boolean + foreignTable?: ReferencedTable extends keyof TablesAndViews ? never : ReferencedTable + } ): this /** * Order the query result by `column`. diff --git a/packages/core/postgrest-js/test/index.test-d.ts b/packages/core/postgrest-js/test/index.test-d.ts index b8fed5a5ac..df83a69da5 100644 --- a/packages/core/postgrest-js/test/index.test-d.ts +++ b/packages/core/postgrest-js/test/index.test-d.ts @@ -10,27 +10,28 @@ const REST_URL = 'http://localhost:54321/rest/v1' const postgrest = new PostgrestClient(REST_URL) const postgrestWithOptions = new PostgrestClient(REST_URL) -type WithThrowOnError = T extends PostgrestFilterBuilder< - infer ClientOptions, - infer Schema, - infer Row, - infer Result, - infer RelationName, - infer Relationships, - infer Method, - boolean -> - ? PostgrestFilterBuilder< - ClientOptions, - Schema, - Row, - Result, - RelationName, - Relationships, - Method, - true - > - : never +type WithThrowOnError = + T extends PostgrestFilterBuilder< + infer ClientOptions, + infer Schema, + infer Row, + infer Result, + infer RelationName, + infer Relationships, + infer Method, + boolean + > + ? PostgrestFilterBuilder< + ClientOptions, + Schema, + Row, + Result, + RelationName, + Relationships, + Method, + true + > + : never // table and view name type safety { @@ -561,3 +562,54 @@ type WithThrowOnError = T extends PostgrestFilterBuilder< expectType>(true) } } + +// `.order()` with `referencedTable` provides typed column names from the referenced table (#971) +{ + // Base case: order by parent table columns still works + postgrest.from('users').select('messages(*)').order('username') + + // referencedTable provides autocomplete for the referenced table's columns + postgrest + .from('users') + .select('messages(*)') + .order('channel_id', { referencedTable: 'messages', ascending: false }) + + // Also works with other valid columns on the referenced table + postgrest + .from('users') + .select('messages(*)') + .order('message', { referencedTable: 'messages', ascending: false }) + + // Invalid column on a known referenced table should error + postgrest + .from('users') + .select('messages(*)') + // @ts-expect-error No overload matches this call. + .order('nonexistent_column', { referencedTable: 'messages', ascending: false }) + + // Deprecated foreignTable also provides typed column names + postgrest + .from('users') + .select('messages(*)') + .order('channel_id', { foreignTable: 'messages', ascending: false }) + + // Invalid column on a known referenced table should error (foreignTable) + postgrest + .from('users') + .select('messages(*)') + // @ts-expect-error No overload matches this call. + .order('nonexistent_column', { foreignTable: 'messages', ascending: false }) + + // Ordering by a column on 'channels' table works + postgrest + .from('messages') + .select('channels(*)') + .order('slug', { referencedTable: 'channels', ascending: true }) + + // Invalid column on channels should error + postgrest + .from('messages') + .select('channels(*)') + // @ts-expect-error No overload matches this call. + .order('bad_col', { referencedTable: 'channels', ascending: true }) +} From d8dccf035f0bd06c56e59cb90aade718bcf31358 Mon Sep 17 00:00:00 2001 From: Vignesh Date: Wed, 29 Jul 2026 02:48:10 +0530 Subject: [PATCH 2/3] fix(postgrest): allow dynamic string columns with typed referencedTable Use conditional type in order() overloads so that: - Valid column literals get autocomplete and type checking - Wide string types (from variables) are accepted - Invalid column literals still produce compile-time errors This fixes the regression where a non-literal column (e.g. from a prop or user selection) with a known referencedTable would fail with 'No overload matches this call.' Addresses reviewer feedback on #2445. --- .../src/PostgrestTransformBuilder.ts | 22 ++++++++++--------- .../core/postgrest-js/test/index.test-d.ts | 13 +++++++++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts b/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts index ca6579a8ae..617e4361b0 100644 --- a/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts +++ b/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts @@ -157,11 +157,12 @@ export default class PostgrestTransformBuilder< column: ColumnName, options?: { ascending?: boolean; nullsFirst?: boolean; referencedTable?: undefined } ): this - order< - ReferencedTable extends string & keyof TablesAndViews, - ColumnName extends string & keyof TablesAndViews[ReferencedTable]['Row'], - >( - column: ColumnName, + order, Column extends string>( + column: Column extends keyof TablesAndViews[ReferencedTable]['Row'] + ? Column + : string extends Column + ? string + : keyof TablesAndViews[ReferencedTable]['Row'] & string, options: { ascending?: boolean; nullsFirst?: boolean; referencedTable: ReferencedTable } ): this order( @@ -184,11 +185,12 @@ export default class PostgrestTransformBuilder< /** * @deprecated Use `options.referencedTable` instead of `options.foreignTable` */ - order< - ReferencedTable extends string & keyof TablesAndViews, - ColumnName extends string & keyof TablesAndViews[ReferencedTable]['Row'], - >( - column: ColumnName, + order, Column extends string>( + column: Column extends keyof TablesAndViews[ReferencedTable]['Row'] + ? Column + : string extends Column + ? string + : keyof TablesAndViews[ReferencedTable]['Row'] & string, options: { ascending?: boolean; nullsFirst?: boolean; foreignTable: ReferencedTable } ): this /** diff --git a/packages/core/postgrest-js/test/index.test-d.ts b/packages/core/postgrest-js/test/index.test-d.ts index df83a69da5..de50311488 100644 --- a/packages/core/postgrest-js/test/index.test-d.ts +++ b/packages/core/postgrest-js/test/index.test-d.ts @@ -612,4 +612,17 @@ type WithThrowOnError = .select('channels(*)') // @ts-expect-error No overload matches this call. .order('bad_col', { referencedTable: 'channels', ascending: true }) + + // Dynamic (non-literal) column with a known referencedTable should compile + const sortColumn: string = 'channel_id' + postgrest + .from('users') + .select('messages(*)') + .order(sortColumn, { referencedTable: 'messages', ascending: false }) + + // Dynamic column with deprecated foreignTable should also compile + postgrest + .from('users') + .select('messages(*)') + .order(sortColumn, { foreignTable: 'messages', ascending: false }) } From 76b36759c4acaa0a94e8f0f138fa709df72cc315 Mon Sep 17 00:00:00 2001 From: Vignesh Date: Wed, 29 Jul 2026 19:34:19 +0530 Subject: [PATCH 3/3] fix(postgrest): extract OrderColumnForTable helper type, add alias docs and tests - Factor the conditional column type into a shared OrderColumnForTable helper, reducing duplication across referencedTable and foreignTable overloads. - Add JSDoc note explaining that aliased referencedTable names that do not match a real table/view fall through to unchecked string, and that collisions with unrelated real tables should use a string variable to bypass checking. - Add tests for aliased referencedTable (unknown alias falls through to string overload). Addresses reviewer feedback on #2445. --- .../src/PostgrestTransformBuilder.ts | 37 +++++++++++++------ .../core/postgrest-js/test/index.test-d.ts | 13 +++++++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts b/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts index 617e4361b0..b9be64ee69 100644 --- a/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts +++ b/packages/core/postgrest-js/src/PostgrestTransformBuilder.ts @@ -6,6 +6,23 @@ import { ClientServerOptions, GenericSchema } from './types/common/common' import type { MaxAffectedEnabled } from './types/feature-flags' import type { TablesAndViews } from './select-query-parser/types' +/** + * Resolves the column type for `order()` when a known `referencedTable` is provided. + * + * - If `Column` is a valid column of the referenced table → accepts it (autocomplete works) + * - If `Column` is a wide `string` type (e.g. from a variable) → accepts it (dynamic usage) + * - If `Column` is an invalid string literal → constrains to valid columns (compile error) + */ +type OrderColumnForTable< + Schema extends GenericSchema, + ReferencedTable extends string, + Column extends string, +> = Column extends keyof TablesAndViews[ReferencedTable]['Row'] + ? Column + : string extends Column + ? string + : keyof TablesAndViews[ReferencedTable]['Row'] & string + export default class PostgrestTransformBuilder< ClientOptions extends ClientServerOptions, Schema extends GenericSchema, @@ -148,7 +165,13 @@ export default class PostgrestTransformBuilder< * @param options.nullsFirst - If `true`, `null`s appear first. If `false`, * `null`s appear last. * @param options.referencedTable - Set this to order a referenced table by - * its columns + * its columns. When `referencedTable` matches a known table or view in the + * schema, column names are validated at compile time. If the embedded relation + * uses an alias (e.g. `.select('archived:messages(*)')`) and the alias does + * not coincide with a real table name, it falls through to the unchecked + * `string` overload. If the alias happens to match a different real table, + * columns will be validated against that unrelated table — use a plain + * `string` variable for the column in that case to bypass checking. * * @category Database * @subcategory Using modifiers @@ -158,11 +181,7 @@ export default class PostgrestTransformBuilder< options?: { ascending?: boolean; nullsFirst?: boolean; referencedTable?: undefined } ): this order, Column extends string>( - column: Column extends keyof TablesAndViews[ReferencedTable]['Row'] - ? Column - : string extends Column - ? string - : keyof TablesAndViews[ReferencedTable]['Row'] & string, + column: OrderColumnForTable, options: { ascending?: boolean; nullsFirst?: boolean; referencedTable: ReferencedTable } ): this order( @@ -186,11 +205,7 @@ export default class PostgrestTransformBuilder< * @deprecated Use `options.referencedTable` instead of `options.foreignTable` */ order, Column extends string>( - column: Column extends keyof TablesAndViews[ReferencedTable]['Row'] - ? Column - : string extends Column - ? string - : keyof TablesAndViews[ReferencedTable]['Row'] & string, + column: OrderColumnForTable, options: { ascending?: boolean; nullsFirst?: boolean; foreignTable: ReferencedTable } ): this /** diff --git a/packages/core/postgrest-js/test/index.test-d.ts b/packages/core/postgrest-js/test/index.test-d.ts index de50311488..9e4b941635 100644 --- a/packages/core/postgrest-js/test/index.test-d.ts +++ b/packages/core/postgrest-js/test/index.test-d.ts @@ -625,4 +625,17 @@ type WithThrowOnError = .from('users') .select('messages(*)') .order(sortColumn, { foreignTable: 'messages', ascending: false }) + + // Aliased referencedTable that does NOT match a real table name + // falls through to the unchecked string overload (any column name accepted) + postgrest + .from('users') + .select('archived:messages(*)') + .order('anything_goes', { referencedTable: 'archived', ascending: false }) + + // Aliased referencedTable with deprecated foreignTable also falls through + postgrest + .from('users') + .select('archived:messages(*)') + .order('anything_goes', { foreignTable: 'archived', ascending: false }) }