Fork me on GitHub
#meander
<
2021-02-11
>
markaddleman16:02:39

Picking up a dropped thread: I'm using meander to parse and rewrite HoneySQL (well, something close to HoneySQL). One of the things I need to do is "fully qualify" column names by finding the table name from the from clause and threading it through column references in the rest of the query. For example: {:select [{:type :col :col :a}] :from [{:type :table :table :t}] should be rewritten as {:select [{:type :col :col :a :table-ref :t}] :from [{:type :table :table :t}] Things get complicated when I have to deal with subqueries. For example {:select [{:type :col :col :a} {:select [{:type :col :col :b}] :from [:sub-t]} :from [:t]} should be rewritten as {:select [{:type :col :col :a :table-ref :t} {:select [{:type :col :col :b :table-ref :sub-t}] :from [{:type :table :table :sub-t}]} :from [{:type :table :table :t}]} In this example, you can see that I want to match the :from clause that is nearest in the tree to the column. Because columns can exist in several places within a query, I'd like to use the $ operator to find the column references. The problem is that I need to attach some boundary conditions to $ so it find a :from clause from an sub-query. Any ideas?

noprompt16:02:42

What should this do when there is more in :from?

noprompt16:02:35

(m/let [table-ref (keyword (gensym "T__"))]
  {:from <what-goes-here>
   :select [(m/or {:type :col :as !column}
                  !not-column)
            ...]})
{:from [{:type :table :table ?table-ref} ...]
 :select [{:table-ref ?table-ref & !column} ...
          !not-column ...]}

markaddleman16:02:31

I knew I should have discussed that 🙂 In my case, if there is more than one from, the columns are guaranteed to be fully qualified from the start so this qualification rewrite is, essentially, a nop

noprompt17:02:29

You may want to consider using m/cata for on the right side like so

{:select [!selection ...]
 :from (m/and [{:type :table :table ?table}]
              ?from)}
{:select [(m/cata [::qualify-selection !selection ?table])]
 :from ?from}

;; Rewrite [::qualify-selection ,,,]
[::qualify-selection {:type :col :table-ref nil :as ?col} ?table]
{:table-ref ?table & ?col}

noprompt17:02:11

Then just make a rule for [::qualify-selection ,,,] that stops or rewrites as needed.

markaddleman17:02:44

Yes, that's my current approach. Unfortunately, it gets to be verbose because columns can exist in the select clause, where clause, group-by, function calls.... I am looking for a more terse solution but it may not exist

noprompt17:02:06

Normally qualification tends to a top down approach and for this sort of qualification you will necessarily have a few cases.

noprompt17:02:49

e.g. you’ll be calling (qualify-selection x table) or something like this.

noprompt17:02:07

Dealing with SQL, and especially HoneySQL, is going to require some code.

markaddleman18:02:27

Yeah. I think I found a solution that uses a meander pattern to locate and rewrite the deepest subquery. Outside of meander, I can loop over rewrites until I find a stable result

noprompt18:02:11

Ye olde fix ?

noprompt18:02:03

(defn fix [f]
  (fn [x]
    (let [x* (f x)]
      (if (= x x*)
        x
        (recur x*))))

markaddleman18:02:15

Yup 🙂 I'm not using a meander strategy for this but I probably should

noprompt18:02:41

I’ve been debating whether or not to keep those around going forward.

markaddleman19:02:53

I haven't used them so it's hard for me to say but I remember thinking that the most recent epsilon stuff around functions seemed very powerful. I don't recall the specifics right now