Fork me on GitHub
#sql
<
2021-07-18
>
seancorfield21:07:42

@deleted-user Can you share your code that reproduces this?

seancorfield21:07:53

You may just need to wrap the value with [:lift ..] to tell HoneySQL it is data, not part of the DSL...

seancorfield21:07:14

No, the elements of HoneySQL's DSL are not namespaced.

seancorfield21:07:55

If you are passing a structured value, you just wrap it in [:lift ..] -- vector or hash map

seancorfield21:07:12

[:lift ..] is HoneySQL syntax. It tells HoneySQL not to interpret the value, but treat it as opaque data, i.e., lift it out as a parameter.

seancorfield21:07:46

If you have any values that could be hash maps or vectors, you must wrap them in [:lift ..].

seancorfield21:07:14

That's the whole point of that syntax: otherwise HoneySQL will treat a vector as a function call and a hash map as a statement.

seancorfield21:07:36

I'm not sure what you're asking.

seancorfield21:07:16

Then wrap it in [:lift ..]

seancorfield21:07:26

Vectors are function calls in HoneySQL.

seancorfield21:07:39

That's why you must wrap them in [:lift ..]

seancorfield21:07:59

(let [v [:lift :some :data]]
  (sql/format {:select :foo :from :bar :where [:= :col [:lift v]]}))
☝️:skin-tone-2: JSON values must be wrapped in [:lift ..]

seancorfield21:07:50

If you're working with JSON values in SQL, you need to flag them as lifted values to prevent them being interpreted as function calls or DSL statements.

seancorfield21:07:57

Yes, if you don't wrap vectors and hash maps, HoneySQL will interpret them as function calls and DSL statements.

seancorfield21:07:21

If your vector is [:foo :bar] that is a function call: foo(bar).

seancorfield21:07:46

You have to wrap structured values always to avoid problems.

seancorfield21:07:28

You can make lots of mistakes with HoneySQL -- it can't detect most of them.

seancorfield21:07:46

HoneySQL does not (can not) guarantee valid SQL.

seancorfield21:07:27

I understand what you're saying. I'm just saying that if you work with structured values, you need to be extra careful.

seancorfield21:07:17

HoneySQL can't tell the difference between a function call you've built programmatically and any other vector of data.

seancorfield21:07:45

Same goes for hash maps (DSL statements).

seancorfield21:07:48

That wouldn't solve the problem.

seancorfield21:07:24

Post an issue on HoneySQL with examples you think could be detected / dealt with and I'll be happy to post an explanation of why namespacing things won't solve it 🙂

seancorfield21:07:01

In an arbitrary SQL dialect [:foo 42] could be a valid SQL function call.

seancorfield21:07:19

The absence of :lift can't be detected (because a function call could be valid in that position).

seancorfield21:07:46

You're concerned about the broader case of any of HoneySQL's special syntax from masking an actual function call in the SQL code, right?

seancorfield21:07:02

Unfortunately, with SQL being such a broad and diverse language and so many local variants (in terms of what users have defined), it's almost impossible for HoneySQL to prevent footguns.

seancorfield21:07:29

If you follow along in the #honeysql channel, you'll see all sorts of stuff that people have shot themselves with.

seancorfield21:07:38

If you don't use JSON with SQL, you're much less likely to shoot yourself in the foot with HoneySQL.

seancorfield21:07:33

But mixing JSON and SQL leads to some pretty gnarly expressions that even in plain SQL can be hard to detangle. Add in a DSL based on JSON-like data structures (HoneySQL) and you really are walking a tightrope.

seancorfield21:07:08

That's an inherent conflict between Clojure data structures being used for two different meanings in the same expression.

seancorfield21:07:46

If you use JSON with string keys, you're safe:

dev=> (sql/format {:select [[["lift" 42]]]})
["SELECT (?, ?)" "lift" 42]

seancorfield21:07:24

But if you use keywords or symbols, you're in HoneySQL's DSL territory:

dev=> (sql/format {:select [[[:lift 42]]]})
["SELECT ?" 42]

seancorfield21:07:58

JSON should be strings though -- keywords and symbols do not exist in JSON, only in Clojure.

seancorfield21:07:28

(and HoneySQL's DSL has been this way for many, many years at this point)

seancorfield21:07:46

What machinery is in place to actually turn {:foo 42} into JSON? The database driver isn't doing that.

seancorfield21:07:31

I'm happy to add more caveats/warnings to the HoneySQL docs about working with JSON requiring [:lift ..] if your JSON actually has keywords in it, rather than string keys.

seancorfield21:07:51

We're talking about HoneySQL-using code that needs [:lift v] instead of plain v in it.

seancorfield21:07:37

If you didn't have the "magic" to turn Clojure data structures into JSON, then those (JSON) data structures wouldn't get confused with Clojure data structures that HoneySQL uses/expects 🙂 It's an inherent problem with mixing the two -- you can't avoid conflicts completely.

seancorfield21:07:06

And HoneySQL 2.x's DSL is almost identical to HoneySQL 1.x's DSL.

seancorfield22:07:16

That's why HoneySQL 2.x provides an "escape hatch" (`:lift`) so that users can consistently indicate to HoneySQL where the boundaries are between the Clojure data it should process and the Clojure data it shouldn't.

seancorfield22:07:48

You could also use named parameters and avoid this problem.

seancorfield22:07:27

I can't change it. There's a lot of HoneySQL code out there.

seancorfield22:07:05

A key goal of v2, yes.

seancorfield22:07:25

(I am fairly sure you'd run into similar problems trying to mix JSON-as-Clojure-data with the 1.x HoneySQL DSL but there were places where 1.x wasn't very consistent)

seancorfield22:07:21

My point is that this isn't a new problem with v2: if you have a big blog of Clojure data and you expect some parts of it to be handled with different semantics to other parts of it, you-the-user need to add some sort of annotations into that data to delineate those semantics.

seancorfield22:07:04

If you use named parameters, the problem goes away completely I believe.

seancorfield22:07:01

[:param :foo] in the DSL and pass {:params {:foo your-json-value}} in the sql/format call.

seancorfield22:07:42

dev=> (sql/format {:select [[[:param :foo]]]} {:params {:foo [:lift 42]}})
["SELECT ?" [:lift 42]]

seancorfield22:07:10

That completely separates the two "types" of Clojure data (DSL and JSON) so there can be no confusion.

seancorfield22:07:42

(anyways, must dash -- my partner and I are off to the garden center)

🌻 3
seancorfield23:07:34

I'd probably write that upsert-multi! with:

(-> (sql/insert-into table)
    (assoc :on-conflict on-conflict)
    (assoc :do-update-set do-update-set)
    (sqlh/returning :*))
(I don't like as-> except inside a -> pipeline -- and this also avoids the two apply calls)

seancorfield23:07:35

Then you have a nice place to hook in wrapping values in [:lift ..] to ensure all your values -- including your JSON -- gets lifted out as parameters because you can map over rows and replace every value v with [:lift v]. Nice to have it all in one place so no one can misuse it.

seancorfield23:07:40

Given that you have ->snake_case on table/column, I'm a bit surprised you're not converting ->kebab-case in the builder? (which would be next.jdbc/unqualified-snake-kebab-opts out of the box)

seancorfield23:07:10

Yes, for safety.

seancorfield23:07:14

[:lift 42] and [:lift {:foo "bar"}] are "the same" to HoneySQL: they are both values to be lifted out as parameters, so both produce ? and put the value into the parameter list.