~subsetpark/doozer

doozer/doozer/to-sql.janet -rw-r--r-- 4.7 KiB
4a8e0e3e — Zach Smith update docs 1 year, 2 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
(def- backend-key :doozer/sql-backend)
(varfn to-sql [query] "")

(defn- render-expr-with-parameters
  [backend expr]
  (def parameters @{})

  (defn walker
    [node]
    (match node
      # Match on :as specially for JOINS; 
      # whereas `<expr> :as <alias> => <expr> AS alias`, joins stick their AS
      # in the middle, not the end.
      {:table tab :on join-on :type join-type :as as}
      (let [rendered-on (walker join-on)]
        (:render-join backend
                      join-type
                      (:quote-element backend tab)
                      (:quote-element backend as)
                      rendered-on))

      {:as alias :referent inner-exp}
      (:render-as backend
                  (walker inner-exp)
                  (:quote-element backend alias))

      {:op op :args args}
      (:render-operation backend op (map walker args))

      {:apply function :args args}
      (let [rendered-args (:render-function-arguments backend (map walker args))]
        (:render-apply backend function rendered-args))

      {:fragment fragment :args args}
      (let [rendered-args (map walker args)]
        (reduce
          (fn [acc param] (string/replace "?" param acc))
          fragment
          rendered-args))

      {:subquery subquery}
      (let [[rendered-subquery subquery-params] (to-sql subquery)]
        (merge-into parameters subquery-params)
        (:render-subquery backend rendered-subquery))

      {:atom true}
      (let [segment (:render-element backend node)]
        (put parameters (node :symbol) (node :value))
        segment)

      {:table tab} (:quote-element backend tab)

      (errorf "SQL rendering error: Couldn't render form: %q" node)))

  (let [rendered-expr (walker expr)]
    [rendered-expr parameters]))

(defn- render-clauses-with-parameters
  [backend clauses]
  (let [rendered-clauses @[]
        all-parameters @{}]
    (each clause clauses
      (let [[rendered-clause clause-parameters] (render-expr-with-parameters backend clause)]
        (array/push rendered-clauses rendered-clause)
        (merge-into all-parameters clause-parameters)))
    [rendered-clauses all-parameters]))

(defn- render-from
  [backend {:from from :as alias}]
  (render-expr-with-parameters backend {:as alias :referent from}))

(defn- render-limit
  [backend {:limit limit}]
  (:render-limit backend (string limit)))

(defn- params-keyword-to-positional
  `
  Given an uninterpolated string using keyword value markers and a params
  table, convert it to use positional markers and a params sequence.

  Note to my future self: while at first glance this seems quite brittle, as it
  simply replaces the first instance of the keyword marker that it finds, we
  are guaranteed not to have any instances of ` : ` (as long as that remains an
  invariant of our SQL generation code), as all user-supplied strings will have
  been similarly treated.

  The exception is raw SQL fragments, which could be constructed to collide
  with keyword strings.
  `
  [sql-string all-parameters]
  (var sql-string sql-string)
  (def params-sequence @[])
  (var i 1)
  (each [kw val] (pairs all-parameters)
    (set sql-string (string/replace (describe kw) (string "$" i) sql-string))
    (++ i)
    (array/push params-sequence val))

  (array/insert params-sequence 0 sql-string)
  params-sequence)

(varfn to-sql
  ```
  Render `query` into SQL. Produces a two-tuple of [sql, parameters], where
  `sql` is a SQL string, including parameter placeholders, and `parameters` is
  a dictionary mapping those placeholders to the values that should be
  interpolated into the query.
  ```
  [query]
  (if-let [backend (dyn backend-key)]
    (let [[rendered-from from-parameters] (render-from backend query)
          [rendered-joins join-parameters] (render-clauses-with-parameters backend (query :join))
          [rendered-wheres where-parameters] (render-clauses-with-parameters backend (query :where))
          [rendered-selects select-parameters] (render-clauses-with-parameters backend (query :select))
          rendered-limit (render-limit backend query)

          sql-string (:assemble-base
                       backend
                       rendered-from
                       (:assemble-join-clauses backend rendered-joins)
                       (:assemble-where-clauses backend rendered-wheres)
                       (:assemble-select-clauses backend rendered-selects)
                       rendered-limit)
          all-parameters (merge from-parameters join-parameters where-parameters select-parameters)]

      (case (get-in backend [:config :sql-param-type])
        :positional (params-keyword-to-positional sql-string all-parameters)
        [sql-string all-parameters]))

    (errorf "Can't generate SQL; `%q` must be set." backend-key)))