Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/postgrest-typegen/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ Hard split between **introspection** and **generation**:
(`pg_proc.prosqlbody`) and reads the function comment's `@notNull` lines (see
"View column nullability" and "`RETURNS TABLE` column nullability" in the
README). It reads only the join tree and range table, never `varnullingrels`,
so one code path serves PostgreSQL 15 and later.
so one code path serves PostgreSQL 14 and later. PostgreSQL 14 names range
table entries `RTE` where later versions write `RANGETBLENTRY`.
- `src/generation/` -- `generateTypescript` / `generateGo` / `generatePython`.
Pure functions: `GeneratorMetadata` in, the complete source file (ending in a
single newline) out. No database access.
Expand Down Expand Up @@ -150,3 +151,10 @@ extend `sortGeneratorMetadata` instead.
- Generation unit tests: pure, no Docker, fixture-builder + per-language inline
snapshots (`src/` and `test/generation/`).
- Introspection/parity tests: `bun:test` + testcontainers PostgreSQL.
- PostgreSQL 14 is the minimum supported version. The parity test runs on it and
on PostgreSQL 15 against the same goldens, so every introspection query has to
run on 14 and produce the same metadata there. The version-specific suites
(view and `RETURNS TABLE` nullability) include 14 as well. Raising the minimum
changes `MINIMUM_POSTGRES_VERSION` in `test/parity/parity.test.ts`, the
version lists of those suites and "Supported PostgreSQL versions" in the
README together.
6 changes: 6 additions & 0 deletions packages/postgrest-typegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,12 @@ npm install pg
npm install oxfmt@0.66.0
```

## Supported PostgreSQL versions

`introspect()` supports PostgreSQL 14 and later, the same versions the Supabase
CLI accepts as `db.major_version`. View and `RETURNS TABLE` column nullability
are inferred the same way on every supported version.

## postgrest-js type tests

Every pull request that touches this package also runs postgrest-js's type tests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export interface QueryNullability {
* joins, subqueries, non-recursive CTEs and `GROUP BY`, and nothing on the way
* puts it on the nullable side of an outer join, in a set operation or under
* grouping sets. The outer-join check walks the join tree instead of reading
* `varnullingrels`, which PostgreSQL 15 does not have.
* `varnullingrels`, which PostgreSQL 14 and 15 do not have.
*/
export function queryNullability(
rows: ViewDefinitionRow[],
Expand Down Expand Up @@ -152,7 +152,7 @@ export function queryNullability(
return false;
}
const entry = list(query.fields.rtable)[varno - 1];
if (!isNode(entry, "RANGETBLENTRY")) {
if (!(isNode(entry, "RANGETBLENTRY") || isNode(entry, "RTE"))) {
return false;
}
switch (entry.fields.rtekind) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const resultNullability = (metadata: GeneratorMetadata, name: string) =>
).map((column) => [column.name, column.is_nullable]),
);

describe.each(["15", "17", "18"])(
describe.each(["14", "15", "17", "18"])(
"RETURNS TABLE column nullability (PostgreSQL %s)",
(version) => {
let container: StartedPostgreSqlContainer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import { introspect } from "../../src/introspection/index.ts";
import type { GeneratorMetadata } from "../../src/types.ts";

/**
* Integration coverage for view column nullability. PostgreSQL 15 has no
* `varnullingrels`, PostgreSQL 18 routes grouped columns through a group range
* table entry, and PostgreSQL 17 is what Supabase projects run, so each of them
* must resolve the same columns as non-null.
* Integration coverage for view column nullability. PostgreSQL 14 names range
* table entries `RTE`, PostgreSQL 14 and 15 have no `varnullingrels`,
* PostgreSQL 18 routes grouped columns through a group range table entry, and
* PostgreSQL 17 is what Supabase projects run, so each of them must resolve the
* same columns as non-null.
*/
const SCHEMA = /* SQL */ `
create schema private;
Expand Down Expand Up @@ -71,7 +72,7 @@ const nullability = (metadata: GeneratorMetadata, view: string) =>
.map((column) => [column.name, column.is_nullable]),
);

describe.each(["15", "17", "18"])(
describe.each(["14", "15", "17", "18"])(
"view column nullability (PostgreSQL %s)",
(version) => {
let container: StartedPostgreSqlContainer;
Expand Down
73 changes: 42 additions & 31 deletions packages/postgrest-typegen/test/parity/parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,39 +39,50 @@ const FIXTURE_DIR = join(import.meta.dir, "..", "introspection", "fixtures");
const EXPECTED_DIR = join(import.meta.dir, "expected");
const golden = (name: string) => readFileSync(join(EXPECTED_DIR, name), "utf8");

let container: StartedPostgreSqlContainer;
let pool: Pool;
let metadata: GeneratorMetadata;
/**
* PostgreSQL 14 is the oldest version this package supports, matching the
* oldest `db.major_version` the Supabase CLI accepts. The goldens hold on it
* as well, so an introspection query that reads a newer catalog column, or a
* query tree walk that misreads its node trees, fails here.
*/
const MINIMUM_POSTGRES_VERSION = "14";

describe.each([MINIMUM_POSTGRES_VERSION, "15"])(
"generator parity vs postgres-meta CLI (PostgreSQL %s)",
(version) => {
let container: StartedPostgreSqlContainer;
let pool: Pool;
let metadata: GeneratorMetadata;

beforeAll(async () => {
container = await new PostgreSqlContainer("postgres:15-alpine")
.withUsername("postgres")
.withPassword("postgres")
.withDatabase("postgres")
.withWaitStrategy(Wait.forHealthCheck())
.withStartupTimeout(120_000)
.start();
pool = new Pool({ connectionString: container.getConnectionUri() });
await pool.query(readFileSync(join(FIXTURE_DIR, "00-init.sql"), "utf8"));
await pool.query(readFileSync(join(FIXTURE_DIR, "01-memes.sql"), "utf8"));
metadata = sortGeneratorMetadata(await introspect(pool));
}, 180_000);
beforeAll(async () => {
container = await new PostgreSqlContainer(`postgres:${version}-alpine`)
.withUsername("postgres")
.withPassword("postgres")
.withDatabase("postgres")
.withWaitStrategy(Wait.forHealthCheck())
.withStartupTimeout(120_000)
.start();
pool = new Pool({ connectionString: container.getConnectionUri() });
await pool.query(readFileSync(join(FIXTURE_DIR, "00-init.sql"), "utf8"));
await pool.query(readFileSync(join(FIXTURE_DIR, "01-memes.sql"), "utf8"));
metadata = sortGeneratorMetadata(await introspect(pool));
}, 180_000);

afterAll(async () => {
await pool?.end();
await container?.stop();
});
afterAll(async () => {
await pool?.end();
await container?.stop();
});

describe("generator parity vs postgres-meta CLI", () => {
test("typescript", async () => {
expect(await generateTypescript(metadata)).toBe(golden("typescript.txt"));
});
test("typescript", async () => {
expect(await generateTypescript(metadata)).toBe(golden("typescript.txt"));
});

test("go", () => {
expect(generateGo(metadata)).toBe(golden("go.txt"));
});
test("go", () => {
expect(generateGo(metadata)).toBe(golden("go.txt"));
});

test("python", () => {
expect(generatePython(metadata)).toBe(golden("python.txt"));
});
});
test("python", () => {
expect(generatePython(metadata)).toBe(golden("python.txt"));
});
},
);
Loading