diff --git a/.github/workflows/validate-python-types.yml b/.github/workflows/validate-python-types.yml new file mode 100644 index 00000000..1e5809b1 --- /dev/null +++ b/.github/workflows/validate-python-types.yml @@ -0,0 +1,71 @@ +name: Validate Python Type Generation + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + validate-python-types: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Python dependencies + run: | + pip install pydantic mypy + + - name: Start test database + working-directory: test/db + run: | + docker compose up -d --wait + + - name: Wait for database to be ready + run: | + # Install PostgreSQL client for health check + sudo apt-get update && sudo apt-get install -y postgresql-client + until pg_isready -h localhost -p 5432 -U postgres; do + echo "Waiting for database..." + sleep 1 + done + echo "Database is ready!" + + - name: Generate Python types + id: generate-types + run: | + node --loader ts-node/esm scripts/generate-python-types-test.ts > generated_types.py + echo "Generated Python types (first 30 lines):" + head -30 generated_types.py + + - name: Validate Python types runtime + run: | + python -c "import generated_types; print('✓ Generated Python types are valid and can be imported')" + + - name: Validate Python types with mypy + run: | + mypy generated_types.py --strict + + - name: Cleanup + if: always() + working-directory: test/db + run: docker compose down diff --git a/scripts/generate-python-types-test.ts b/scripts/generate-python-types-test.ts new file mode 100644 index 00000000..45111f56 --- /dev/null +++ b/scripts/generate-python-types-test.ts @@ -0,0 +1,44 @@ +#!/usr/bin/env node + +/** + * Script to generate Python types for CI validation + * This script uses the test database setup to generate Python types + */ + +import { build } from '../src/server/app.js' + +const TEST_CONNECTION_STRING = 'postgresql://postgres:postgres@localhost:5432' + +async function generatePythonTypes() { + const app = build() + + try { + const response = await app.inject({ + method: 'GET', + url: '/generators/python', + headers: { + pg: TEST_CONNECTION_STRING, + }, + query: { + access_control: 'public', + }, + }) + + if (response.statusCode !== 200) { + console.error(`Failed to generate types: ${response.statusCode}`) + console.error(response.body) + process.exit(1) + } + + // Write to stdout so it can be captured + process.stdout.write(response.body) + } catch (error) { + console.error('Error generating Python types:', error) + process.exit(1) + } finally { + await app.close() + } +} + +generatePythonTypes() + diff --git a/test/db/00-init.sql b/test/db/00-init.sql index f2161591..8ddc77ba 100644 --- a/test/db/00-init.sql +++ b/test/db/00-init.sql @@ -9,7 +9,8 @@ CREATE TABLE public.users ( id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text, status user_status DEFAULT 'ACTIVE', - decimal numeric + decimal numeric, + user_uuid uuid DEFAULT gen_random_uuid() ); INSERT INTO public.users (name) diff --git a/test/lib/functions.ts b/test/lib/functions.ts index 9d6088b6..ce26e078 100644 --- a/test/lib/functions.ts +++ b/test/lib/functions.ts @@ -75,15 +75,15 @@ test('list set-returning function with single object limit', async () => { "definition": " SELECT * FROM public.users_audit WHERE user_id = user_row.id; ", - "id": 16506, + "id": 16507, "identity_argument_types": "user_row users", "is_set_returning_function": true, "language": "sql", "name": "get_user_audit_setof_single_row", "prorows": 1, "return_type": "SETOF users_audit", - "return_type_id": 16418, - "return_type_relation_id": 16416, + "return_type_id": 16419, + "return_type_relation_id": 16417, "schema": "public", "security_definer": false, }, @@ -118,15 +118,15 @@ test('list set-returning function with multiples definitions', async () => { "definition": " SELECT * FROM public.todos WHERE "user-id" = user_row.id; ", - "id": 16509, + "id": 16510, "identity_argument_types": "user_row users", "is_set_returning_function": true, "language": "sql", "name": "get_todos_setof_rows", "prorows": 1000, "return_type": "SETOF todos", - "return_type_id": 16404, - "return_type_relation_id": 16402, + "return_type_id": 16405, + "return_type_relation_id": 16403, "schema": "public", "security_definer": false, }, @@ -136,7 +136,7 @@ test('list set-returning function with multiples definitions', async () => { "has_default": false, "mode": "in", "name": "todo_row", - "type_id": 16404, + "type_id": 16405, }, ], "argument_types": "todo_row todos", @@ -153,15 +153,15 @@ test('list set-returning function with multiples definitions', async () => { "definition": " SELECT * FROM public.todos WHERE "user-id" = todo_row."user-id"; ", - "id": 16510, + "id": 16511, "identity_argument_types": "todo_row todos", "is_set_returning_function": true, "language": "sql", "name": "get_todos_setof_rows", "prorows": 1000, "return_type": "SETOF todos", - "return_type_id": 16404, - "return_type_relation_id": 16402, + "return_type_id": 16405, + "return_type_relation_id": 16403, "schema": "public", "security_definer": false, }, diff --git a/test/lib/tables.ts b/test/lib/tables.ts index 677204fc..a0dfbaad 100644 --- a/test/lib/tables.ts +++ b/test/lib/tables.ts @@ -117,6 +117,24 @@ test('list', async () => { "schema": "public", "table": "users", }, + { + "check": null, + "comment": null, + "data_type": "uuid", + "default_value": "gen_random_uuid()", + "enums": [], + "format": "uuid", + "identity_generation": null, + "is_generated": false, + "is_identity": false, + "is_nullable": true, + "is_unique": false, + "is_updatable": true, + "name": "user_uuid", + "ordinal_position": 5, + "schema": "public", + "table": "users", + }, ], "comment": null, "dead_rows_estimate": Any, diff --git a/test/lib/types.ts b/test/lib/types.ts index 349a1b80..b256697e 100644 --- a/test/lib/types.ts +++ b/test/lib/types.ts @@ -74,7 +74,7 @@ test('list types with include Table Types', async () => { "id": Any, "name": "todos", "schema": "public", - "type_relation_id": 16402, + "type_relation_id": 16403, } ` ) diff --git a/test/lib/views.ts b/test/lib/views.ts index 275eb4a3..e623e14b 100644 --- a/test/lib/views.ts +++ b/test/lib/views.ts @@ -15,7 +15,7 @@ test('list', async () => { "default_value": null, "enums": [], "format": "int8", - "id": "16423.1", + "id": "16424.1", "identity_generation": null, "is_generated": false, "is_identity": false, @@ -26,7 +26,7 @@ test('list', async () => { "ordinal_position": 1, "schema": "public", "table": "todos_view", - "table_id": 16423, + "table_id": 16424, }, { "check": null, @@ -35,7 +35,7 @@ test('list', async () => { "default_value": null, "enums": [], "format": "text", - "id": "16423.2", + "id": "16424.2", "identity_generation": null, "is_generated": false, "is_identity": false, @@ -46,7 +46,7 @@ test('list', async () => { "ordinal_position": 2, "schema": "public", "table": "todos_view", - "table_id": 16423, + "table_id": 16424, }, { "check": null, @@ -55,7 +55,7 @@ test('list', async () => { "default_value": null, "enums": [], "format": "int8", - "id": "16423.3", + "id": "16424.3", "identity_generation": null, "is_generated": false, "is_identity": false, @@ -66,7 +66,7 @@ test('list', async () => { "ordinal_position": 3, "schema": "public", "table": "todos_view", - "table_id": 16423, + "table_id": 16424, }, ], "comment": null, @@ -112,7 +112,7 @@ test('retrieve', async () => { "default_value": null, "enums": [], "format": "int8", - "id": "16423.1", + "id": "16424.1", "identity_generation": null, "is_generated": false, "is_identity": false, @@ -123,7 +123,7 @@ test('retrieve', async () => { "ordinal_position": 1, "schema": "public", "table": "todos_view", - "table_id": 16423, + "table_id": 16424, }, { "check": null, @@ -132,7 +132,7 @@ test('retrieve', async () => { "default_value": null, "enums": [], "format": "text", - "id": "16423.2", + "id": "16424.2", "identity_generation": null, "is_generated": false, "is_identity": false, @@ -143,7 +143,7 @@ test('retrieve', async () => { "ordinal_position": 2, "schema": "public", "table": "todos_view", - "table_id": 16423, + "table_id": 16424, }, { "check": null, @@ -152,7 +152,7 @@ test('retrieve', async () => { "default_value": null, "enums": [], "format": "int8", - "id": "16423.3", + "id": "16424.3", "identity_generation": null, "is_generated": false, "is_identity": false, @@ -163,7 +163,7 @@ test('retrieve', async () => { "ordinal_position": 3, "schema": "public", "table": "todos_view", - "table_id": 16423, + "table_id": 16424, }, ], "comment": null, diff --git a/test/server/indexes.ts b/test/server/indexes.ts index b3fb7f0c..1ad4d0a2 100644 --- a/test/server/indexes.ts +++ b/test/server/indexes.ts @@ -22,7 +22,7 @@ test('list indexes', async () => { 0, ], "comment": null, - "id": 16399, + "id": 16400, "index_attributes": [ { "attribute_name": "id", @@ -57,7 +57,7 @@ test('list indexes', async () => { }) test('retrieve index', async () => { - const res = await app.inject({ method: 'GET', path: '/indexes/16399' }) + const res = await app.inject({ method: 'GET', path: '/indexes/16400' }) const index = res.json() expect(index).toMatchInlineSnapshot( ` @@ -71,7 +71,7 @@ test('retrieve index', async () => { 0, ], "comment": null, - "id": 16399, + "id": 16400, "index_attributes": [ { "attribute_name": "id", diff --git a/test/server/query.ts b/test/server/query.ts index 9d6c0e1b..01f9cc92 100644 --- a/test/server/query.ts +++ b/test/server/query.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest' -import { app } from './utils' +import { app, normalizeUuids } from './utils' test('query', async () => { const res = await app.inject({ @@ -7,19 +7,21 @@ test('query', async () => { path: '/query', payload: { query: 'SELECT * FROM users' }, }) - expect(res.json()).toMatchInlineSnapshot(` + expect(normalizeUuids(res.json())).toMatchInlineSnapshot(` [ { "decimal": null, "id": 1, "name": "Joe Bloggs", "status": "ACTIVE", + "user_uuid": "00000000-0000-0000-0000-000000000000", }, { "decimal": null, "id": 2, "name": "Jane Doe", "status": "ACTIVE", + "user_uuid": "00000000-0000-0000-0000-000000000000", }, ] `) @@ -758,13 +760,14 @@ test('parameter binding with positional parameters', async () => { parameters: [1, 'ACTIVE'], }, }) - expect(res.json()).toMatchInlineSnapshot(` + expect(normalizeUuids(res.json())).toMatchInlineSnapshot(` [ { "decimal": null, "id": 1, "name": "Joe Bloggs", "status": "ACTIVE", + "user_uuid": "00000000-0000-0000-0000-000000000000", }, ] `) diff --git a/test/server/typegen.ts b/test/server/typegen.ts index 4e720277..d71b468e 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -307,6 +307,7 @@ test('typegen: typescript', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null test_unnamed_row_composite: | Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"] | null @@ -321,12 +322,14 @@ test('typegen: typescript', async () => { id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Update: { decimal?: number | null id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Relationships: [] } @@ -493,18 +496,21 @@ test('typegen: typescript', async () => { id: number | null name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null } Insert: { decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Update: { decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Relationships: [] } @@ -575,6 +581,7 @@ test('typegen: typescript', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null } SetofOptions: { from: "*" @@ -590,6 +597,7 @@ test('typegen: typescript', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null }[] SetofOptions: { from: "*" @@ -605,6 +613,7 @@ test('typegen: typescript', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null } SetofOptions: { from: "todos" @@ -889,6 +898,7 @@ test('typegen: typescript', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null }[] SetofOptions: { from: "*" @@ -1482,6 +1492,7 @@ test('typegen w/ one-to-one relationships', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null test_unnamed_row_composite: | Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"] | null @@ -1496,12 +1507,14 @@ test('typegen w/ one-to-one relationships', async () => { id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Update: { decimal?: number | null id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Relationships: [] } @@ -1680,18 +1693,21 @@ test('typegen w/ one-to-one relationships', async () => { id: number | null name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null } Insert: { decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Update: { decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Relationships: [] } @@ -1762,6 +1778,7 @@ test('typegen w/ one-to-one relationships', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null } SetofOptions: { from: "*" @@ -1777,6 +1794,7 @@ test('typegen w/ one-to-one relationships', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null }[] SetofOptions: { from: "*" @@ -1792,6 +1810,7 @@ test('typegen w/ one-to-one relationships', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null } SetofOptions: { from: "todos" @@ -2076,6 +2095,7 @@ test('typegen w/ one-to-one relationships', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null }[] SetofOptions: { from: "*" @@ -2669,6 +2689,7 @@ test('typegen: typescript w/ one-to-one relationships', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null test_unnamed_row_composite: | Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"] | null @@ -2683,12 +2704,14 @@ test('typegen: typescript w/ one-to-one relationships', async () => { id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Update: { decimal?: number | null id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Relationships: [] } @@ -2867,18 +2890,21 @@ test('typegen: typescript w/ one-to-one relationships', async () => { id: number | null name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null } Insert: { decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Update: { decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Relationships: [] } @@ -2949,6 +2975,7 @@ test('typegen: typescript w/ one-to-one relationships', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null } SetofOptions: { from: "*" @@ -2964,6 +2991,7 @@ test('typegen: typescript w/ one-to-one relationships', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null }[] SetofOptions: { from: "*" @@ -2979,6 +3007,7 @@ test('typegen: typescript w/ one-to-one relationships', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null } SetofOptions: { from: "todos" @@ -3263,6 +3292,7 @@ test('typegen: typescript w/ one-to-one relationships', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null }[] SetofOptions: { from: "*" @@ -3861,6 +3891,7 @@ test('typegen: typescript w/ postgrestVersion', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null test_unnamed_row_composite: | Database["public"]["CompositeTypes"]["composite_type_with_array_attribute"] | null @@ -3875,12 +3906,14 @@ test('typegen: typescript w/ postgrestVersion', async () => { id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Update: { decimal?: number | null id?: number name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Relationships: [] } @@ -4059,18 +4092,21 @@ test('typegen: typescript w/ postgrestVersion', async () => { id: number | null name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null } Insert: { decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Update: { decimal?: number | null id?: number | null name?: string | null status?: Database["public"]["Enums"]["user_status"] | null + user_uuid?: string | null } Relationships: [] } @@ -4141,6 +4177,7 @@ test('typegen: typescript w/ postgrestVersion', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null } SetofOptions: { from: "*" @@ -4156,6 +4193,7 @@ test('typegen: typescript w/ postgrestVersion', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null }[] SetofOptions: { from: "*" @@ -4171,6 +4209,7 @@ test('typegen: typescript w/ postgrestVersion', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null } SetofOptions: { from: "todos" @@ -4455,6 +4494,7 @@ test('typegen: typescript w/ postgrestVersion', async () => { id: number name: string | null status: Database["public"]["Enums"]["user_status"] | null + user_uuid: string | null }[] SetofOptions: { from: "*" @@ -5117,24 +5157,27 @@ test('typegen: go', async () => { "package database type PublicUsersSelect struct { - Decimal *float64 \`json:"decimal"\` - Id int64 \`json:"id"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` + Decimal *float64 \`json:"decimal"\` + Id int64 \`json:"id"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + UserUuid *string \`json:"user_uuid"\` } type PublicUsersInsert struct { - Decimal *float64 \`json:"decimal"\` - Id *int64 \`json:"id"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` + Decimal *float64 \`json:"decimal"\` + Id *int64 \`json:"id"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + UserUuid *string \`json:"user_uuid"\` } type PublicUsersUpdate struct { - Decimal *float64 \`json:"decimal"\` - Id *int64 \`json:"id"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` + Decimal *float64 \`json:"decimal"\` + Id *int64 \`json:"id"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + UserUuid *string \`json:"user_uuid"\` } type PublicTodosSelect struct { @@ -5349,10 +5392,11 @@ test('typegen: go', async () => { } type PublicUsersViewSelect struct { - Decimal *float64 \`json:"decimal"\` - Id *int64 \`json:"id"\` - Name *string \`json:"name"\` - Status *string \`json:"status"\` + Decimal *float64 \`json:"decimal"\` + Id *int64 \`json:"id"\` + Name *string \`json:"name"\` + Status *string \`json:"status"\` + UserUuid *string \`json:"user_uuid"\` } type PublicUserTodosSummaryViewSelect struct { @@ -5728,11 +5772,13 @@ test('typegen: swift', async () => { internal let id: Int64 internal let name: String? internal let status: UserStatus? + internal let userUuid: UUID? internal enum CodingKeys: String, CodingKey { case decimal = "decimal" case id = "id" case name = "name" case status = "status" + case userUuid = "user_uuid" } } internal struct UsersInsert: Codable, Hashable, Sendable, Identifiable { @@ -5740,11 +5786,13 @@ test('typegen: swift', async () => { internal let id: Int64? internal let name: String? internal let status: UserStatus? + internal let userUuid: UUID? internal enum CodingKeys: String, CodingKey { case decimal = "decimal" case id = "id" case name = "name" case status = "status" + case userUuid = "user_uuid" } } internal struct UsersUpdate: Codable, Hashable, Sendable, Identifiable { @@ -5752,11 +5800,13 @@ test('typegen: swift', async () => { internal let id: Int64? internal let name: String? internal let status: UserStatus? + internal let userUuid: UUID? internal enum CodingKeys: String, CodingKey { case decimal = "decimal" case id = "id" case name = "name" case status = "status" + case userUuid = "user_uuid" } } internal struct UsersAuditSelect: Codable, Hashable, Sendable, Identifiable { @@ -5840,11 +5890,13 @@ test('typegen: swift', async () => { internal let id: Int64? internal let name: String? internal let status: UserStatus? + internal let userUuid: UUID? internal enum CodingKeys: String, CodingKey { case decimal = "decimal" case id = "id" case name = "name" case status = "status" + case userUuid = "user_uuid" } } internal struct UsersViewWithMultipleRefsToUsersSelect: Codable, Hashable, Sendable { @@ -6221,11 +6273,13 @@ test('typegen: swift w/ public access control', async () => { public let id: Int64 public let name: String? public let status: UserStatus? + public let userUuid: UUID? public enum CodingKeys: String, CodingKey { case decimal = "decimal" case id = "id" case name = "name" case status = "status" + case userUuid = "user_uuid" } } public struct UsersInsert: Codable, Hashable, Sendable, Identifiable { @@ -6233,11 +6287,13 @@ test('typegen: swift w/ public access control', async () => { public let id: Int64? public let name: String? public let status: UserStatus? + public let userUuid: UUID? public enum CodingKeys: String, CodingKey { case decimal = "decimal" case id = "id" case name = "name" case status = "status" + case userUuid = "user_uuid" } } public struct UsersUpdate: Codable, Hashable, Sendable, Identifiable { @@ -6245,11 +6301,13 @@ test('typegen: swift w/ public access control', async () => { public let id: Int64? public let name: String? public let status: UserStatus? + public let userUuid: UUID? public enum CodingKeys: String, CodingKey { case decimal = "decimal" case id = "id" case name = "name" case status = "status" + case userUuid = "user_uuid" } } public struct UsersAuditSelect: Codable, Hashable, Sendable, Identifiable { @@ -6333,11 +6391,13 @@ test('typegen: swift w/ public access control', async () => { public let id: Int64? public let name: String? public let status: UserStatus? + public let userUuid: UUID? public enum CodingKeys: String, CodingKey { case decimal = "decimal" case id = "id" case name = "name" case status = "status" + case userUuid = "user_uuid" } } public struct UsersViewWithMultipleRefsToUsersSelect: Codable, Hashable, Sendable { @@ -6375,251 +6435,255 @@ test('typegen: python', async () => { query: { access_control: 'public' }, }) expect(body).toMatchInlineSnapshot(` -"from __future__ import annotations - -import datetime -import uuid -from typing import ( - Annotated, - Any, - List, - Literal, - NotRequired, - Optional, - TypeAlias, - TypedDict, -) - -from pydantic import BaseModel, Field, Json - -PublicUserStatus: TypeAlias = Literal["ACTIVE", "INACTIVE"] - -PublicMemeStatus: TypeAlias = Literal["new", "old", "retired"] - -class PublicUsers(BaseModel): - decimal: Optional[float] = Field(alias="decimal") - id: int = Field(alias="id") - name: Optional[str] = Field(alias="name") - status: Optional[PublicUserStatus] = Field(alias="status") - -class PublicUsersInsert(TypedDict): - decimal: NotRequired[Annotated[float, Field(alias="decimal")]] - id: NotRequired[Annotated[int, Field(alias="id")]] - name: NotRequired[Annotated[str, Field(alias="name")]] - status: NotRequired[Annotated[PublicUserStatus, Field(alias="status")]] - -class PublicUsersUpdate(TypedDict): - decimal: NotRequired[Annotated[float, Field(alias="decimal")]] - id: NotRequired[Annotated[int, Field(alias="id")]] - name: NotRequired[Annotated[str, Field(alias="name")]] - status: NotRequired[Annotated[PublicUserStatus, Field(alias="status")]] - -class PublicTodos(BaseModel): - details: Optional[str] = Field(alias="details") - id: int = Field(alias="id") - user_id: int = Field(alias="user-id") - -class PublicTodosInsert(TypedDict): - details: NotRequired[Annotated[str, Field(alias="details")]] - id: NotRequired[Annotated[int, Field(alias="id")]] - user_id: Annotated[int, Field(alias="user-id")] - -class PublicTodosUpdate(TypedDict): - details: NotRequired[Annotated[str, Field(alias="details")]] - id: NotRequired[Annotated[int, Field(alias="id")]] - user_id: NotRequired[Annotated[int, Field(alias="user-id")]] - -class PublicUsersAudit(BaseModel): - created_at: Optional[datetime.datetime] = Field(alias="created_at") - id: int = Field(alias="id") - previous_value: Optional[Json[Any]] = Field(alias="previous_value") - user_id: Optional[int] = Field(alias="user_id") - -class PublicUsersAuditInsert(TypedDict): - created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] - id: NotRequired[Annotated[int, Field(alias="id")]] - previous_value: NotRequired[Annotated[Json[Any], Field(alias="previous_value")]] - user_id: NotRequired[Annotated[int, Field(alias="user_id")]] - -class PublicUsersAuditUpdate(TypedDict): - created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] - id: NotRequired[Annotated[int, Field(alias="id")]] - previous_value: NotRequired[Annotated[Json[Any], Field(alias="previous_value")]] - user_id: NotRequired[Annotated[int, Field(alias="user_id")]] - -class PublicUserDetails(BaseModel): - details: Optional[str] = Field(alias="details") - user_id: int = Field(alias="user_id") - -class PublicUserDetailsInsert(TypedDict): - details: NotRequired[Annotated[str, Field(alias="details")]] - user_id: Annotated[int, Field(alias="user_id")] - -class PublicUserDetailsUpdate(TypedDict): - details: NotRequired[Annotated[str, Field(alias="details")]] - user_id: NotRequired[Annotated[int, Field(alias="user_id")]] - -class PublicEmpty(BaseModel): - pass - -class PublicEmptyInsert(TypedDict): - pass - -class PublicEmptyUpdate(TypedDict): - pass - -class PublicTableWithOtherTablesRowType(BaseModel): - col1: Optional[PublicUserDetails] = Field(alias="col1") - col2: Optional[PublicAView] = Field(alias="col2") - -class PublicTableWithOtherTablesRowTypeInsert(TypedDict): - col1: NotRequired[Annotated[PublicUserDetails, Field(alias="col1")]] - col2: NotRequired[Annotated[PublicAView, Field(alias="col2")]] - -class PublicTableWithOtherTablesRowTypeUpdate(TypedDict): - col1: NotRequired[Annotated[PublicUserDetails, Field(alias="col1")]] - col2: NotRequired[Annotated[PublicAView, Field(alias="col2")]] - -class PublicTableWithPrimaryKeyOtherThanId(BaseModel): - name: Optional[str] = Field(alias="name") - other_id: int = Field(alias="other_id") - -class PublicTableWithPrimaryKeyOtherThanIdInsert(TypedDict): - name: NotRequired[Annotated[str, Field(alias="name")]] - other_id: NotRequired[Annotated[int, Field(alias="other_id")]] - -class PublicTableWithPrimaryKeyOtherThanIdUpdate(TypedDict): - name: NotRequired[Annotated[str, Field(alias="name")]] - other_id: NotRequired[Annotated[int, Field(alias="other_id")]] - -class PublicEvents(BaseModel): - created_at: datetime.datetime = Field(alias="created_at") - data: Optional[Json[Any]] = Field(alias="data") - event_type: Optional[str] = Field(alias="event_type") - id: int = Field(alias="id") - -class PublicEventsInsert(TypedDict): - created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] - data: NotRequired[Annotated[Json[Any], Field(alias="data")]] - event_type: NotRequired[Annotated[str, Field(alias="event_type")]] - id: NotRequired[Annotated[int, Field(alias="id")]] - -class PublicEventsUpdate(TypedDict): - created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] - data: NotRequired[Annotated[Json[Any], Field(alias="data")]] - event_type: NotRequired[Annotated[str, Field(alias="event_type")]] - id: NotRequired[Annotated[int, Field(alias="id")]] - -class PublicEvents2024(BaseModel): - created_at: datetime.datetime = Field(alias="created_at") - data: Optional[Json[Any]] = Field(alias="data") - event_type: Optional[str] = Field(alias="event_type") - id: int = Field(alias="id") - -class PublicEvents2024Insert(TypedDict): - created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] - data: NotRequired[Annotated[Json[Any], Field(alias="data")]] - event_type: NotRequired[Annotated[str, Field(alias="event_type")]] - id: Annotated[int, Field(alias="id")] - -class PublicEvents2024Update(TypedDict): - created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] - data: NotRequired[Annotated[Json[Any], Field(alias="data")]] - event_type: NotRequired[Annotated[str, Field(alias="event_type")]] - id: NotRequired[Annotated[int, Field(alias="id")]] - -class PublicEvents2025(BaseModel): - created_at: datetime.datetime = Field(alias="created_at") - data: Optional[Json[Any]] = Field(alias="data") - event_type: Optional[str] = Field(alias="event_type") - id: int = Field(alias="id") - -class PublicEvents2025Insert(TypedDict): - created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] - data: NotRequired[Annotated[Json[Any], Field(alias="data")]] - event_type: NotRequired[Annotated[str, Field(alias="event_type")]] - id: Annotated[int, Field(alias="id")] - -class PublicEvents2025Update(TypedDict): - created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] - data: NotRequired[Annotated[Json[Any], Field(alias="data")]] - event_type: NotRequired[Annotated[str, Field(alias="event_type")]] - id: NotRequired[Annotated[int, Field(alias="id")]] - -class PublicCategory(BaseModel): - id: int = Field(alias="id") - name: str = Field(alias="name") - -class PublicCategoryInsert(TypedDict): - id: NotRequired[Annotated[int, Field(alias="id")]] - name: Annotated[str, Field(alias="name")] - -class PublicCategoryUpdate(TypedDict): - id: NotRequired[Annotated[int, Field(alias="id")]] - name: NotRequired[Annotated[str, Field(alias="name")]] - -class PublicMemes(BaseModel): - category: Optional[int] = Field(alias="category") - created_at: datetime.datetime = Field(alias="created_at") - id: int = Field(alias="id") - metadata: Optional[Json[Any]] = Field(alias="metadata") - name: str = Field(alias="name") - status: Optional[PublicMemeStatus] = Field(alias="status") - -class PublicMemesInsert(TypedDict): - category: NotRequired[Annotated[int, Field(alias="category")]] - created_at: Annotated[datetime.datetime, Field(alias="created_at")] - id: NotRequired[Annotated[int, Field(alias="id")]] - metadata: NotRequired[Annotated[Json[Any], Field(alias="metadata")]] - name: Annotated[str, Field(alias="name")] - status: NotRequired[Annotated[PublicMemeStatus, Field(alias="status")]] - -class PublicMemesUpdate(TypedDict): - category: NotRequired[Annotated[int, Field(alias="category")]] - created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] - id: NotRequired[Annotated[int, Field(alias="id")]] - metadata: NotRequired[Annotated[Json[Any], Field(alias="metadata")]] - name: NotRequired[Annotated[str, Field(alias="name")]] - status: NotRequired[Annotated[PublicMemeStatus, Field(alias="status")]] - -class PublicAView(BaseModel): - id: Optional[int] = Field(alias="id") - -class PublicTodosView(BaseModel): - details: Optional[str] = Field(alias="details") - id: Optional[int] = Field(alias="id") - user_id: Optional[int] = Field(alias="user-id") - -class PublicUsersView(BaseModel): - decimal: Optional[float] = Field(alias="decimal") - id: Optional[int] = Field(alias="id") - name: Optional[str] = Field(alias="name") - status: Optional[PublicUserStatus] = Field(alias="status") - -class PublicUserTodosSummaryView(BaseModel): - todo_count: Optional[int] = Field(alias="todo_count") - todo_details: Optional[List[str]] = Field(alias="todo_details") - user_id: Optional[int] = Field(alias="user_id") - user_name: Optional[str] = Field(alias="user_name") - user_status: Optional[PublicUserStatus] = Field(alias="user_status") - -class PublicUsersViewWithMultipleRefsToUsers(BaseModel): - initial_id: Optional[int] = Field(alias="initial_id") - initial_name: Optional[str] = Field(alias="initial_name") - second_id: Optional[int] = Field(alias="second_id") - second_name: Optional[str] = Field(alias="second_name") - -class PublicTodosMatview(BaseModel): - details: Optional[str] = Field(alias="details") - id: Optional[int] = Field(alias="id") - user_id: Optional[int] = Field(alias="user-id") - -class PublicCompositeTypeWithArrayAttribute(BaseModel): - my_text_array: List[str] = Field(alias="my_text_array") - -class PublicCompositeTypeWithRecordAttribute(BaseModel): - todo: PublicTodos = Field(alias="todo")" -`) + "from __future__ import annotations + + import datetime + import uuid + from typing import ( + Annotated, + Any, + List, + Literal, + NotRequired, + Optional, + TypeAlias, + TypedDict, + ) + + from pydantic import BaseModel, Field, Json + + PublicUserStatus: TypeAlias = Literal["ACTIVE", "INACTIVE"] + + PublicMemeStatus: TypeAlias = Literal["new", "old", "retired"] + + class PublicUsers(BaseModel): + decimal: Optional[float] = Field(alias="decimal") + id: int = Field(alias="id") + name: Optional[str] = Field(alias="name") + status: Optional[PublicUserStatus] = Field(alias="status") + user_uuid: Optional[uuid.UUID] = Field(alias="user_uuid") + + class PublicUsersInsert(TypedDict): + decimal: NotRequired[Annotated[float, Field(alias="decimal")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + name: NotRequired[Annotated[str, Field(alias="name")]] + status: NotRequired[Annotated[PublicUserStatus, Field(alias="status")]] + user_uuid: NotRequired[Annotated[uuid.UUID, Field(alias="user_uuid")]] + + class PublicUsersUpdate(TypedDict): + decimal: NotRequired[Annotated[float, Field(alias="decimal")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + name: NotRequired[Annotated[str, Field(alias="name")]] + status: NotRequired[Annotated[PublicUserStatus, Field(alias="status")]] + user_uuid: NotRequired[Annotated[uuid.UUID, Field(alias="user_uuid")]] + + class PublicTodos(BaseModel): + details: Optional[str] = Field(alias="details") + id: int = Field(alias="id") + user_id: int = Field(alias="user-id") + + class PublicTodosInsert(TypedDict): + details: NotRequired[Annotated[str, Field(alias="details")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + user_id: Annotated[int, Field(alias="user-id")] + + class PublicTodosUpdate(TypedDict): + details: NotRequired[Annotated[str, Field(alias="details")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + user_id: NotRequired[Annotated[int, Field(alias="user-id")]] + + class PublicUsersAudit(BaseModel): + created_at: Optional[datetime.datetime] = Field(alias="created_at") + id: int = Field(alias="id") + previous_value: Optional[Json[Any]] = Field(alias="previous_value") + user_id: Optional[int] = Field(alias="user_id") + + class PublicUsersAuditInsert(TypedDict): + created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + previous_value: NotRequired[Annotated[Json[Any], Field(alias="previous_value")]] + user_id: NotRequired[Annotated[int, Field(alias="user_id")]] + + class PublicUsersAuditUpdate(TypedDict): + created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + previous_value: NotRequired[Annotated[Json[Any], Field(alias="previous_value")]] + user_id: NotRequired[Annotated[int, Field(alias="user_id")]] + + class PublicUserDetails(BaseModel): + details: Optional[str] = Field(alias="details") + user_id: int = Field(alias="user_id") + + class PublicUserDetailsInsert(TypedDict): + details: NotRequired[Annotated[str, Field(alias="details")]] + user_id: Annotated[int, Field(alias="user_id")] + + class PublicUserDetailsUpdate(TypedDict): + details: NotRequired[Annotated[str, Field(alias="details")]] + user_id: NotRequired[Annotated[int, Field(alias="user_id")]] + + class PublicEmpty(BaseModel): + pass + + class PublicEmptyInsert(TypedDict): + pass + + class PublicEmptyUpdate(TypedDict): + pass + + class PublicTableWithOtherTablesRowType(BaseModel): + col1: Optional[PublicUserDetails] = Field(alias="col1") + col2: Optional[PublicAView] = Field(alias="col2") + + class PublicTableWithOtherTablesRowTypeInsert(TypedDict): + col1: NotRequired[Annotated[PublicUserDetails, Field(alias="col1")]] + col2: NotRequired[Annotated[PublicAView, Field(alias="col2")]] + + class PublicTableWithOtherTablesRowTypeUpdate(TypedDict): + col1: NotRequired[Annotated[PublicUserDetails, Field(alias="col1")]] + col2: NotRequired[Annotated[PublicAView, Field(alias="col2")]] + + class PublicTableWithPrimaryKeyOtherThanId(BaseModel): + name: Optional[str] = Field(alias="name") + other_id: int = Field(alias="other_id") + + class PublicTableWithPrimaryKeyOtherThanIdInsert(TypedDict): + name: NotRequired[Annotated[str, Field(alias="name")]] + other_id: NotRequired[Annotated[int, Field(alias="other_id")]] + + class PublicTableWithPrimaryKeyOtherThanIdUpdate(TypedDict): + name: NotRequired[Annotated[str, Field(alias="name")]] + other_id: NotRequired[Annotated[int, Field(alias="other_id")]] + + class PublicEvents(BaseModel): + created_at: datetime.datetime = Field(alias="created_at") + data: Optional[Json[Any]] = Field(alias="data") + event_type: Optional[str] = Field(alias="event_type") + id: int = Field(alias="id") + + class PublicEventsInsert(TypedDict): + created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] + data: NotRequired[Annotated[Json[Any], Field(alias="data")]] + event_type: NotRequired[Annotated[str, Field(alias="event_type")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + + class PublicEventsUpdate(TypedDict): + created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] + data: NotRequired[Annotated[Json[Any], Field(alias="data")]] + event_type: NotRequired[Annotated[str, Field(alias="event_type")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + + class PublicEvents2024(BaseModel): + created_at: datetime.datetime = Field(alias="created_at") + data: Optional[Json[Any]] = Field(alias="data") + event_type: Optional[str] = Field(alias="event_type") + id: int = Field(alias="id") + + class PublicEvents2024Insert(TypedDict): + created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] + data: NotRequired[Annotated[Json[Any], Field(alias="data")]] + event_type: NotRequired[Annotated[str, Field(alias="event_type")]] + id: Annotated[int, Field(alias="id")] + + class PublicEvents2024Update(TypedDict): + created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] + data: NotRequired[Annotated[Json[Any], Field(alias="data")]] + event_type: NotRequired[Annotated[str, Field(alias="event_type")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + + class PublicEvents2025(BaseModel): + created_at: datetime.datetime = Field(alias="created_at") + data: Optional[Json[Any]] = Field(alias="data") + event_type: Optional[str] = Field(alias="event_type") + id: int = Field(alias="id") + + class PublicEvents2025Insert(TypedDict): + created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] + data: NotRequired[Annotated[Json[Any], Field(alias="data")]] + event_type: NotRequired[Annotated[str, Field(alias="event_type")]] + id: Annotated[int, Field(alias="id")] + + class PublicEvents2025Update(TypedDict): + created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] + data: NotRequired[Annotated[Json[Any], Field(alias="data")]] + event_type: NotRequired[Annotated[str, Field(alias="event_type")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + + class PublicCategory(BaseModel): + id: int = Field(alias="id") + name: str = Field(alias="name") + + class PublicCategoryInsert(TypedDict): + id: NotRequired[Annotated[int, Field(alias="id")]] + name: Annotated[str, Field(alias="name")] + + class PublicCategoryUpdate(TypedDict): + id: NotRequired[Annotated[int, Field(alias="id")]] + name: NotRequired[Annotated[str, Field(alias="name")]] + + class PublicMemes(BaseModel): + category: Optional[int] = Field(alias="category") + created_at: datetime.datetime = Field(alias="created_at") + id: int = Field(alias="id") + metadata: Optional[Json[Any]] = Field(alias="metadata") + name: str = Field(alias="name") + status: Optional[PublicMemeStatus] = Field(alias="status") + + class PublicMemesInsert(TypedDict): + category: NotRequired[Annotated[int, Field(alias="category")]] + created_at: Annotated[datetime.datetime, Field(alias="created_at")] + id: NotRequired[Annotated[int, Field(alias="id")]] + metadata: NotRequired[Annotated[Json[Any], Field(alias="metadata")]] + name: Annotated[str, Field(alias="name")] + status: NotRequired[Annotated[PublicMemeStatus, Field(alias="status")]] + + class PublicMemesUpdate(TypedDict): + category: NotRequired[Annotated[int, Field(alias="category")]] + created_at: NotRequired[Annotated[datetime.datetime, Field(alias="created_at")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + metadata: NotRequired[Annotated[Json[Any], Field(alias="metadata")]] + name: NotRequired[Annotated[str, Field(alias="name")]] + status: NotRequired[Annotated[PublicMemeStatus, Field(alias="status")]] + + class PublicAView(BaseModel): + id: Optional[int] = Field(alias="id") + + class PublicTodosView(BaseModel): + details: Optional[str] = Field(alias="details") + id: Optional[int] = Field(alias="id") + user_id: Optional[int] = Field(alias="user-id") + + class PublicUsersView(BaseModel): + decimal: Optional[float] = Field(alias="decimal") + id: Optional[int] = Field(alias="id") + name: Optional[str] = Field(alias="name") + status: Optional[PublicUserStatus] = Field(alias="status") + user_uuid: Optional[uuid.UUID] = Field(alias="user_uuid") + + class PublicUserTodosSummaryView(BaseModel): + todo_count: Optional[int] = Field(alias="todo_count") + todo_details: Optional[List[str]] = Field(alias="todo_details") + user_id: Optional[int] = Field(alias="user_id") + user_name: Optional[str] = Field(alias="user_name") + user_status: Optional[PublicUserStatus] = Field(alias="user_status") + + class PublicUsersViewWithMultipleRefsToUsers(BaseModel): + initial_id: Optional[int] = Field(alias="initial_id") + initial_name: Optional[str] = Field(alias="initial_name") + second_id: Optional[int] = Field(alias="second_id") + second_name: Optional[str] = Field(alias="second_name") + + class PublicTodosMatview(BaseModel): + details: Optional[str] = Field(alias="details") + id: Optional[int] = Field(alias="id") + user_id: Optional[int] = Field(alias="user-id") + + class PublicCompositeTypeWithArrayAttribute(BaseModel): + my_text_array: List[str] = Field(alias="my_text_array") + + class PublicCompositeTypeWithRecordAttribute(BaseModel): + todo: PublicTodos = Field(alias="todo")" + `) }) test('typegen: python w/ excluded/included schemas', async () => { diff --git a/test/server/utils.ts b/test/server/utils.ts index 0222fcf3..63e1e53d 100644 --- a/test/server/utils.ts +++ b/test/server/utils.ts @@ -1,3 +1,29 @@ import { build as buildApp } from '../../src/server/app' export const app = buildApp() + +/** + * Normalizes UUIDs in test data to make snapshots resilient to UUID changes. + * Replaces all UUID strings with a consistent placeholder. + */ +export function normalizeUuids(data: unknown): unknown { + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + + if (typeof data === 'string' && uuidRegex.test(data)) { + return '00000000-0000-0000-0000-000000000000' + } + + if (Array.isArray(data)) { + return data.map(normalizeUuids) + } + + if (data !== null && typeof data === 'object') { + const normalized: Record = {} + for (const [key, value] of Object.entries(data)) { + normalized[key] = normalizeUuids(value) + } + return normalized + } + + return data +}