tomjaguarpaw / haskell-opaleye

Other
599 stars 115 forks source link

fromPGS equivalent for toFields #593

Open tysonzero opened 1 month ago

tysonzero commented 1 month ago

In general it seems like there are a fair amount of asymmetries between FromFields and ToFields, including the special DefaultFromField class, but the asymmetry I'm most concerned with right now is the lack of a fromPGSToField and/or fromPGSFieldRenderer. It seems like without it I have to reach for unsafeCast or Internal modules to serialize a custom type (in my case for PostGIS types). Perhaps with a tutorial (#592) I'll realize i'm missing something, but after following the types around a bit I'm unsure how to get the results I want without unsafe/Internal functions.

tomjaguarpaw commented 1 month ago

Asymmetry

Yeah, the DefaultFromField asymmetry comes specifically from the days before Field Nullable sqlType/Field NonNullable sqlType when the nullable/non-nullable distinction looked like Column (Maybe sqlType)/Column sqlType. We needed the instance

DefaultFromField a b => DefaultFromField (Nullable a) (Maybe b)

to automatically generate the code for decoding nullable field types. Since moving from Column to Field we don't actually need DefaultFromField or FromField any more. However, it's very difficult to smoothly transition between type class hierarchies in Haskell, so I think we stuck with it, short of coming up with a whole parallel hierarchy of functions Opaleye.RunSelect2.

Creating ToFields

It seems like without it I have to reach for unsafeCast or Internal modules to serialize a custom type (in my case for PostGIS types).

Have you found a way of doing this at all? As far as I know the only way that would work currently would be to serialise to a SqlText and then use unsafeCast on the value (and unsafeCoerceField on the type). Is that what you're doing?

I agree that it would be good to have fromPGSToField (or, what I think should be called, fromGPSAction). Would you like to make a PR? Alternatively I'll look into it when I get some time.

tomjaguarpaw commented 1 month ago

One problem with using postgresql-simple's ToField/Action would be that to render them we ultimately have to use buildAction and that runs in IO (because it contacts the DB itself to do escaping!).

tysonzero commented 1 month ago

However, it's very difficult to smoothly transition between type class hierarchies in Haskell, so I think we stuck with it

No kidding, the books I would happily write on that topic if I had the time. Might be worth throwing something in the docs about the historical reasons but not super high priority IMO.

Have you found a way of doing this at all? As far as I know the only way that would work currently would be to serialise to a SqlText and then use unsafeCast on the value (and unsafeCoerceField on the type). Is that what you're doing?

I didn't need an unsafeCoerceField as unsafeCast was enough, but yes:

instance Default ToFields (V2 Double) (Field SqlGeographyPoint) where
    def = toToFields $ \(V2 x y) -> let
        geo = Geos.PointGeometry (Geos.Point $ Geos.Coordinate2 x y) (Just 4326)
        in unsafeCast "geography(POINT, 4326)" . toFieldsI . T.decodeUtf8Lenient $ Geos.writeHex geo

One problem with using postgresql-simple's ToField/Action would be that to render them we ultimately have to use buildAction and that runs in IO (because it contacts the DB itself to do escaping!).

Hmm, interesting. I mean back to the whole symmetry thing, it's not all that crazy to me that ToFields would be a little more complicated / IO-involved than the current simple function, given how much more machinery and IO is involved for FromFields. Obviously I can imagine significant transition pains though.

tomjaguarpaw commented 1 month ago

I didn't need an unsafeCoerceField as unsafeCast was enough

Ah yes, unsafeCast is already very unsafe :) I should add a safer version of that.

Would something like this be helpful to create the ToFields?

makeToToField ::
  forall haskell sqlType1 sqlType2.
  IsSqlType sqlType2 =>
  (haskell -> Column.Field sqlType1) ->
  C.ToFields haskell (Column.Field sqlType2)

Implemented at: https://github.com/tomjaguarpaw/haskell-opaleye/commit/db98600e2b4a3f6661c0afa9f21d904bd3a72577

I mean back to the whole symmetry thing, it's not all that crazy to me that ToFields would be a little more complicated / IO-involved than the current simple function

I'm still confused why escaping should require a trip to the database. Surely there must be a way of escaping that doesn't require knowing any dynamic property of the database!