diff --git a/src/server/templates/python.ts b/src/server/templates/python.ts index 2d9459ca..b112f089 100644 --- a/src/server/templates/python.ts +++ b/src/server/templates/python.ts @@ -345,6 +345,7 @@ const PY_TYPE_MAP: Record = { timestamptz: 'datetime.datetime', uuid: 'uuid.UUID', vector: 'list[Any]', + interval: 'str', // JSON json: 'Json[Any]', diff --git a/src/server/templates/typescript.ts b/src/server/templates/typescript.ts index 75ca7de4..352c4ddc 100644 --- a/src/server/templates/typescript.ts +++ b/src/server/templates/typescript.ts @@ -869,7 +869,7 @@ export const Constants = { } // TODO: Make this more robust. Currently doesn't handle range types - returns them as unknown. -const pgTypeToTsType = ( +export const pgTypeToTsType = ( schema: PostgresSchema, pgType: string, { @@ -902,6 +902,7 @@ const pgTypeToTsType = ( 'timestamptz', 'uuid', 'vector', + 'interval', ].includes(pgType) ) { return 'string' diff --git a/test/db/00-init.sql b/test/db/00-init.sql index 8ddc77ba..c30e1f4a 100644 --- a/test/db/00-init.sql +++ b/test/db/00-init.sql @@ -465,4 +465,39 @@ CREATE OR REPLACE FUNCTION "public"."days_since_event" ("public"."events") RETUR SET "search_path" TO '' AS $_$ SELECT ROUND(EXTRACT(EPOCH FROM (NOW() - $1.created_at)) / 86400); -$_$; \ No newline at end of file +$_$; + +-- Table with interval columns for testing interval type (nullable and not nullable) +CREATE TABLE public.interval_test ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + duration_required interval NOT NULL, + duration_optional interval +); + +-- Insert test data with interval values +INSERT INTO public.interval_test (duration_required, duration_optional) +VALUES + ('1 day 2 hours 30 minutes', '3 days 5 hours'), + ('1 week', NULL), + ('2 hours 15 minutes', '45 minutes'); + +-- Function that takes interval parameter and returns interval +CREATE OR REPLACE FUNCTION public.add_interval_to_duration( + base_duration interval, + additional_interval interval +) +RETURNS interval +LANGUAGE SQL +STABLE +AS $$ + SELECT base_duration + additional_interval; +$$; + +-- Function that takes a table row with interval and returns interval +CREATE OR REPLACE FUNCTION public.double_duration(interval_test_row public.interval_test) +RETURNS interval +LANGUAGE SQL +STABLE +AS $$ + SELECT interval_test_row.duration_required * 2; +$$; \ No newline at end of file diff --git a/test/server/typegen.ts b/test/server/typegen.ts index d71b468e..7bb7013b 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -119,6 +119,24 @@ test('typegen: typescript', async () => { } Relationships: [] } + interval_test: { + Row: { + duration_optional: string | null + duration_required: string + id: number + } + Insert: { + duration_optional?: string | null + duration_required: string + id?: number + } + Update: { + duration_optional?: string | null + duration_required?: string + id?: number + } + Relationships: [] + } memes: { Row: { category: number | null @@ -525,6 +543,10 @@ test('typegen: typescript', async () => { } } Functions: { + add_interval_to_duration: { + Args: { additional_interval: string; base_duration: string } + Returns: string + } blurb: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } Returns: { @@ -574,6 +596,12 @@ test('typegen: typescript', async () => { error: true } & "the function public.details_words with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } + double_duration: { + Args: { + interval_test_row: Database["public"]["Tables"]["interval_test"]["Row"] + } + Returns: string + } function_returning_row: { Args: never Returns: { @@ -1291,6 +1319,24 @@ test('typegen w/ one-to-one relationships', async () => { } Relationships: [] } + interval_test: { + Row: { + duration_optional: string | null + duration_required: string + id: number + } + Insert: { + duration_optional?: string | null + duration_required: string + id?: number + } + Update: { + duration_optional?: string | null + duration_required?: string + id?: number + } + Relationships: [] + } memes: { Row: { category: number | null @@ -1722,6 +1768,10 @@ test('typegen w/ one-to-one relationships', async () => { } } Functions: { + add_interval_to_duration: { + Args: { additional_interval: string; base_duration: string } + Returns: string + } blurb: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } Returns: { @@ -1771,6 +1821,12 @@ test('typegen w/ one-to-one relationships', async () => { error: true } & "the function public.details_words with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } + double_duration: { + Args: { + interval_test_row: Database["public"]["Tables"]["interval_test"]["Row"] + } + Returns: string + } function_returning_row: { Args: never Returns: { @@ -2488,6 +2544,24 @@ test('typegen: typescript w/ one-to-one relationships', async () => { } Relationships: [] } + interval_test: { + Row: { + duration_optional: string | null + duration_required: string + id: number + } + Insert: { + duration_optional?: string | null + duration_required: string + id?: number + } + Update: { + duration_optional?: string | null + duration_required?: string + id?: number + } + Relationships: [] + } memes: { Row: { category: number | null @@ -2919,6 +2993,10 @@ test('typegen: typescript w/ one-to-one relationships', async () => { } } Functions: { + add_interval_to_duration: { + Args: { additional_interval: string; base_duration: string } + Returns: string + } blurb: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } Returns: { @@ -2968,6 +3046,12 @@ test('typegen: typescript w/ one-to-one relationships', async () => { error: true } & "the function public.details_words with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } + double_duration: { + Args: { + interval_test_row: Database["public"]["Tables"]["interval_test"]["Row"] + } + Returns: string + } function_returning_row: { Args: never Returns: { @@ -3690,6 +3774,24 @@ test('typegen: typescript w/ postgrestVersion', async () => { } Relationships: [] } + interval_test: { + Row: { + duration_optional: string | null + duration_required: string + id: number + } + Insert: { + duration_optional?: string | null + duration_required: string + id?: number + } + Update: { + duration_optional?: string | null + duration_required?: string + id?: number + } + Relationships: [] + } memes: { Row: { category: number | null @@ -4121,6 +4223,10 @@ test('typegen: typescript w/ postgrestVersion', async () => { } } Functions: { + add_interval_to_duration: { + Args: { additional_interval: string; base_duration: string } + Returns: string + } blurb: { Args: { "": Database["public"]["Tables"]["todos"]["Row"] } Returns: { @@ -4170,6 +4276,12 @@ test('typegen: typescript w/ postgrestVersion', async () => { error: true } & "the function public.details_words with parameter or with a single unnamed json/jsonb parameter, but no matches were found in the schema cache" } + double_duration: { + Args: { + interval_test_row: Database["public"]["Tables"]["interval_test"]["Row"] + } + Returns: string + } function_returning_row: { Args: never Returns: { @@ -5339,6 +5451,24 @@ test('typegen: go', async () => { Id *int64 \`json:"id"\` } + type PublicIntervalTestSelect struct { + DurationOptional interface{} \`json:"duration_optional"\` + DurationRequired interface{} \`json:"duration_required"\` + Id int64 \`json:"id"\` + } + + type PublicIntervalTestInsert struct { + DurationOptional interface{} \`json:"duration_optional"\` + DurationRequired interface{} \`json:"duration_required"\` + Id *int64 \`json:"id"\` + } + + type PublicIntervalTestUpdate struct { + DurationOptional interface{} \`json:"duration_optional"\` + DurationRequired interface{} \`json:"duration_required"\` + Id *int64 \`json:"id"\` + } + type PublicCategorySelect struct { Id int32 \`json:"id"\` Name string \`json:"name"\` @@ -5614,6 +5744,36 @@ test('typegen: swift', async () => { case status = "status" } } + internal struct IntervalTestSelect: Codable, Hashable, Sendable, Identifiable { + internal let durationOptional: IntervalSelect? + internal let durationRequired: IntervalSelect + internal let id: Int64 + internal enum CodingKeys: String, CodingKey { + case durationOptional = "duration_optional" + case durationRequired = "duration_required" + case id = "id" + } + } + internal struct IntervalTestInsert: Codable, Hashable, Sendable, Identifiable { + internal let durationOptional: IntervalSelect? + internal let durationRequired: IntervalSelect + internal let id: Int64? + internal enum CodingKeys: String, CodingKey { + case durationOptional = "duration_optional" + case durationRequired = "duration_required" + case id = "id" + } + } + internal struct IntervalTestUpdate: Codable, Hashable, Sendable, Identifiable { + internal let durationOptional: IntervalSelect? + internal let durationRequired: IntervalSelect? + internal let id: Int64? + internal enum CodingKeys: String, CodingKey { + case durationOptional = "duration_optional" + case durationRequired = "duration_required" + case id = "id" + } + } internal struct MemesSelect: Codable, Hashable, Sendable { internal let category: Int32? internal let createdAt: String @@ -6115,6 +6275,36 @@ test('typegen: swift w/ public access control', async () => { case status = "status" } } + public struct IntervalTestSelect: Codable, Hashable, Sendable, Identifiable { + public let durationOptional: IntervalSelect? + public let durationRequired: IntervalSelect + public let id: Int64 + public enum CodingKeys: String, CodingKey { + case durationOptional = "duration_optional" + case durationRequired = "duration_required" + case id = "id" + } + } + public struct IntervalTestInsert: Codable, Hashable, Sendable, Identifiable { + public let durationOptional: IntervalSelect? + public let durationRequired: IntervalSelect + public let id: Int64? + public enum CodingKeys: String, CodingKey { + case durationOptional = "duration_optional" + case durationRequired = "duration_required" + case id = "id" + } + } + public struct IntervalTestUpdate: Codable, Hashable, Sendable, Identifiable { + public let durationOptional: IntervalSelect? + public let durationRequired: IntervalSelect? + public let id: Int64? + public enum CodingKeys: String, CodingKey { + case durationOptional = "duration_optional" + case durationRequired = "duration_required" + case id = "id" + } + } public struct MemesSelect: Codable, Hashable, Sendable { public let category: Int32? public let createdAt: String @@ -6609,6 +6799,21 @@ test('typegen: python', async () => { event_type: NotRequired[Annotated[str, Field(alias="event_type")]] id: NotRequired[Annotated[int, Field(alias="id")]] + class PublicIntervalTest(BaseModel): + duration_optional: Optional[str] = Field(alias="duration_optional") + duration_required: str = Field(alias="duration_required") + id: int = Field(alias="id") + + class PublicIntervalTestInsert(TypedDict): + duration_optional: NotRequired[Annotated[str, Field(alias="duration_optional")]] + duration_required: Annotated[str, Field(alias="duration_required")] + id: NotRequired[Annotated[int, Field(alias="id")]] + + class PublicIntervalTestUpdate(TypedDict): + duration_optional: NotRequired[Annotated[str, Field(alias="duration_optional")]] + duration_required: NotRequired[Annotated[str, Field(alias="duration_required")]] + id: NotRequired[Annotated[int, Field(alias="id")]] + class PublicCategory(BaseModel): id: int = Field(alias="id") name: str = Field(alias="name") diff --git a/test/types.test.ts b/test/types.test.ts index df2af697..8a213902 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -1,6 +1,7 @@ import { expect, test, describe } from 'vitest' import { build } from '../src/server/app.js' import { TEST_CONNECTION_STRING } from './lib/utils.js' +import { pgTypeToTsType } from '../src/server/templates/typescript' describe('server/routes/types', () => { test('should list types', async () => { @@ -43,4 +44,15 @@ describe('server/routes/types', () => { expect(response.statusCode).toBe(404) await app.close() }) + + test('nullable interval column maps to string | null', () => { + const result = pgTypeToTsType({ name: 'public' } as any, 'interval', { + types: [], + schemas: [], + tables: [], + views: [], + }) + + expect(result).toBe('string') + }) })