circuithub / rel8

Hey! Hey! Can u rel8?
https://rel8.readthedocs.io
Other
154 stars 39 forks source link

Better docs on how to write Projections #131

Open Profpatsch opened 3 years ago

Profpatsch commented 3 years ago

The INSERT/UPSERT docs currently don’t describe how to write a Projection.

Following the type in the reference leads to

type Projection a b = Transpose (Field a) a -> Transpose (Field a) b 

where Transpose is an associated type of the Table class, which has over a dozen instances, so it’s very hard to figure out how a Projection would actually look like in practice.

ocharles commented 3 years ago

Do you think attaching some examples to the Projection type would help? The intuition is it's a function on the columns in the table. For example:

fooAndBar :: Projection (MyTable Expr) (Expr Foo, Expr Bar)
fooAndBar MyTable{ foo, bar } = (foo, bar)

(I think that's right)

Profpatsch commented 3 years ago

Do you think attaching some examples to the Projection type would help? The intuition is it's a function on the columns in the table. For example:

yes, I think there’s so many type classes that are just there for plumbing and subtyping, having examples of all the instances in the places that matter (e.g. concrete types for Projection) would really help.

But definitely also an example in the tutorial.

Profpatsch commented 3 years ago
fooAndBar :: Projection (MyTable Expr) (Expr Foo, Expr Bar)
fooAndBar MyTable{ foo, bar } = (foo, bar)

This definitely doesn’t work for me. First of all, it infers a MyTable Name and I’m pretty sure it also doesn’t simply allow using the fields in a tuple.

This will need a bunch more documentation, nobody can figure out what

class (Transposes (Context a) (Field a) a (Transpose (Field a) a), Transposes (Context a) (Field a) b (Transpose (Field a) b)) => Projecting a b 

means without a few good examples and some semantics.

Profpatsch commented 3 years ago

Staring at the types a bit more, I inferred that I need something from type MyTable (c :: Context) -> ??

?? naively is MyTable (c2 :: Context), but I have no idea how that is a projection if I have to give it all fields.

There is some Transformation and Rep shenanigans going on, but for the life of me I can’t figure out how that translates to a tuple of fields.

What speaks against just making the type of Projection :: (MyTable Expr -> MyTable (Const Bool)) or something along that spirit, so for every field you can tell whether it should be taken into account by setting the field to Const True?

ocharles commented 3 years ago

In lieu of any documentation, can you share some code you have and what you want? I'm happy to help you out with that just to unblock you!

Profpatsch commented 3 years ago

I fell back to Hasql and wrote this statement:

Hasql.Trans.statement
                ( translationKeyId,
                  translationLanguage,
                  translationTranslation
                )
                $ do
                  let sql =
                        [Embed.embedVerbatimBytes|
                     INSERT INTO translation_service.translations
                     (keyid, language, translation)
                     VALUES ($1, $2, $3)
                     -- Every key/language combo can just appear once.
                     -- if there is a conflict, we just override the entry
                     -- with the new translation.
                     ON CONFLICT (keyid, language)
                     DO UPDATE
                     SET
                     translation = EXCLUDED.translation
                   |]
                  let encoder =
                          ( contrazip3
                              (Enc.param (Enc.nonNullable Enc.int8))
                              (Enc.param (Enc.nonNullable Enc.text))
                              (Enc.param (Enc.nonNullable Enc.text))
                          )

The code I mocked up with Rel8 (except for the upsert):

 data TranslationTable f = TranslationTable
  { -- translationId :: Column f TranslationId,
    translationKeyId :: Column f KeyId,
    translationLanguage :: Column f DbLanguageTag,
    translationTranslation :: Column f DbTranslatedString
  }
  deriving stock (Generic)
  deriving anyclass (Rel8able)

 insert $
  Insert
    { into = translationSchema,
      rows =
        ( do
            pure $
              TranslationTable
                { translationKeyId = lit translationKeyId,
                  translationLanguage = lit (at & addTranslationLanguage),
                  translationTranslation = lit (at & addTranslationTranslation)
                }
        ),
      -- if there is a conflict, we just override the entry
      onConflict =
        DoUpdate $
          let index :: TranslationTable Expr -> TranslationTable Expr = \tt -> tt
           in Upsert
                -- TODO: can’t get the UPSERT index working,
                -- see https://github.com/circuithub/rel8/issues/131
                { index = id, -- \(TranslationTable {translationKeyId, translationLanguage}) -> (translationKeyId, translationLanguage),
                  set = \_old new -> new,
                  updateWhere = \_ _ -> lit True
                },
      returning = pure ()
    }

So basically just overwrite the columns with the new set.

ocharles commented 3 years ago

Ok, I think something has broken at some point, because what should work no longer does... Sorry you've ran into this - you're not holding it wrong, we just gave you something broken!