diff --git a/src/lib/sql/columns.sql.ts b/src/lib/sql/columns.sql.ts index d4f6b6c7..7c2d1d52 100644 --- a/src/lib/sql/columns.sql.ts +++ b/src/lib/sql/columns.sql.ts @@ -37,6 +37,14 @@ SELECT END END AS data_type, COALESCE(bt.typname, t.typname) AS format, + CASE + WHEN t.typtype = 'd' THEN nt.nspname + ELSE NULL + END AS domain_schema, + CASE + WHEN t.typtype = 'd' THEN t.typname + ELSE NULL + END AS domain_name, a.attidentity IN ('a', 'd') AS is_identity, CASE a.attidentity diff --git a/src/lib/types.ts b/src/lib/types.ts index 26b3bc78..e19ba916 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -27,6 +27,10 @@ export const postgresColumnSchema = Type.Object({ default_value: Type.Unknown(), data_type: Type.String(), format: Type.String(), + // Set only when the column's type is a DOMAIN. `data_type` and `format` keep + // reporting the domain's base type, as information_schema.columns does. + domain_schema: Type.Union([Type.String(), Type.Null()]), + domain_name: Type.Union([Type.String(), Type.Null()]), is_identity: Type.Boolean(), identity_generation: Type.Union([ Type.Literal('ALWAYS'), diff --git a/test/db/00-init.sql b/test/db/00-init.sql index c30e1f4a..f5f73e80 100644 --- a/test/db/00-init.sql +++ b/test/db/00-init.sql @@ -500,4 +500,16 @@ LANGUAGE SQL STABLE AS $$ SELECT interval_test_row.duration_required * 2; -$$; \ No newline at end of file +$$; + +-- Domain types, to check that column introspection preserves the domain +-- identity instead of collapsing to the base type. +CREATE DOMAIN public.text_search AS jsonb CHECK (jsonb_typeof(VALUE) = 'object'); +CREATE DOMAIN public.positive_int AS int4 NOT NULL CHECK (VALUE > 0); + +CREATE TABLE public.domain_test ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + search public.text_search, + quantity public.positive_int, + plain jsonb +); diff --git a/test/lib/columns.ts b/test/lib/columns.ts index 3fcac79f..5a6d2464 100644 --- a/test/lib/columns.ts +++ b/test/lib/columns.ts @@ -14,6 +14,8 @@ test('list', async () => { "comment": null, "data_type": "bigint", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int8", "id": StringMatching /\\^\\\\d\\+\\\\\\.3\\$/, @@ -60,6 +62,8 @@ test('list from a single table', async () => { "comment": null, "data_type": "text", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "text", "id": StringMatching /\\^\\\\d\\+\\\\\\.\\\\d\\+\\$/, @@ -80,6 +84,8 @@ test('list from a single table', async () => { "comment": null, "data_type": "text", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "text", "id": StringMatching /\\^\\\\d\\+\\\\\\.\\\\d\\+\\$/, @@ -158,6 +164,8 @@ test('retrieve, create, update, delete', async () => { "comment": "foo", "data_type": "smallint", "default_value": "'42'::smallint", + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int2", "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/, @@ -189,6 +197,8 @@ test('retrieve, create, update, delete', async () => { "comment": "foo", "data_type": "smallint", "default_value": "'42'::smallint", + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int2", "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/, @@ -228,6 +238,8 @@ test('retrieve, create, update, delete', async () => { "comment": "bar", "data_type": "integer", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int4", "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/, @@ -259,6 +271,8 @@ test('retrieve, create, update, delete', async () => { "comment": "bar", "data_type": "integer", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int4", "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/, @@ -304,6 +318,8 @@ test('enum column with quoted name', async () => { "comment": null, "data_type": "USER-DEFINED", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [ "v", ], @@ -412,6 +428,8 @@ test('array column', async () => { "comment": null, "data_type": "ARRAY", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "_int2", "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/, @@ -459,6 +477,8 @@ test('column with default value', async () => { "comment": null, "data_type": "timestamp with time zone", "default_value": "now()", + "domain_name": null, + "domain_schema": null, "enums": [], "format": "timestamptz", "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/, @@ -537,6 +557,8 @@ test('update with name unchanged', async () => { "comment": null, "data_type": "smallint", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int2", "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/, @@ -585,6 +607,8 @@ test('update with array types', async () => { "comment": null, "data_type": "ARRAY", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "_text", "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/, @@ -633,6 +657,8 @@ test('update with incompatible types', async () => { "comment": null, "data_type": "integer", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int4", "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/, @@ -680,6 +706,8 @@ test('update is_unique', async () => { "comment": null, "data_type": "text", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "text", "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/, @@ -714,6 +742,8 @@ test('update is_unique', async () => { "comment": null, "data_type": "text", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "text", "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/, @@ -763,6 +793,8 @@ test('alter column to type with uppercase', async () => { "comment": null, "data_type": "USER-DEFINED", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "T", "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/, @@ -810,6 +842,8 @@ test('enums are populated in enum array columns', async () => { "comment": null, "data_type": "ARRAY", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [ "a", ], @@ -864,6 +898,8 @@ create table public.t ( "comment": null, "data_type": "bigint", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int8", "id": StringMatching /\\^\\\\d\\+\\\\\\.1\\$/, @@ -915,6 +951,8 @@ test('column with multiple checks', async () => { "comment": null, "data_type": "bigint", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int8", "identity_generation": null, @@ -948,6 +986,8 @@ test('column with multiple unique constraints', async () => { "comment": null, "data_type": "bigint", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int8", "identity_generation": null, @@ -1000,6 +1040,8 @@ test('column with fully-qualified type', async () => { "comment": null, "data_type": "USER-DEFINED", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "my_type", "identity_generation": null, @@ -1017,3 +1059,54 @@ test('column with fully-qualified type', async () => { await pgMeta.query(`drop table public.t; drop schema s cascade;`) }) + +test('domain columns keep the domain identity', async () => { + const res = await pgMeta.columns.list() + const columns = res.data + ?.filter(({ table }) => table === 'domain_test') + .map(({ name, data_type, format, domain_schema, domain_name, is_nullable }) => ({ + name, + data_type, + format, + domain_schema, + domain_name, + is_nullable, + })) + + expect(columns).toMatchInlineSnapshot(` + [ + { + "data_type": "bigint", + "domain_name": null, + "domain_schema": null, + "format": "int8", + "is_nullable": false, + "name": "id", + }, + { + "data_type": "jsonb", + "domain_name": "text_search", + "domain_schema": "public", + "format": "jsonb", + "is_nullable": true, + "name": "search", + }, + { + "data_type": "integer", + "domain_name": "positive_int", + "domain_schema": "public", + "format": "int4", + "is_nullable": false, + "name": "quantity", + }, + { + "data_type": "jsonb", + "domain_name": null, + "domain_schema": null, + "format": "jsonb", + "is_nullable": true, + "name": "plain", + }, + ] + `) +}) diff --git a/test/lib/foreign-tables.ts b/test/lib/foreign-tables.ts index 6be2360f..e6dc0a1b 100644 --- a/test/lib/foreign-tables.ts +++ b/test/lib/foreign-tables.ts @@ -29,6 +29,8 @@ test('list', async () => { "comment": null, "data_type": "bigint", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int8", "identity_generation": null, @@ -47,6 +49,8 @@ test('list', async () => { "comment": null, "data_type": "text", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "text", "identity_generation": null, @@ -65,6 +69,8 @@ test('list', async () => { "comment": null, "data_type": "USER-DEFINED", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [ "ACTIVE", "INACTIVE", @@ -112,6 +118,8 @@ test('retrieve', async () => { "comment": null, "data_type": "bigint", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int8", "identity_generation": null, @@ -130,6 +138,8 @@ test('retrieve', async () => { "comment": null, "data_type": "text", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "text", "identity_generation": null, @@ -148,6 +158,8 @@ test('retrieve', async () => { "comment": null, "data_type": "USER-DEFINED", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [ "ACTIVE", "INACTIVE", diff --git a/test/lib/tables.ts b/test/lib/tables.ts index 752b9e96..b651c720 100644 --- a/test/lib/tables.ts +++ b/test/lib/tables.ts @@ -47,6 +47,8 @@ test('list', async () => { "comment": null, "data_type": "bigint", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int8", "identity_generation": "BY DEFAULT", @@ -65,6 +67,8 @@ test('list', async () => { "comment": null, "data_type": "text", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "text", "identity_generation": null, @@ -83,6 +87,8 @@ test('list', async () => { "comment": null, "data_type": "USER-DEFINED", "default_value": "'ACTIVE'::user_status", + "domain_name": null, + "domain_schema": null, "enums": [ "ACTIVE", "INACTIVE", @@ -104,6 +110,8 @@ test('list', async () => { "comment": null, "data_type": "numeric", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "numeric", "identity_generation": null, @@ -122,6 +130,8 @@ test('list', async () => { "comment": null, "data_type": "uuid", "default_value": "gen_random_uuid()", + "domain_name": null, + "domain_schema": null, "enums": [], "format": "uuid", "identity_generation": null, @@ -481,6 +491,8 @@ test('primary keys', async () => { "comment": null, "data_type": "bigint", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int8", "identity_generation": null, @@ -499,6 +511,8 @@ test('primary keys', async () => { "comment": null, "data_type": "text", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "text", "identity_generation": null, diff --git a/test/lib/views.ts b/test/lib/views.ts index e623e14b..120ddcdc 100644 --- a/test/lib/views.ts +++ b/test/lib/views.ts @@ -13,6 +13,8 @@ test('list', async () => { "comment": null, "data_type": "bigint", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int8", "id": "16424.1", @@ -33,6 +35,8 @@ test('list', async () => { "comment": null, "data_type": "text", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "text", "id": "16424.2", @@ -53,6 +57,8 @@ test('list', async () => { "comment": null, "data_type": "bigint", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int8", "id": "16424.3", @@ -110,6 +116,8 @@ test('retrieve', async () => { "comment": null, "data_type": "bigint", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int8", "id": "16424.1", @@ -130,6 +138,8 @@ test('retrieve', async () => { "comment": null, "data_type": "text", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "text", "id": "16424.2", @@ -150,6 +160,8 @@ test('retrieve', async () => { "comment": null, "data_type": "bigint", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int8", "id": "16424.3", diff --git a/test/server/materialized-views.ts b/test/server/materialized-views.ts index 8799a6a9..b777ea77 100644 --- a/test/server/materialized-views.ts +++ b/test/server/materialized-views.ts @@ -42,6 +42,8 @@ test('materialized views with columns', async () => { "comment": null, "data_type": "bigint", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int8", "identity_generation": null, @@ -60,6 +62,8 @@ test('materialized views with columns', async () => { "comment": null, "data_type": "text", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "text", "identity_generation": null, @@ -78,6 +82,8 @@ test('materialized views with columns', async () => { "comment": null, "data_type": "bigint", "default_value": null, + "domain_name": null, + "domain_schema": null, "enums": [], "format": "int8", "identity_generation": null, diff --git a/test/server/typegen.ts b/test/server/typegen.ts index 50a0896b..a47c59f6 100644 --- a/test/server/typegen.ts +++ b/test/server/typegen.ts @@ -31,6 +31,27 @@ test('typegen: typescript', async () => { } Relationships: [] } + domain_test: { + Row: { + id: number + plain: Json | null + quantity: number + search: Json | null + } + Insert: { + id?: number + plain?: Json | null + quantity: number + search?: Json | null + } + Update: { + id?: number + plain?: Json | null + quantity?: number + search?: Json | null + } + Relationships: [] + } empty: { Row: {} Insert: {} @@ -1231,6 +1252,27 @@ test('typegen w/ one-to-one relationships', async () => { } Relationships: [] } + domain_test: { + Row: { + id: number + plain: Json | null + quantity: number + search: Json | null + } + Insert: { + id?: number + plain?: Json | null + quantity: number + search?: Json | null + } + Update: { + id?: number + plain?: Json | null + quantity?: number + search?: Json | null + } + Relationships: [] + } empty: { Row: {} Insert: {} @@ -2456,6 +2498,27 @@ test('typegen: typescript w/ one-to-one relationships', async () => { } Relationships: [] } + domain_test: { + Row: { + id: number + plain: Json | null + quantity: number + search: Json | null + } + Insert: { + id?: number + plain?: Json | null + quantity: number + search?: Json | null + } + Update: { + id?: number + plain?: Json | null + quantity?: number + search?: Json | null + } + Relationships: [] + } empty: { Row: {} Insert: {} @@ -3686,6 +3749,27 @@ test('typegen: typescript w/ postgrestVersion', async () => { } Relationships: [] } + domain_test: { + Row: { + id: number + plain: Json | null + quantity: number + search: Json | null + } + Insert: { + id?: number + plain?: Json | null + quantity: number + search?: Json | null + } + Update: { + id?: number + plain?: Json | null + quantity?: number + search?: Json | null + } + Relationships: [] + } empty: { Row: {} Insert: {} @@ -5469,6 +5553,27 @@ test('typegen: go', async () => { Id *int64 \`json:"id"\` } + type PublicDomainTestSelect struct { + Id int64 \`json:"id"\` + Plain interface{} \`json:"plain"\` + Quantity int32 \`json:"quantity"\` + Search interface{} \`json:"search"\` + } + + type PublicDomainTestInsert struct { + Id *int64 \`json:"id"\` + Plain interface{} \`json:"plain"\` + Quantity int32 \`json:"quantity"\` + Search interface{} \`json:"search"\` + } + + type PublicDomainTestUpdate struct { + Id *int64 \`json:"id"\` + Plain interface{} \`json:"plain"\` + Quantity *int32 \`json:"quantity"\` + Search interface{} \`json:"search"\` + } + type PublicCategorySelect struct { Id int32 \`json:"id"\` Name string \`json:"name"\` @@ -5600,6 +5705,42 @@ test('typegen: swift', async () => { case name = "name" } } + internal struct DomainTestSelect: Codable, Hashable, Sendable, Identifiable { + internal let id: Int64 + internal let plain: AnyJSON? + internal let quantity: Int32 + internal let search: AnyJSON? + internal enum CodingKeys: String, CodingKey { + case id = "id" + case plain = "plain" + case quantity = "quantity" + case search = "search" + } + } + internal struct DomainTestInsert: Codable, Hashable, Sendable, Identifiable { + internal let id: Int64? + internal let plain: AnyJSON? + internal let quantity: Int32 + internal let search: AnyJSON? + internal enum CodingKeys: String, CodingKey { + case id = "id" + case plain = "plain" + case quantity = "quantity" + case search = "search" + } + } + internal struct DomainTestUpdate: Codable, Hashable, Sendable, Identifiable { + internal let id: Int64? + internal let plain: AnyJSON? + internal let quantity: Int32? + internal let search: AnyJSON? + internal enum CodingKeys: String, CodingKey { + case id = "id" + case plain = "plain" + case quantity = "quantity" + case search = "search" + } + } internal struct EmptySelect: Codable, Hashable, Sendable { } internal struct EmptyInsert: Codable, Hashable, Sendable { @@ -6131,6 +6272,42 @@ test('typegen: swift w/ public access control', async () => { case name = "name" } } + public struct DomainTestSelect: Codable, Hashable, Sendable, Identifiable { + public let id: Int64 + public let plain: AnyJSON? + public let quantity: Int32 + public let search: AnyJSON? + public enum CodingKeys: String, CodingKey { + case id = "id" + case plain = "plain" + case quantity = "quantity" + case search = "search" + } + } + public struct DomainTestInsert: Codable, Hashable, Sendable, Identifiable { + public let id: Int64? + public let plain: AnyJSON? + public let quantity: Int32 + public let search: AnyJSON? + public enum CodingKeys: String, CodingKey { + case id = "id" + case plain = "plain" + case quantity = "quantity" + case search = "search" + } + } + public struct DomainTestUpdate: Codable, Hashable, Sendable, Identifiable { + public let id: Int64? + public let plain: AnyJSON? + public let quantity: Int32? + public let search: AnyJSON? + public enum CodingKeys: String, CodingKey { + case id = "id" + case plain = "plain" + case quantity = "quantity" + case search = "search" + } + } public struct EmptySelect: Codable, Hashable, Sendable { } public struct EmptyInsert: Codable, Hashable, Sendable { @@ -6814,6 +6991,24 @@ test('typegen: python', async () => { duration_required: NotRequired[Annotated[str, Field(alias="duration_required")]] id: NotRequired[Annotated[int, Field(alias="id")]] + class PublicDomainTest(BaseModel): + id: int = Field(alias="id") + plain: Optional[Json[Any]] = Field(alias="plain") + quantity: int = Field(alias="quantity") + search: Optional[Json[Any]] = Field(alias="search") + + class PublicDomainTestInsert(TypedDict): + id: NotRequired[Annotated[int, Field(alias="id")]] + plain: NotRequired[Annotated[Optional[Json[Any]], Field(alias="plain")]] + quantity: Annotated[int, Field(alias="quantity")] + search: NotRequired[Annotated[Optional[Json[Any]], Field(alias="search")]] + + class PublicDomainTestUpdate(TypedDict): + id: NotRequired[Annotated[int, Field(alias="id")]] + plain: NotRequired[Annotated[Optional[Json[Any]], Field(alias="plain")]] + quantity: NotRequired[Annotated[int, Field(alias="quantity")]] + search: NotRequired[Annotated[Optional[Json[Any]], Field(alias="search")]] + class PublicCategory(BaseModel): id: int = Field(alias="id") name: str = Field(alias="name")