honeysql

Discussion of https://github.com/seancorfield/honeysql :slightly_smiling_face:
orestis 2021-06-18T13:39:12.039Z

Should there be support for CROSS JOIN LATERAL? I tried the [:lateral ...] wrapper but didn't get what I expected...

orestis 2021-06-18T13:44:38.040300Z

{:select [:foo.id [ :x.id :x_id] :x.value ]
 :cross-join [[[:jsonb_to_recordset :foo.json_value]
                 [[:raw "x(id text, value jsonb)"]]]]
   :from [:foo]}
vs
{:select [:foo.id [ :x.id :x_id] :x.value ]
   :cross-join [:lateral
                [[[:jsonb_to_recordset :foo.json_value]
                  [[:raw "x(id text, value jsonb)"]]]]]
   :from [:foo]}
The latter returns:
SELECT foo.id, x.id AS x_id, x.value
FROM foo
CROSS JOIN lateral, (JSONB_TO_RECORDSET(foo.json_value), (x(id text, value jsonb)))
instead of
SELECT foo.id, x.id AS x_id, x.value
FROM foo
CROSS JOIN LATERAL JSONB_TO_RECORDSET(foo.json_value) x(id text, value jsonb)

orestis 2021-06-18T13:45:48.040800Z

Perhaps there should be a :cross-join-lateral keyword? Can I add this on my own?

seancorfield 2021-06-18T17:13:14.043Z

@orestis Is this what you’re after?

dev=> (sql/format {:select [:foo.id [ :x.id :x_id] :x.value ]
 #_=>    :cross-join [[[:lateral
 #_=>                 [[:jsonb_to_recordset :foo.json_value]
 #_=>                   [:raw "x(id text, value jsonb)"]]]]]
 #_=>    :from [:foo]})
["SELECT foo.id, x.id AS x_id, x.value FROM foo CROSS JOIN LATERAL (JSONB_TO_RECORDSET(foo.json_value), x(id text, value jsonb))"]
:cross-join takes a sequence of SQL entities; each can be a simple name or a pair of expression/alias — like :select — so when you want a “function call” there, you need :cross-join [ [ [:fn-call :arg1 :arg2] ] ] just like in a SELECT.

seancorfield 2021-06-18T17:15:12.044300Z

Looking at your desired result more carefully, I think you want this:

dev=> (sql/format {:select [:foo.id [ :x.id :x_id] :x.value ]
 #_=>    :cross-join [[[:lateral
 #_=>                   [:jsonb_to_recordset :foo.json_value]]]
 #_=>                 [[:raw "x(id text, value jsonb)"]]]
 #_=>    :from [:foo]})
["SELECT foo.id, x.id AS x_id, x.value FROM foo CROSS JOIN LATERAL JSONB_TO_RECORDSET(foo.json_value), x(id text, value jsonb)"]
Again, the :raw part needs to be treated as a function call in the sequence of CROSS JOIN expressions.

seancorfield 2021-06-18T17:17:09.045400Z

So we have [:lateral [:jsonb..]] as one function call, wrapped as an alias (with the actual alias omitted): [ [:lateral [:jsonb..]] ] and then the same for [ [:raw ..] ]

seancorfield 2021-06-18T17:19:51.046600Z

I notice you don’t have a , in your desired expression: CROSS JOIN LATERAL JSONB_TO_RECORDSET(foo.json_value) x(id text, value jsonb) — is that intentional? I’m not quite sure what to make of that syntax…

orestis 2021-06-18T17:25:04.047800Z

Thanks for answering @seancorfield - first of all, putting the lateral as in your example works. I was putting it surrounding the entire expression but I was confused 🙂

orestis 2021-06-18T17:26:41.050Z

Regarding the raw placement: my example is actually what I'm after. In this case, the x(id text, value jsonb) is a cast expression. jsonb_to_recordset takes a postgres jsonb array and creates a "set" (or table, or relation) out of it. But since SQL is statically typed you need to provide the columns and their types as a cast expression.

orestis 2021-06-18T17:27:18.050900Z

I discovered that technically for postgres the LATERAL is not needed in a table-returning-function (it can refer to other tables already in the FROM clause).

seancorfield 2021-06-18T17:28:39.052300Z

So you’re aiming for JSONB_TO_RECORDSET(foo.json_value) AS x(id text, value jsonb) essentially?

seancorfield 2021-06-18T17:29:04.053Z

(i.e., x is the name you are trying to give to the record set?)

orestis 2021-06-18T17:29:18.053400Z

If you're interested, given a column containing this JSONB data (using EDN for type laziness): [{:id "foo" :value "bar"}, [{:id "qoo" :value "cux"}]it would return 2 rows with the columns id, value.

orestis 2021-06-18T17:29:33.053800Z

Yes, x is the name of the recordset that you can use in the SELECT clause.

orestis 2021-06-18T17:30:12.054400Z

AS is optional as in many other places. SQL is soooo weird 🙂

orestis 2021-06-18T17:30:58.055Z

So, no worries from my end: honeysql provides what I want, and turns out I didn't need it anyway 🙂 Thanks for taking the time.

orestis 2021-06-18T17:31:36.055800Z

Between honeysql queries-as-data and PostgreSQL's power I can create pivot tables quite easily. Dynamically generate column definitions etc 🙂

seancorfield 2021-06-18T17:31:53.056Z

OK, here it is:

dev=> (sql/format {:select [:foo.id [ :x.id :x_id] :x.value ]
 #_=>    :cross-join [[[:lateral
 #_=>                   [:jsonb_to_recordset :foo.json_value]]
 #_=>                   [[:raw "x(id text, value jsonb)"]]]]
 #_=>    :from [:foo]})
["SELECT foo.id, x.id AS x_id, x.value FROM foo CROSS JOIN LATERAL JSONB_TO_RECORDSET(foo.json_value) x(id text, value jsonb)"]

seancorfield 2021-06-18T17:33:40.057300Z

:cross-join now has a single expression: an alias pair. The first part of that alias pair is [:lateral ..] and the second part is the nested “function call” of [[:raw ..]].

seancorfield 2021-06-18T17:34:24.057900Z

:cross-join [ [expr [AS] alias] ] where [AS] is just intended to show how the aliased expression gets created.

orestis 2021-06-18T17:35:02.058700Z

Gotcha, I ended up with the same result. I was looking for "lateral" in the docs and found a couple of hits but I I was tired 🙂

orestis 2021-06-18T17:35:34.059800Z

BTW, honeysql is CLJC, right? Would be a nice project to put together a webpage that takes a honeysql query on the left and gives you SQL on the right.

orestis 2021-06-18T17:36:14.061Z

I think malli has something similar with also some persistence that makes it for a nice playground and easier for creating issues and so on...

seancorfield 2021-06-18T17:36:26.061400Z

The “problem” is that pretty much any combination of expressions with [..`]` is “valid” as far as the DSL is concerned and most of them produce valid SQL with various meanings, but not all of the possible SQL outputs are going to be valid 🙂

orestis 2021-06-18T17:36:46.061900Z

(hoping someone likes the idea and takes it up, I don't have time myself 😄)

seancorfield 2021-06-18T17:37:06.062400Z

I don’t do cljs/frontend at all so it won’t be me either!

orestis 2021-06-18T17:37:46.063Z

I think that's a valid tradeoff. SQL is infinitely complicated and getting a DSL able to express all of it correctly while still being data will be impossible.

orestis 2021-06-18T17:38:42.063900Z

You do end up using the REPL quite a lot during query development, and for the more complex stuff you do have to run the query against a live DB anyway -- but that's what we do in Clojure anyway 🙂

seancorfield 2021-06-18T17:40:55.064800Z

Yeah, I tend to build my more complex queries up in RCFs with dummy data values, to ensure I’m getting what I want. But I also use MySQL so life is a lot simpler 🙂

orestis 2021-06-18T17:43:42.065Z

RCF?

seancorfield 2021-06-18T17:44:12.065800Z

Rich Comment Form.

orestis 2021-06-18T17:44:36.066600Z

Hehe most of our queries are pretty simple too. But we're migrating a MongoDB database with an actual valid use case of using schema-less in some areas. And these areas we have to dig into Postgres JSON support which is great but also weird 🙂

orestis 2021-06-18T17:44:45.067Z

Ah yes. Same here 🙂

seancorfield 2021-06-18T17:45:06.067500Z

A name coined by Stu Halloway for (comment ..) forms used while developing/exploring problem solutions — because Rich Hickey does it that way (you can see some such forms at the bottom of some namespaces in Clojure).

seancorfield 2021-06-18T17:45:33.067800Z

https://github.com/seancorfield/honeysql/issues/332

orestis 2021-06-18T17:52:30.068300Z

I knew the term but not the acronym 🙂