<commit_before>(ns asphalt.core
  (:refer-clojure :exclude [update])
  (:require
    [clojure.string   :as str]
    [asphalt.internal :as i])
  (:import
    [java.util.regex Pattern]
    [java.sql  Connection PreparedStatement
               ResultSet ResultSetMetaData]
    [javax.sql DataSource]))


;; ----- parse SQL for named parameters and types -----


(defn parse-sql
  "Given a SQL statement with embedded parameter names return a three-element vector:
  [SQL-template-string
   param-pairs           (each pair is a two-element vector of param key and type)
   result-column-types]
  that can be used later to extract param values from maps."
  ([^String sql]
    (parse-sql sql {}))
  ([^String sql options]
    (let [{:keys [escape-char param-start-char type-start-char name-encoder]
           :or {escape-char \\ param-start-char \$ type-start-char \^}} options
        ec escape-char       ; escape char
        mc param-start-char  ; marker char
        tc type-start-char   ; type char
        nn (count sql)
        ^StringBuilder sb (StringBuilder. nn)
        ks (transient [])  ; param keys
        ts (transient [])  ; result types
        tlast (fn [t] (get t (dec (count t))))  ; `last` on a transient
        conj-new! (fn [t name] (conj! t name))
        conj-old! (fn [t name] (let [old (tlast t)] (conj! (pop! t) (str old name))))
        valid-nchar? (fn [ch] (let [partial-name (tlast ks)] (i/valid-name-char? type-start-char partial-name ch)))
        valid-tchar? (fn [ch] (let [partial-name (tlast ts)] (i/valid-type-char? partial-name ch)))
        handle-token (fn [token-name i valid-char? t retval]
                       (let [j (int (inc ^int i))]
                         (cond
                          (>= ^int i (dec nn)) (i/illegal-arg "SQL found ending with a dangling" token-name
                                                 "marker char:" sql)
                          (valid-char?
                            (.charAt sql j))   (do (conj-new! t "") retval)
                          :otherwise           (i/illegal-arg "Invalid first character in" token-name ":"
                                                 (str \' (.charAt sql j) \') "at position" j))))]
    (loop [i (int 0) ; current index
           e? false  ; current escape state
           n? false  ; parameter name in progress
           t? false  ; result type in progress
           ]
      (if (>= i nn)
        (cond
          e? (i/illegal-arg "SQL found ending with dangling escape character:" sql)
          n? (.append sb \?))
        (let [ch (.charAt sql i)
              [e? n? t?] (cond
                           ;; escape mode
                           e?              (do (.append sb ch) [false false false])
                           ;; param name in progress?
                           n?              (if (valid-nchar? ch)
                                             (do (conj-old! ks ch) [false true  false])
                                             (do (.append sb \?)
                                               (.append sb ch)     [false false false]))
                           ;; result type in progress?
                           t?              (if (valid-tchar? ch)
                                             (do (conj-old! ts ch) [false false true])
                                             (do (.append sb ch)   [false false false]))
                           ;; escape character encountered
                           (= ch ^char ec) (if (>= i (dec nn))
                                             (i/illegal-arg "SQL found ending with dangling escape character:" sql)
                                             [true false false])
                           ;; start of a param name?
                           (= ch ^char mc) (handle-token "param-name" i valid-nchar? ks [false true false])
                           ;; start of a result type?
                           (= ch ^char tc) (handle-token "result-column-type" i valid-tchar? ts [false false true])
                           ;; catch-all case
                           :else       (do (.append sb ch) [false false false]))]
          (recur (unchecked-inc i) e? n? t?))))
    (i/make-sql-template
      (.toString sb)
      (mapv #(let [[p-name p-type] (i/split-param-name-and-type type-start-char %)]
               [(i/encode-name p-name) (i/encode-type p-type sql)])
        (persistent! ks))
      (mapv #(i/encode-type % sql) (persistent! ts))))))


(defmacro defsql<commit_msg>add docstring to defsql<commit_after>(ns asphalt.core
  (:refer-clojure :exclude [update])
  (:require
    [clojure.string   :as str]
    [asphalt.internal :as i])
  (:import
    [java.util.regex Pattern]
    [java.sql  Connection PreparedStatement
               ResultSet ResultSetMetaData]
    [javax.sql DataSource]))


;; ----- parse SQL for named parameters and types -----


(defn parse-sql
  "Given a SQL statement with embedded parameter names return a three-element vector:
  [SQL-template-string
   param-pairs           (each pair is a two-element vector of param key and type)
   result-column-types]
  that can be used later to extract param values from maps."
  ([^String sql]
    (parse-sql sql {}))
  ([^String sql options]
    (let [{:keys [escape-char param-start-char type-start-char name-encoder]
           :or {escape-char \\ param-start-char \$ type-start-char \^}} options
        ec escape-char       ; escape char
        mc param-start-char  ; marker char
        tc type-start-char   ; type char
        nn (count sql)
        ^StringBuilder sb (StringBuilder. nn)
        ks (transient [])  ; param keys
        ts (transient [])  ; result types
        tlast (fn [t] (get t (dec (count t))))  ; `last` on a transient
        conj-new! (fn [t name] (conj! t name))
        conj-old! (fn [t name] (let [old (tlast t)] (conj! (pop! t) (str old name))))
        valid-nchar? (fn [ch] (let [partial-name (tlast ks)] (i/valid-name-char? type-start-char partial-name ch)))
        valid-tchar? (fn [ch] (let [partial-name (tlast ts)] (i/valid-type-char? partial-name ch)))
        handle-token (fn [token-name i valid-char? t retval]
                       (let [j (int (inc ^int i))]
                         (cond
                          (>= ^int i (dec nn)) (i/illegal-arg "SQL found ending with a dangling" token-name
                                                 "marker char:" sql)
                          (valid-char?
                            (.charAt sql j))   (do (conj-new! t "") retval)
                          :otherwise           (i/illegal-arg "Invalid first character in" token-name ":"
                                                 (str \' (.charAt sql j) \') "at position" j))))]
    (loop [i (int 0) ; current index
           e? false  ; current escape state
           n? false  ; parameter name in progress
           t? false  ; result type in progress
           ]
      (if (>= i nn)
        (cond
          e? (i/illegal-arg "SQL found ending with dangling escape character:" sql)
          n? (.append sb \?))
        (let [ch (.charAt sql i)
              [e? n? t?] (cond
                           ;; escape mode
                           e?              (do (.append sb ch) [false false false])
                           ;; param name in progress?
                           n?              (if (valid-nchar? ch)
                                             (do (conj-old! ks ch) [false true  false])
                                             (do (.append sb \?)
                                               (.append sb ch)     [false false false]))
                           ;; result type in progress?
                           t?              (if (valid-tchar? ch)
                                             (do (conj-old! ts ch) [false false true])
                                             (do (.append sb ch)   [false false false]))
                           ;; escape character encountered
                           (= ch ^char ec) (if (>= i (dec nn))
                                             (i/illegal-arg "SQL found ending with dangling escape character:" sql)
                                             [true false false])
                           ;; start of a param name?
                           (= ch ^char mc) (handle-token "param-name" i valid-nchar? ks [false true false])
                           ;; start of a result type?
                           (= ch ^char tc) (handle-token "result-column-type" i valid-tchar? ts [false false true])
                           ;; catch-all case
                           :else       (do (.append sb ch) [false false false]))]
          (recur (unchecked-inc i) e? n? t?))))
    (i/make-sql-template
      (.toString sb)
      (mapv #(let [[p-name p-type] (i/split-param-name-and-type type-start-char %)]
               [(i/encode-name p-name) (i/encode-type p-type sql)])
        (persistent! ks))
      (mapv #(i/encode-type % sql) (persistent! ts))))))


(defmacro defsql
  "Define a parsed SQL template that can be used to execute it later."
{'fraction_non_alphanumeric': 0.118517706391843, 'fraction_numerical': 0.0002192742023900888, 'mean_word_length': 1.3070308548305514, 'pattern_counts': {'":': 2, '<': 3, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 48, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1508679', 'n_tokens_mistral': 2518, 'n_tokens_neox': 2444, 'n_words': 847}