morphismtech / squeal

Squeal, a deep embedding of SQL in Haskell
352 stars 32 forks source link

Upgrade 0.5.1.0 → 0.9.1.3: Query (`Statement`) with generic parameter (formerly using `ToParam`) #353

Open gasi opened 4 months ago

gasi commented 4 months ago

Hi Eitan,

I’m currently doing a big migration from Squeal 0.5.1.0 → 0.9.1.3 and I love it overall, especially how EncodeParams + DecodeRows gets rid of so much boilerplate.

Problem

I was able to figure out 95% of the work, but I get stuck on turning this query that used to take an arbitrary (generic?) parameter (using ToParam), that can convert to a PostgreSQL type, to a Statement with EncodeParams + DecodeRow. I had to revert and hard-code it to PGtext + Text which broke some of my queries that need PGtimestampz + UTCTime:

Statement: selectContentBy

image

Usage: getBy

image

I tried the following without luck:

selectContentBy ::
  (ToParam Schemas pgty hsty, SOP.Generic hsty) =>
  ( TableExpression 'Ungrouped '[] '[] Schemas '[pgty] from ->
    TableExpression 'Ungrouped '[] '[] Schemas '[pgty] from
  ) ->
  Statement Schemas hsty Content
selectContentBy clauses = Query encode decode sql
  where
    encode :: EncodeParams Schemas pgty hsty
    encode = aParam
    decode = decodeContentWithImage
    sql = -- ...

as I get this error ❌:

zoomhub/src/ZoomHub/Storage/PostgreSQL/Internal.hs:180:33: error: [GHC-18872]
    • Couldn't match kind ‘NullType’ with ‘*’
      When matching types
        pgty0 :: [*]
        '[pgty] :: [NullType]
      Expected: EncodeParams
                  '["public"
                    ::: [ZoomHub.Storage.PostgreSQL.Schema.Schema0.ConfigTable0,
                         ZoomHub.Storage.PostgreSQL.Schema.Schema3.ContentTable3,
                         ZoomHub.Storage.PostgreSQL.Schema.Schema0.ImageTable0,
                         ZoomHub.Storage.PostgreSQL.Schema.Schema0.FlickrTable0]]
                  '[pgty]
                  hsty
        Actual: EncodeParams Schemas pgty0 hsty
    • In the first argument of ‘Query’, namely ‘encode’
      In the expression: Query encode decode sql
      In an equation for ‘selectContentBy’:
          selectContentBy clauses
            = Query encode decode sql
            where
                encode :: EncodeParams Schemas pgty hsty
                encode = aParam
                decode = decodeContentWithImage
                sql = -- ...

I was thinking it might be something obvious you could spot right away. If not, I’m happy to go back and do the work to create a minimally reproducing example.

I appreciate your help 😄

echatav commented 4 months ago

Hmmm. Looks like GHC is inferring the kind of pgty0 too monomorphically, too soon. Can you try turning on PolyKinds? See if that helps. That's what the error poetry tells me:

    • Couldn't match kind ‘NullType’ with ‘*’
      When matching types
        pgty0 :: [*]
        '[pgty] :: [NullType]
echatav commented 4 months ago

Or perhaps there is a mistake in the type annotation of your encode term.

-    encode :: EncodeParams Schemas pgty hsty
+    encode :: EncodeParams Schemas '[pgty] hsty

Turning on ScopedTypeVariables should let GHC identify this pgty with the one in the type annotation of selectContentBy.

echatav commented 4 months ago

It might typecheck if you just removed the type signature for encode entirely and let GHC infer it.

gasi commented 4 months ago

@echatav Thank you so much for taking the time to look into this. I tried your suggestions, but unfortunately, I wasn’t able to resolve the error.

To make it easier for you and potentially contribute to Squeal with an example of a parametrized query, I created this draft PR with an extension of the existing Example.hs with a parametrized query: https://github.com/morphismtech/squeal/pull/354/

Does this help you repro and potentially resolve the issue?

Thanks 🙇‍♂️


My dream is that one day a GitHub search for EncodeParams, DecodeRow, consRow, appendRows, etc. will show lots of examples of Squeal in action helping others enjoy this wonderful library. Hopefully figuring out the upgrade in https://github.com/zoomhub/zoomhub/pull/235 will be a first such example ✨

gasi commented 4 months ago

P.S. I let GPT-4 loose on this, but Squeal type errors are too powerful for our AI overlords 😜

echatav commented 4 months ago

I've kind of been stuck working on Squeal because every time I try I run into GHC/Postgres installation location problems.

squeal-postgresql          > build (lib + exe)
squeal-postgresql          > Preprocessing library for squeal-postgresql-0.9.1.3..
squeal-postgresql          > Building library for squeal-postgresql-0.9.1.3..
squeal-postgresql          > ld: warning: -single_module is obsolete
squeal-postgresql          > ld: warning: search path '/opt/homebrew/opt/libpq/lib' not found
squeal-postgresql          > ld: library 'pq' not found
squeal-postgresql          > clang: error: linker command failed with exit code 1 (use -v to see invocation)
squeal-postgresql          > `gcc' failed in phase `Linker'. (Exit code: 1)

Googling hasn't helped me. I hate computers. If you help me figure out how I can get GHC to find LibPQ then I can help with Squeal again.

gasi commented 4 months ago

@echatav Re: libpq. Ouch, I’m sorry! Are you on macOS / Apple Silicon? I had similar issues and it took me a while to resolve. In fact, that’s what that whole stack LTS upgrade is about (I got a new MacBook laptop last year.) If you want, I’d be happy to hop on a Zoom call with you and try and help out. Shoot me an email to daniel@gasienica.ch if you are interested 😄

Re: generic SELECT. I partially figured it out!

  1. First, it was through discovering ~ for type equality in your docs (but I had to enable AllowAmbiguousTypes 😢): https://github.com/morphismtech/squeal/pull/354/commits/3a1851b62fab908c5bc7d213de26cb3c556c1019
  2. Then I found out I can drop pgty and infer it from hsty: https://github.com/morphismtech/squeal/pull/354/commits/168813cb68626f8c826e5f3e4abe6754b28cbdce
  3. Bonus: Added encoding/decoding custom type to example (however, I haven’t looked into how to make it type-safe yet): https://github.com/morphismtech/squeal/pull/354/commits/21057aa06d19cbbc6b63be7b209918b3d0c25d17

I actually was able to get it working first in my own code (without even needing AllowAmbiguousTypes 🤷‍♂️): https://github.com/zoomhub/zoomhub/pull/235/commits/c297109d4527704a0c6868f497f1e3add9026c93#diff-d0b2f0351a2e5459c9b6306ed066f9e76873900c6bfc62fbc24dc5c2cad2f036R219-R232

I’m generally pretty happy about it except for this long from type signature and the need for OidOfNull Schemas. I originally had from inferred using _ + PartialTypeSignatures, but that started causing problems. Suggestions on how to improve it are welcome: https://github.com/zoomhub/zoomhub/pull/235/commits/c297109d4527704a0c6868f497f1e3add9026c93#diff-d0b2f0351a2e5459c9b6306ed066f9e76873900c6bfc62fbc24dc5c2cad2f036R182-R217

echatav commented 4 months ago

@gasi Nice job! Yes, I'm on a macbook. I'll send you an email. I've been wanting to get back to Squeal a bit.

echatav commented 4 months ago

Re: type safety. I guess you mean for OrganizationType on the Haskell side having a corresponding Postgres type so the type column isn't stringly-typed. For that you can use Postgres enum types. Squeal even has special support for creating Postgres enums from a Haskell type like so:

>>> :{
data Schwarma = Beef | Lamb | Chicken
  deriving stock GHC.Generic
  deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo)
  deriving (IsPG, FromPG) via Enumerated Schwarma
:}
>>> :{
let
  createSchwarma :: Definition (Public '[]) '["public" ::: '["schwarma" ::: 'Typedef (PG Schwarma)]]
  createSchwarma = createTypeEnumFrom @Schwarma #schwarma
in
  printSQL createSchwarma
:}
CREATE TYPE "schwarma" AS ENUM ('Beef', 'Lamb', 'Chicken');