Skip to content

Commit 6780555

Browse files
author
Sævar Berg
committed
fix(target-postgres): brand Timestamp(tz)<P> on Date, not string
The pg/timestamp@1 and pg/timestamptz@1 codecs are declared Codec<…, Date, Date, …> and pass the wire value through unchanged, but the emitter-facing Timestamp<P> / Timestamptz<P> aliases were branded on string. For columns with a precision specifier the emitter wrote those branded-string aliases into contract.d.ts, so consumers calling Date methods on a projected timestamp(P) / timestamptz(P) field tripped a TS error even though the runtime value was a Date. Re-base both aliases on a new BrandedDate helper so the type matches the codec declaration. Other parameterized aliases (Char/Varchar/Numeric /Bit/VarBit/Time/Timetz/Interval) correctly stay branded on string — their codecs decode to string. Add a .test-d.ts that pins both directions plus the alias-vs-CodecInput agreement, so future drift between the codec generic and the emitter alias gets caught at typecheck time. Closes TML-2391
1 parent 57c0007 commit 6780555

2 files changed

Lines changed: 54 additions & 2 deletions

File tree

packages/3-targets/3-targets/postgres/src/exports/codec-types.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Branded<T, Shape extends Record<string, unknown>> = T & {
2727
};
2828

2929
type BrandedString<Shape extends Record<string, unknown>> = Branded<string, Shape>;
30+
type BrandedDate<Shape extends Record<string, unknown>> = Branded<Date, Shape>;
3031

3132
export type Char<N extends number> = BrandedString<{ __charLength: N }>;
3233
export type Varchar<N extends number> = BrandedString<{ __varcharLength: N }>;
@@ -36,10 +37,14 @@ export type Numeric<P extends number, S extends number | undefined = undefined>
3637
}>;
3738
export type Bit<N extends number> = BrandedString<{ __bitLength: N }>;
3839
export type VarBit<N extends number> = BrandedString<{ __varbitLength: N }>;
39-
export type Timestamp<P extends number | undefined = undefined> = BrandedString<{
40+
// `Timestamp` / `Timestamptz` brand `Date` — the `pg/timestamp@1` and
41+
// `pg/timestamptz@1` codecs decode to `Date`. Branding `string` here would
42+
// contradict the codec's declared input/output and force consumers to cast
43+
// before calling Date methods on a projected column.
44+
export type Timestamp<P extends number | undefined = undefined> = BrandedDate<{
4045
__timestampPrecision: P;
4146
}>;
42-
export type Timestamptz<P extends number | undefined = undefined> = BrandedString<{
47+
export type Timestamptz<P extends number | undefined = undefined> = BrandedDate<{
4348
__timestamptzPrecision: P;
4449
}>;
4550
export type Time<P extends number | undefined = undefined> = BrandedString<{ __timePrecision: P }>;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { CodecInput } from '@prisma-next/sql-relational-core/ast';
2+
import { expectTypeOf, test } from 'vitest';
3+
import type { codecDefinitions } from '../src/core/codecs';
4+
import type {
5+
Char,
6+
Numeric,
7+
Time,
8+
Timestamp,
9+
Timestamptz,
10+
Varchar,
11+
} from '../src/exports/codec-types';
12+
13+
// Branded aliases must agree with their codec's declared input/output type.
14+
// `pgTimestamp(tz)Codec` decodes to `Date`, so `Timestamp<P>` / `Timestamptz<P>`
15+
// must be Date-shaped — calling Date methods on a projected column must
16+
// typecheck without casts. See `core/codecs.ts:332-400`.
17+
18+
test('Timestamp<P> brand is Date-shaped', () => {
19+
expectTypeOf<Timestamp<3>>().toExtend<Date>();
20+
expectTypeOf<Timestamp>().toExtend<Date>();
21+
expectTypeOf<Timestamp<3>>().not.toExtend<string>();
22+
});
23+
24+
test('Timestamptz<P> brand is Date-shaped', () => {
25+
expectTypeOf<Timestamptz<6>>().toExtend<Date>();
26+
expectTypeOf<Timestamptz>().toExtend<Date>();
27+
expectTypeOf<Timestamptz<6>>().not.toExtend<string>();
28+
});
29+
30+
test('Timestamp/Timestamptz brand agrees with codec input/output type', () => {
31+
type TsInput = CodecInput<typeof codecDefinitions.timestamp.codec>;
32+
type TstzInput = CodecInput<typeof codecDefinitions.timestamptz.codec>;
33+
expectTypeOf<TsInput>().toEqualTypeOf<Date>();
34+
expectTypeOf<TstzInput>().toEqualTypeOf<Date>();
35+
expectTypeOf<Timestamp<3>>().toExtend<TsInput>();
36+
expectTypeOf<Timestamptz<6>>().toExtend<TstzInput>();
37+
});
38+
39+
// Sanity: the other parameterized aliases stay string-shaped, because
40+
// their codecs decode to string.
41+
42+
test('string-shaped parameterized aliases are unchanged', () => {
43+
expectTypeOf<Char<16>>().toExtend<string>();
44+
expectTypeOf<Varchar<255>>().toExtend<string>();
45+
expectTypeOf<Numeric<10, 2>>().toExtend<string>();
46+
expectTypeOf<Time<3>>().toExtend<string>();
47+
});

0 commit comments

Comments
 (0)