Skip to content

Commit 5b1bc24

Browse files
committed
Postgres TPU tests for custom-mapped Point type.
1 parent fd1b4b1 commit 5b1bc24

5 files changed

Lines changed: 131 additions & 0 deletions

File tree

src/TypeProviderUsers/TypeProviderUser.Postgres.UserTypes/Library.fs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,23 @@ type Address =
2525
box (JsonSerializer.Serialize(a))
2626
static member FromPrimitive(o : obj) : Address =
2727
JsonSerializer.Deserialize<Address>(o :?> string)
28+
29+
/// 2D point as a user primitive that stores as PG `point`. Where Address
30+
/// exercises an obj-underlying type whose value carries the column data as
31+
/// a string, Point2D exercises an obj-underlying type whose value is a
32+
/// driver-specific struct (NpgsqlPoint) — Npgsql's native CLR
33+
/// representation for the `point` backend type. This proves the
34+
/// System.Object escape hatch also handles non-string driver values.
35+
// Note: 15 = NpgsqlTypes.NpgsqlDbType.Point (Npgsql 8.x). Hardcoded
36+
// as a literal for the same attribute-argument reason as Jsonb above.
37+
[<RawBackendSQLType("point")>]
38+
[<SQLParameterDbType("NpgsqlDbType", 15)>]
39+
type Point2D =
40+
{ X : double
41+
Y : double
42+
}
43+
static member ToPrimitive(p : Point2D) : obj =
44+
box (NpgsqlTypes.NpgsqlPoint(p.X, p.Y))
45+
static member FromPrimitive(o : obj) : Point2D =
46+
let pt = o :?> NpgsqlTypes.NpgsqlPoint
47+
{ X = pt.X; Y = pt.Y }

src/TypeProviderUsers/TypeProviderUser.Postgres/Shared.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type TestModel = SQLModel<".">
1313
type CleanTestData = SQL<"""
1414
vendor postgres {
1515
drop table if exists __RZSQL_MIGRATIONS;
16+
drop table if exists UserLocations;
1617
drop table if exists UserAddresses;
1718
drop table if exists ArticleComments;
1819
drop table if exists Articles;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
module TypeProviderUser.Postgres.TestUserPrimitivePoint
2+
open NUnit.Framework
3+
open Rezoom.SQL
4+
open Rezoom.SQL.Raw
5+
open TypeProviderUser.Postgres.UserTypes
6+
7+
// Point2D maps to PG's `point` type via the System.Object escape hatch
8+
// with NpgsqlPoint as the driver value (not a string), exercising a
9+
// different shape from the jsonb/Address case in TestUserPrimitiveSystemObject.
10+
11+
let private homerPoint = { X = 1.5; Y = 2.5 }
12+
let private margePoint = { X = 1.5; Y = 2.5 }
13+
let private bartPoint = { X = -7.25; Y = 99.0 }
14+
15+
type InsertAndSelectPoints = SQL<"""
16+
insert into UserLocations(UserId, Coord)
17+
values((select Id from Users where Name = 'Homer'), @homer);
18+
insert into UserLocations(UserId, Coord)
19+
values((select Id from Users where Name = 'Marge'), @marge);
20+
select Coord from UserLocations order by Id;
21+
""">
22+
23+
[<Test>]
24+
let ``select roundtrips a Point2D user primitive over PG point`` () =
25+
let results = InsertAndSelectPoints.Command(homerPoint, margePoint) |> runOnTestData
26+
Assert.AreEqual(2, results.Count)
27+
Assert.AreEqual(homerPoint, results.[0].Coord)
28+
Assert.AreEqual(margePoint, results.[1].Coord)
29+
30+
// PG's point type has no `=` operator (42883: "operator does not
31+
// exist: point = point"). Equality is `~=` (the same-as operator),
32+
// which Rezoom's parser doesn't know — unsafe_inject_raw is the
33+
// idiomatic escape hatch here. The parameter @needle still binds
34+
// through Rezoom as a Point2D, then PG's ~= compares it against the
35+
// column value at row scan time, exercising the full
36+
// parameter-as-point pipeline.
37+
type FindPointByParameterSameAs = SQL<"""
38+
insert into UserLocations(UserId, Coord)
39+
values((select Id from Users where Name = 'Homer'), @homer);
40+
insert into UserLocations(UserId, Coord)
41+
values((select Id from Users where Name = 'Marge'), @bart);
42+
select Coord from UserLocations ul where unsafe_inject_raw(@filter);
43+
""">
44+
45+
[<Test>]
46+
let ``select with Point2D parameter equality matches via PG ~= operator`` () =
47+
// Identifiers are emitted unquoted by Rezoom's PG backend, so PG
48+
// folds them lowercase — `ul.coord`, not `"Coord"`.
49+
//
50+
// Caveat: Rezoom.SQL.Raw.arg does not apply user-type ToPrimitive
51+
// translation — it routes the value straight to ADO.NET with a
52+
// guessed DbType. So we cannot pass a Point2D here and expect the
53+
// Point2D → NpgsqlPoint conversion to happen automatically. We
54+
// pre-convert to NpgsqlPoint in user space; Npgsql then
55+
// auto-detects the wire format from the value's runtime type.
56+
// The fully-translated user-type → parameter pipeline is already
57+
// exercised by the INSERT in the roundtrip test above; this test
58+
// covers the WHERE-side parameter comparison via ~=.
59+
let needle = NpgsqlTypes.NpgsqlPoint(homerPoint.X, homerPoint.Y)
60+
let results =
61+
FindPointByParameterSameAs.Command
62+
( bart = bartPoint
63+
, filter = [| sql "ul.coord ~= "; arg needle |]
64+
, homer = homerPoint
65+
)
66+
|> runOnTestData
67+
Assert.AreEqual(1, results.Count)
68+
Assert.AreEqual(homerPoint, results.[0].Coord)
69+
70+
// Same functional intent as FindPointByParameterSameAs above, but using
71+
// the vendor/imagine escape hatch instead of unsafe_inject_raw. The
72+
// IMAGINE clause is typechecked against Rezoom's dialect, informing the
73+
// typechecker that @needle is a Point2D and that the result set has a
74+
// Coord column. The vendor body runs PG-native SQL — including ~= and
75+
// the `{@needle}` extra-brace param reference — and the user-type
76+
// translation pipeline still fires for @needle on the parameter side,
77+
// so the caller passes a real Point2D, not a NpgsqlPoint, from F#.
78+
type FindPointByParameterVendor = SQL<"""
79+
insert into UserLocations(UserId, Coord)
80+
values((select Id from Users where Name = 'Homer'), @homer);
81+
insert into UserLocations(UserId, Coord)
82+
values((select Id from Users where Name = 'Marge'), @bart);
83+
vendor postgres {
84+
select Coord from UserLocations where coord ~= {@needle}
85+
} imagine {
86+
select Coord from UserLocations where Coord = @needle
87+
};
88+
""">
89+
90+
[<Test>]
91+
let ``select with Point2D parameter equality matches via vendor ~= with IMAGINE`` () =
92+
// No manual NpgsqlPoint conversion: @needle stays typed as Point2D
93+
// all the way through Rezoom, so the user-type SQLParameterDbType
94+
// attribute is applied to the actual parameter being compared.
95+
let results =
96+
FindPointByParameterVendor.Command
97+
( bart = bartPoint
98+
, homer = homerPoint
99+
, needle = homerPoint
100+
)
101+
|> runOnTestData
102+
Assert.AreEqual(1, results.Count)
103+
Assert.AreEqual(homerPoint, results.[0].Coord)

src/TypeProviderUsers/TypeProviderUser.Postgres/TypeProviderUser.Postgres.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<Compile Include="Shared.fs" />
1818
<Compile Include="TestSelects.fs" />
1919
<Compile Include="TestUserPrimitiveSystemObject.fs" />
20+
<Compile Include="TestUserPrimitivePoint.fs" />
2021
<Compile Include="TestMigrateConnectionString.fs" />
2122
<Compile Include="Program.fs" />
2223
<None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />

src/TypeProviderUsers/TypeProviderUser.Postgres/V1.model.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,9 @@ create table UserAddresses
3636
, Home Address
3737
);
3838

39+
create table UserLocations
40+
( Id int64 primary key autoincrement
41+
, UserId int64 references Users(Id)
42+
, Coord Point2D
43+
);
44+

0 commit comments

Comments
 (0)