Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Saurabh's tutorial to the README #447

Open
tomjaguarpaw opened this issue Dec 19, 2019 · 48 comments
Open

Add Saurabh's tutorial to the README #447

tomjaguarpaw opened this issue Dec 19, 2019 · 48 comments

Comments

@tomjaguarpaw
Copy link
Owner

Requested by @williamyaoh. I'll have to refamiliarise myself with it.

https://haskell-webapps.readthedocs.io/en/latest/docs/opaleye/opaleye.html

@saurabhnanda
Copy link
Contributor

saurabhnanda commented Dec 19, 2019 via email

@tomjaguarpaw
Copy link
Owner Author

should i spruce this up for the latest version?

Updating to use the new names would be great.

@tomjaguarpaw
Copy link
Owner Author

@saurabhnanda Is the source code for your tutorial available in a repo? If so I will try to bring it up to date with current Opaleye.

@saurabhnanda
Copy link
Contributor

saurabhnanda commented Aug 28, 2020 via email

@saurabhnanda
Copy link
Contributor

@tomjaguarpaw I finally got around to finishing my static site builder and am now revamping the tutorial. Even when I was writing the first one, I was unable to explain why the optional/required annotations are required when setting up a Table:

userTable :: Table (Field SqlInt4, Field SqlText, Field SqlText)
                   (Field SqlInt4, Field SqlText, Field SqlText)
userTable = Table "users" (p3 ( required "id"
                              , required "name"
                              , required "email"))

Aren't the type annotations enough? Is it even possible to have a required field that corresponds to a Maybe (Field a) in Haskell? Or to have an optional field that does not correspond to a Maybe (Field a)?

While we are cleaning-up (and introducing breaking changes), would it be possible to re-consider this design decision?

@saurabhnanda
Copy link
Contributor

I'm getting used to the new API myself, I think my previous comment should read as Field a vs FieldNullable a, right?

@saurabhnanda
Copy link
Contributor

the API has changed significantly since 0.5-0.6 that we are using in production. Is runInsert_ really the user-facing API now? It requires a user to understand many data-types to construct an INSERT statement.

Also, iOnConflict should be OnConflict instead of Maybe OnConflict and OnConflict should be changed to:

data OnConflict = OnConflictOmit | OnConflictDoNothing

@tomjaguarpaw
Copy link
Owner Author

I was unable to explain why the optional/required annotations are required when setting up a Table

They're not required. If you use tableField then it will infer optional vs required based on the type annotations.

@tomjaguarpaw
Copy link
Owner Author

Is runInsert_ really the user-facing API now?

Yes it is. We have some freedom to come up with a better API because the the old version of runInsert has been has been removed, and runUpdate and runInsert are deprecated and will be removed in the next version. We can use those names for a better API if we can think of one.

@tomjaguarpaw
Copy link
Owner Author

tomjaguarpaw commented Sep 1, 2020

I'm getting used to the new API myself, I think my previous comment should read as Field a vs FieldNullable a, right?

The API hasn't fully switched to the Field_/Field/FieldNullable nomenclature yet, but when it does the types will be

requiredTableField :: String -> TableFields (Field_ n a) (Field_ n a)

optionalTableField :: String -> TableFields (Maybe (Field_ n a)) (Field_ n a)

That is, the Field_ type has nullability as one of its type parameters (and the SQL type as the other). Field will be a synonym Field NonNullable and FieldNullable will be a synonym for Field Nullable.

@saurabhnanda
Copy link
Contributor

Is there any reason why Returning a b doesn't have a ReturnNothing constructor? Does Postgres insist on returning something in every INSERT statement?

@saurabhnanda
Copy link
Contributor

saurabhnanda commented Sep 2, 2020

I had this note in my old tutorial. Is there a technical explanation for why the API behaves this way (runSelect doesn't force you to call fromFields on its output, but runInsert_ does)?

.. note:: Strangely, while runSelect converts DB => Haskell types automagically, runInsertMany and runUpdate refuse to do Haskell => DB conversions on their own. Hence the need to do it explicitly when using these functions.

Also, in a tutorial, how does one explain why some API functions have a trailing underscore, while others don't?

@saurabhnanda
Copy link
Contributor

I'm trying to rediscover Opaleye as a beginner, to be able to write an effective tutorial.

When I look at runUpdate_ the following note is very puzzling:

Be careful: providing Nothing to a field created by optional updates the field to its default value. Many users have been confused by this because they assume it means that the field is to be left unchanged. For an easier time wrap your update function in updateEasy.

Is there a valid (and extremely common) use-case for updating a row with colName = DEFAULT? If not, can we make following the default behaviour, and also emit smaller SQL statements as a side-effect?

runUpdate_ conn Update
  { uTable = sometable
  , uUpdateWith = (\r -> r & _3 .~ (toFields "someText"))
  , uWhere = whatever
  , uReturning = rCount
  }

Generated SQL:

UPDATE sometable
SET col3 = "someText" -- all other columns are omitted
WHERE whatever
RETURNING count(*)

@tomjaguarpaw
Copy link
Owner Author

Is there any reason why Returning a b doesn't have a ReturnNothing constructor? Does Postgres insist on returning something in every INSERT statement?

Yeah, we could add the ability to return nothing (or more precisely, ()).

@tomjaguarpaw
Copy link
Owner Author

how does one explain why some API functions have a trailing underscore, while others don't?

runInsert_/runUpdate_/runDelete_ have a training underscore because there were already (deprecated) functions called runInsert/runUpdate/runDelete. When the latter are removed the new functions can take their place.

@tomjaguarpaw
Copy link
Owner Author

I had this note in my old tutorial. Is there a technical explanation for why the API behaves this way (runSelect doesn't force you to call fromFields on its output, but runInsert_ does)?

I'm not sure what you mean. How does runInsert_ force you to call fromFields on its output? Do you mean toFields on its input?

@saurabhnanda
Copy link
Contributor

I'm not sure what you mean. How does runInsert_ force you to call fromFields on its output? Do you mean toFields on its input?

Let me rephrase. runSelect doesn't force you to do convert from DB => Haskell manually, whereas runInsert_ forces you to convert from Haskell => DB manually.

@tomjaguarpaw
Copy link
Owner Author

Is there a valid (and extremely common) use-case for updating a row with colName = DEFAULT?

No, I don't think so.

If not, can we make following the default behaviour, and also emit smaller SQL statements as a side-effect?

The current API is a consequence of the structure of the Table type. At the moment a table definition has type Table fieldsWrite fieldsRead. fieldsWrite is used for both UPDATEs and INSERTs. INSERTs must provide all columns and must have the ability to provide DEFAULT, therefore UPDATEs have this property too.

The API you propose is achievable with updateEasy isn't it? I think you can write

runUpdate_ conn Update
  { uTable = sometable
  , uUpdateWith = updateEasy (\r -> r & _3 .~ (toFields "someText"))
  , uWhere = whatever
  , uReturning = rCount
  }

This still generates code for all fields. It will be something like

UPDATE sometable
SET col1 = col1, col2 = col2, col3 = "someText", col4 = col4
WHERE whatever
RETURNING count(*)

but I suppose I can detect unchanged columns and leave them out.

@saurabhnanda
Copy link
Contributor

but I suppose I can detect unchanged columns and leave them out.

Hang on, I think I just got confused. I wrote the following example in the tutorial and I think it can be written with \r -> r & _3 .~ toField x technique without involving updateEasy.

updateUser :: Connection             
           -> (Int, String, String, Maybe Day) 
           -> IO ()
updateUser conn (i, n, e, d) = 
  void $ runUpdate_ conn u
  where
    u = Update 
      { uTable = userTable  
      , uUpdateWith = (\(iDb, _, _, _) -> (iDb, toFields n, toFields e, toFields d))
      , uWhere = (\(iDb, _, _, _) -> iDb .== toFields i)
      , uReturning = rCount
      }

Is updateEasy required only when tableW and tableR are different? And does the Default Updater machinery take care of readOnly fields?

@tomjaguarpaw
Copy link
Owner Author

Let me rephrase. runSelect doesn't force you to do convert from DB => Haskell manually, whereas runInsert_ forces you to convert from Haskell => DB manually.

Right, so the way I see this is that runSelect doesn't allow you to do DB => Haskell manually; Fields produced by a Select can't live outside the Select. On the other hand runInsert_ does allow you to create the fields yourself if you want to.

I think of this as a "lower level" API that other APIs can be written in terms of. If you think there's a nicer higher-level API then let's see what that could be.

@tomjaguarpaw
Copy link
Owner Author

tomjaguarpaw commented Sep 2, 2020

And does the Default Updater machinery take care of readOnly fields?

Unfortunately readOnly fields are completely broken for UPDATEs, and I only realised this recently. The behaviour of a readOnly field is to always use DEFAULT whenever you write to it. This behaviour is correct for inserts but not what you want for UPDATEs. I'm not sure what to do about this. Maybe redesigning the Table type is the right thing to do.

@tomjaguarpaw
Copy link
Owner Author

Is updateEasy required only when tableW and tableR are different?

Correct.

@tomjaguarpaw
Copy link
Owner Author

Is updateEasy required only when tableW and tableR are different?

Correct.

And more precisely, this occurs exactly when some fields are optionalTableFields.

@saurabhnanda
Copy link
Contributor

And more precisely, this occurs exactly when some fields are optionalTableFields.

And readOnlyFields?

@tomjaguarpaw
Copy link
Owner Author

Yes, true, but there isn't an appropriate instance to make Updater work with readOnlyTableFields and there probably should never be because as mentioned above readOnlyTableField doesn't make sense for updating. I should probably deprecate readOnlyTableField or at least put on a big warning.

@saurabhnanda
Copy link
Contributor

Btw - I've updated the first chapter of the tutorial, and it's available at https://www.haskelltutorials.com/opaleye/instant-gratification.html -- still working on the rest. Targeting to update and publish one chapter every 2 days, or so.

@saurabhnanda
Copy link
Contributor

Yes, true, but there isn't an appropriate instance to make Updater work with readOnlyTableFields and there probably should never be because as mentioned above readOnlyTableField doesn't make sense for updating. I should probably deprecate readOnlyTableField or at least put on a big warning.

We use our own hacked-up version of read-only fields a lot in production. We use it for id, created_at and updated_at columns all throughout our app.

@tomjaguarpaw
Copy link
Owner Author

We use our own hacked-up version of read-only fields a lot in production. We use it for id, created_at and updated_at columns all throughout our app.

Very cool idea. Would be good to see how you did this.

@saurabhnanda
Copy link
Contributor

saurabhnanda commented Sep 3, 2020

@tomjaguarpaw
Copy link
Owner Author

Yes, looks approximately correct. It would be more correct to say that it is to do with the Default instance (which is implemented in terms of p1 ... p62) but what you have is probably close enough.

Also, there's a typo in "stange".

@saurabhnanda
Copy link
Contributor

@tomjaguarpaw I've completed one more chapter in the tutorial (and also ended up creating a bunch of other TODOs in the process!). Any feedback on this - https://www.haskelltutorials.com/opaleye/implementation-guide.html ?

@ocharles
Copy link
Collaborator

ocharles commented Sep 13, 2020 via email

@saurabhnanda
Copy link
Contributor

This is no longer true, and Opaleye can now be used through just it's Functor/Applicative/Monad interface, with no loss of functionality

How did I miss this! When did this happen?

@ocharles
Copy link
Collaborator

ocharles commented Sep 13, 2020 via email

@saurabhnanda
Copy link
Contributor

Let me update my code samples in that case. Is any interface preferred? Any known drawbacks of the new Monad interface?

@ocharles any other feedback on the implementation guide? Does this match your experience of using opaleye in production?

@ocharles
Copy link
Collaborator

ocharles commented Sep 13, 2020 via email

@tomjaguarpaw
Copy link
Owner Author

Let me update my code samples in that case. Is any interface preferred? Any known drawbacks of the new Monad interface?

The monadic API adds LATERAL to every join. It's not certain what the performance impact but @ocharles and @duairc have run their benchmarks on it and not found any degradation.

@ocharles
Copy link
Collaborator

I'd say it's more than just benchmarks - we run with a fork that literally adds LATERAL everywhere it's permitted in production and I don't think we've noticed any performance degradation. OTOH, we've exploited this for performance improvements, especially when joining an aggregation against every row (as we can directly feed a row into an aggregation, rather than having to produce a common column to join on)

@tomjaguarpaw
Copy link
Owner Author

@saurabhnanda Very nice indeed! Just a few comments below.

By the way, I implemented a sample database for Munihac. I'd like to tidy it up and publish it more officially as a suite of Opaleye examples.

Using ad-hoc joins will force you to make one of one of the following choices. If you want to understand them in detail please read Opaleye Query Tutorial

MaybeFields, optional and optionalRestrict are new alternatives that provide a LEFT JOIN API. They are much more convenient to use than the old LEFT JOIN API.

Another minor disadvantage could be the dependency on using the Arrows language extension. Some people still consider it to be highly experimental.

As Ollie pointed out, there is a Monad interface now.

It is strongyly recommend

Small typo here: should be "strongly recommended"

@saurabhnanda
Copy link
Contributor

@tomjaguarpaw I'm working on updating the chapters related to setting up Haskell <=> DB bridge using DefaultFromFields and am unable to explain (even to myself) why Unpackspec in QueryRunnerColumn (Unpackspec (Column pgType) ()) (FieldParser haskellType) exists. What does it hold over and above FieldParser?

@tomjaguarpaw
Copy link
Owner Author

If you have

myTable :: Table (Field SqlInt4, Field SqlInt4) (Field SqlInt4, Field SqlInt4)

q1 = selectTable myTable
q2 = fmap fst (selectTable myTable)

Then q1 generates (roughly)

SELECT column1, column2 FROM mytable

and q2 generates (roughly)

SELECT column1 FROM mytable

It's the Unpackspec in the FromFields that is responsible for generating column1, column2 in the first case and column1 in the second case.

@tomjaguarpaw
Copy link
Owner Author

But the fact that a FromFields contains an Unpackspec is completely an implementation detail. Can you avoid mentioning it at all?

@saurabhnanda
Copy link
Contributor

It's the Unpackspec in the FromFields that is responsible for generating column1, column2 in the first case and column1 in the second case.

Based on this explanation, shouldn't Unpackspec be part of Query instead of FromField?

I was under the impression that via Unpackspec one has the ability to map multiple DB fields to a single Haskell value. Then that's not the case, right?

And would it be right to say, that via FromField one sets up the field/col level bridge and via Table (including profunctor magic) one sets up the row-level bridge?

@saurabhnanda
Copy link
Contributor

@tomjaguarpaw the machinery to use for Haskell => DB conversion would be https://hackage.haskell.org/package/opaleye-0.7.1.0/docs/Opaleye-ToFields.html#t:ToFields , right?

Why is it plural? Is there any use-case where one would write ToFields haskells fields for multiple fields in one-shot? Isn't calling haskell -> sql on different part of a Haskell data-structure the job of the profunctor?

@saurabhnanda
Copy link
Contributor

@tomjaguarpaw and as I'm looking at the user-facing API, I'm wondering whether they asymmetry is really warranted, i.e. DefaultFromField vs Default ToFields? Is it not possible to have one of the following?

  • ToFields and FromFields
  • ToField and FromField
  • Default ToField and Default FromField
  • Default ToFields and Default FromFields

@saurabhnanda
Copy link
Contributor

@tomjaguarpaw are you happy with the following as a final user-facing API?

instance DefaultFromField SqlText OrderStatus where
  defaultFromField = fromPGSFieldParser orderStatusFieldParser

instance Default ToFields OrderStatus (Column SqlText) where
  def = toToFields (toFields . orderStatusToText)

@tomjaguarpaw
Copy link
Owner Author

the machinery to use for Haskell => DB conversion would be https://hackage.haskell.org/package/opaleye-0.7.1.0/docs/Opaleye-ToFields.html#t:ToFields , right?

Right

Why is it plural? Is there any use-case where one would write ToFields haskells fields for multiple fields in one-shot? Isn't calling haskell -> sql on different part of a Haskell data-structure the job of the profunctor?

You might not write the instance for something containing multiple fields (although you could). But even so you often call it like toFields ("Hello", 1234 :: Int) :: (Field SqlText, Field SqlInt4) so indeed it creates multiple fields at once.

@tomjaguarpaw
Copy link
Owner Author

are you happy with the following as a final user-facing API?

instance DefaultFromField SqlText OrderStatus where
  defaultFromField = fromPGSFieldParser orderStatusFieldParser

That looks fine

instance Default ToFields OrderStatus (Column SqlText) where
  def = toToFields (toFields . orderStatusToText)

I think lmap orderStatusToText toFields is better because that pattern generalises to all other Profunctors.

On the other hand I added an experimental function to allow easy mapping between Haskell and DB types called fromFieldToFieldsEnum. Although it has "enum" in its name it works for all DB types, not just enums. It might be best to fully support this and make it the official way of mapping Haskell <-> DB types. What do you think?

I'm wondering whether they asymmetry is really warranted, i.e. DefaultFromField vs Default ToFields?

This is a bit of a wart that was needed to support nullable and non-nullable fields uniformly. I hope it won't be needed in the new Field world.

It's the Unpackspec in the FromFields that is responsible for generating column1, column2 in the first case and column1 in the second case.

Based on this explanation, shouldn't Unpackspec be part of Query instead of FromField?

This is getting into quite technical areas. I don't understand why this question needs answering. The fact that a FromFields contains an Unpackspec is an implementation detail. Isn't it sufficient to say that a FromFields deals with running a Select and turning its Fields into Haskell values?

I was under the impression that via Unpackspec one has the ability to map multiple DB fields to a single Haskell value. Then that's not the case, right?

The Unpackspec doesn't have anything to do with Haskell values. It's a way of observing what Fields are being selected in a query.

And would it be right to say, that via FromField one sets up the field/col level bridge and via Table (including profunctor magic) one sets up the row-level bridge?

FromField certainly describes how to turn Fields into Haskell values and Table describes the full row that a table contains.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants