honeysql

Discussion of https://github.com/seancorfield/honeysql :slightly_smiling_face:
seancorfield 2021-02-14T06:18:58.135400Z

OK, seancorfield/honeysql {:mvn/version "2.0.0-alpha1"} is available for early testing... or [seancorfield/honeysql "2.0.0-alpha1"]... unfortunately http://cljdoc.org doesn't like this release so you can find the docs at https://github.com/seancorfield/honeysql/blob/v2/doc/getting-started.md (I'm working on fixing broken links etc so please let me know if you hit any problems).

1
kwrooijen 2021-02-14T09:17:02.136400Z

Reading the docs now. I'm really happy with some of these changes (written in differences-fromt-1-x.md ) 😍

kwrooijen 2021-02-14T09:18:10.136900Z

The removal of the reader literals is especially great

kwrooijen 2021-02-14T09:19:26.137400Z

The new :cast keyword is actually going to fix a nasty bug for me

kwrooijen 2021-02-14T09:21:44.138600Z

If you try to insert an empty array in Postgres without casting the type it'll actually fail. (Not sure why Postgres needs to know the type of the data if it's empty but ok). Now that I don't need to use sql/call this might actually be easier for me

kwrooijen 2021-02-14T09:32:57.139700Z

It's going to be some work to upgrade to 2.0. Mainly because of this https://github.com/seancorfield/honeysql/issues/276 @seancorfield any chance you thought about this? Of if you'd want to implement something like this at all?

seancorfield 2021-02-14T18:06:42.140800Z

@kevin.van.rooijen I'm open to suggestions while it's still in alpha.

seancorfield 2021-02-14T18:08:56.142Z

My initial reading of Gungnir's purpose is that it feels like something that should applied to SQL execution rather than SQL generation.

kwrooijen 2021-02-15T12:10:16.155400Z

What do you mean by execution? Simple Gungnir usecase: Query:

{:select :* :from :user :where [:= :user/email "KEVIN@MAIL.COM"]}
Transformation:
string/lower-case :user/email
Result:
{:select :* :from :user :where [:= :user/email "<mailto:kevin@mail.com|kevin@mail.com>"]}

seancorfield 2021-02-15T17:42:57.155900Z

@kevin.van.rooijen I mean that the transformation "doesn't matter" until the value is about to be stored into the DB or has just been retrieved from the DB. You've attached Gungnir to SQL generation but a similar transformation could be done against next.jdbc around the setting of parameters and the reading of SQL values back from the DB.

seancorfield 2021-02-15T17:57:26.156100Z

I'll take a deeper look at Gungnir over the next week or two and discuss my thoughts in more detail after that -- but my gut reaction is that attaching this to the set parameter / read result set functionality in next.jdbc would be "better" because then it would operate on things like update! and insert! from next.jdbc.sql.

kwrooijen 2021-02-15T19:14:35.156300Z

I mean that the transformation "doesn't matter" until the value is about to be stored into the DB or has just been retrieved from the DB. I don't think this is correct. Because I'm lower-casing the email in my query, not the resulting values. This is so that I don't have any case matching issues

kwrooijen 2021-02-15T19:17:23.156500Z

But maybe this can be solved in next.jdbc as you said

seancorfield 2021-02-15T19:34:07.156700Z

You misunderstand: You need the parameter lowercased before you store it into the parameter, as part of the prepared statement setup, for your query. Which could happen in next.jdbc's set-parameter -- and which would then apply to all of the ways you could run queries, not just those that go through HoneySQL.

seancorfield 2021-02-15T19:35:47.156900Z

And if this was wrapped around next.jdbc, Gungnir could also transform values coming back from the DB if needed -- although that is orthogonal.

seancorfield 2021-02-15T19:39:45.157100Z

To illustrate my point: if you do (jdbc/execute! ds (-&gt; (select :*) (from :user) (where [:= :user/email "<mailto:KEVIN@MAIL.COM|KEVIN@MAIL.COM>"]))) Gungnir would intercept that but if you did (sql/find-by-keys ds :user {:user/email "<mailto:KEVIN@MAIL.COM|KEVIN@MAIL.COM>"}) it wouldn't -- with Gungnir based on HoneySQL; but Gungnir could intercept the second version as well if it was based on next.jdbc. Does that make more sense?

kwrooijen 2021-02-15T20:37:22.157300Z

Right, so basically what I'm doing now could be handled with set-params. I'll take a look at that this week then, sounds like a much better solution

seancorfield 2021-02-15T20:44:45.157500Z

Feel free to ping me if you have Qs -- the qualified name is not present by the time set-parameter is called but the metadata, available through the PreparedStatement, may provide enough information.

seancorfield 2021-02-15T20:45:52.157700Z

And although clojure.java.jdbc has a different protocol, the machinery should be the same, so you could have one ns for next.jdbc and a separate ns for c.j.j if you wanted to support both libraries.

kwrooijen 2021-02-15T20:49:25.157900Z

I'm already using some features from next.jdbc so it's already required πŸ™‚

kwrooijen 2021-02-15T20:49:29.158100Z

Thanks for the tip

seancorfield 2021-02-15T20:54:59.158300Z

I just took another look at it and I don't think there's enough metadata to figure it out: the parameter metadata only has type information and no concept of naming; and the result set metadata is about what comes back not what goes in.

seancorfield 2021-02-15T20:56:03.158500Z

Which means the level you need access to is above the SQL statement / parameters, unfortunately. 😞

kwrooijen 2021-02-15T21:05:45.158700Z

Hmm that's unfortunate

kwrooijen 2021-02-15T21:07:24.158900Z

Is it possible to patch this to add the extra metadata?

kwrooijen 2021-02-15T21:07:37.159100Z

Would be a good excuse for me to dive into next.jdbc

seancorfield 2021-02-15T21:24:53.159300Z

Yes, you could extend the protocol via metadata on a value-by-value basis -- the trick I've been using is to wrap the value in a function (`constantly`) so the protocol implementation can easily retrieve the original value.

seancorfield 2021-02-15T21:25:10.159500Z

That's how the next.jdbc.types type adapters work.

seancorfield 2021-02-15T21:26:21.159700Z

I've just been looking at the Gungnir source code: it looks like it overrides a specific set of operators within HoneySQL to provide the before-read transform of the values associated with certain (qualified) keywords -- is that accurate?

seancorfield 2021-02-15T21:28:21.159900Z

(and it also wraps some of next.jdbc to achieve the same thing directly with insert! and update!?)

kwrooijen 2021-02-15T22:03:15.160100Z

Right, with Gungnir you can transform specific qualified-keywords defined in the model https://kwrooijen.github.io/gungnir/model.html It supports three things: :before-save - Done within gungnir, separate from HSQL / NJ :before-read - Patched version of HSQL (linked in the issue of HSQL) :after-read - custom result-set builder with NJ https://github.com/kwrooijen/gungnir/blob/master/src/clj/gungnir/database/builder.clj

kwrooijen 2021-02-15T22:04:08.160500Z

You can read me model document for the use cases / how they are implemented by the end-user. It's all based on qualified keywords

seancorfield 2021-02-15T22:06:22.160800Z

Given HoneySQL v2's recursive descent formatting -- which is independent of the actual operators/function calls being used -- it's certainly a lot harder to implement a hook like that. I'll continue to give it some thought.

kwrooijen 2021-02-15T22:10:14.161Z

All right. I'm a bit in between projects right now so Gungnir doesn't have my highest priority at the moment. Worst case I'd have to walk the HSQL datastructure but hopefully we can prevent that

seancorfield 2021-02-15T22:34:05.161200Z

What would you do with an expression like this in Gungnir? (where [:= :user/email [:|| "KEVIN" "@" "<http://MAIL.COM|MAIL.COM>"]])

seancorfield 2021-02-15T22:35:32.161400Z

(it would produce ["... WHERE email = CONCAT(?, ?, ?)" "KEVIN" "@" "<http://MAIL.COM|MAIL.COM>"])

kwrooijen 2021-02-16T07:11:40.161600Z

the [;|| "KEVIN" "@" "<http://MAIL.COM|MAIL.COM>"] part would be in the user defined part. So you'd for example have this (end-user defined)

(defmethod gungnir.model/before-read :string/lower-case [_k v]
  (clojure.string/lower-case v))
And v woul be [;|| "KEVIN" "@" "<http://MAIL.COM|MAIL.COM>"] . So the end-user can decide what to do with it

kwrooijen 2021-02-16T07:12:20.161800Z

(In this example it would fail because we're using string/lower-case on a vector, but the user could change that)

seancorfield 2021-02-16T16:58:16.171600Z

@kevin.van.rooijen Ah, good to know. OK, that gives me some ideas then...

kwrooijen 2021-02-16T16:58:53.171800Z

Let me know if you need any more info (sorry for the occasional late replies, timezones πŸ˜„ )

seancorfield 2021-02-16T17:00:24.173Z

To be expected when I'm on Pacific time and you're on Dutch(?) time πŸ™‚

kwrooijen 2021-02-16T17:04:59.173200Z

Yep

seancorfield 2021-02-14T18:42:41.142300Z

I have fixed the docs on http://cljdoc.org now: https://cljdoc.org/d/seancorfield/honeysql/2.0.0-alpha1/doc/readme

athomasoriginal 2021-02-14T21:47:54.143700Z

Just upgraded my side project to V2 (10k loc all clojure)! Everything is working well and the upgrade went smoothly. Thanks for everything Sean and let me know if there is anything I can test!

seancorfield 2021-02-14T21:49:05.144700Z

Oh, nice! Thank you!

seancorfield 2021-02-14T21:49:31.145200Z

What database are you using @tkjone?

athomasoriginal 2021-02-14T21:49:42.145500Z

Postgres πŸ˜‰

athomasoriginal 2021-02-14T21:49:51.145800Z

Because of the upgrade I was able to β€’ simplify helper functions built around sql/call, β€’ remove nilenso β€’ gain clarity on my queries using postgres specific functions

seancorfield 2021-02-14T21:49:57.146Z

Do you use the nilenso lib?

πŸ‘ 1
athomasoriginal 2021-02-14T21:51:14.146400Z

haha well, not anymore πŸ™‚

1
athomasoriginal 2021-02-14T21:53:47.148100Z

The areas I can see future upgraders maybe having challenges is if, as you mentioned, there is use of nilenso but one forgot which keywords were nilenso specific. Not a big deal, I just totally forgot I was using exists [UPDATE]: The above issue is unrelated to the nilenso library. It was undocumented behaviour from HoneySQL V1. See the convo in the thread below

athomasoriginal 2021-02-14T21:56:08.150300Z

Only other points of momentary confusion: β€’ The switch from hsql/call to :function-name - but you have that well documented, I for some reason spaced when reading it β€’ Switch from format/value to :lift - again, well documented, but I was looking for it in the V1 to V2 transition guide. Again, my assumption getting the best of me haha

seancorfield 2021-02-14T21:56:20.150700Z

The only references to exists I see in the nilenso lib are as part of the if-exists / if-not-exists around create/drop table/extension?

athomasoriginal 2021-02-14T21:57:28.151200Z

hmm. One sec. Let me look at the sql that was failing one more time.

seancorfield 2021-02-14T21:58:43.151900Z

Ah, I totally missed format/value -- I'll add that to the migration doc and the :lift special syntax. Thanks.

seancorfield 2021-02-14T22:00:38.152100Z

(docs updated)

πŸ‘ 1
athomasoriginal 2021-02-14T22:07:32.152300Z

This is the line I was successfully using with nilenso

(-&gt;&gt; {:exists
        {:select [:datname]
         :from   [:pg_catalog.pg_database]
         :where  [:= :datname dbname]}}
       (hsql/format)))
and when I run the above with hsql 2 I get this exception:
"Unknown SQL clauses: :exists"

athomasoriginal 2021-02-14T22:08:35.152500Z

To get it to work I updated it to

{:select [[[:exists {:select ...}]]]}

seancorfield 2021-02-14T22:10:58.152800Z

Hmm, interesting. I can't find any mention of that in nilenso's code.

seancorfield 2021-02-14T22:11:32.153Z

Ah, it's an undocumented part of HoneySQL 1.x!

athomasoriginal 2021-02-14T22:12:31.153200Z

Shoot! My bad!! but that makes sense! After a while, I would have to read H1 source more and more to figure out how to do something.

seancorfield 2021-02-14T22:12:40.153400Z

But I did migrate it -- it just needs to be added to the migration doc and probably elsewhere:

;; EXISTS should never have been implemented as SQL syntax: it's an operator!
  #_(is (= (format {:exists {:select [:a] :from [:foo]}})
           ["EXISTS (SELECT a FROM foo)"]))
  ;; select function call with an alias:
  (is (= (format {:select [[[:exists {:select [:a] :from [:foo]}] :x]]})
         ["SELECT EXISTS (SELECT a FROM foo) AS x"]))

seancorfield 2021-02-14T22:13:02.153600Z

(that's from the v2 tests)

athomasoriginal 2021-02-14T22:14:40.153800Z

Nice, so it won’t be allowed to be used as SQL syntax, correct?

seancorfield 2021-02-14T22:17:58.154Z

It never should have been allowed -- that was a bug in how it was added 😞

seancorfield 2021-02-14T22:18:32.154200Z

It was added way back in 0.5.3

athomasoriginal 2021-02-14T22:19:07.154400Z

πŸ™ˆ

seancorfield 2021-02-14T22:19:09.154600Z

I added it to the differences doc: https://github.com/seancorfield/honeysql/commit/2af7d0b6902b149c442f43d1ecf23473108fb585

athomasoriginal 2021-02-14T22:20:03.154800Z

Very nice!!!