-
Notifications
You must be signed in to change notification settings - Fork 4
/
sql_reader.clj
162 lines (144 loc) · 6.54 KB
/
sql_reader.clj
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
(ns kameleon.sql-reader
(:use [clojure.java.io :only [file reader]]
[korma.core :exclude [update]]
[slingshot.slingshot :only [throw+]])
(:require [clojure.string :as string]
[clojure.tools.logging :as log]
[me.raynes.fs :as fs]))
(def test-file
"/Users/dennis/src/iplant/ua/de-database-schema/src/main/data/01_data_formats.sql")
(defn char-seq
"Returns a lazy sequence of characters obtained from a reader."
[rdr]
(let [c (.read rdr)]
(if-not (< c 0)
(lazy-seq (cons (char c) (char-seq rdr)))
'())))
(defn is-space
"Determines whether or not a character is whitespace."
[c]
(Character/isWhitespace (char c)))
(declare c-comment-end-candidate c-comment c-comment-candidate line-comment
line-comment-candidate single-quoted-string double-quoted-string
statement-beginning statement-base statement)
(defn c-comment-end-candidate
"Handles what may or may not be the end of a C-style comment. If the comment
is really ending at this point then the state switches back to whatever it
was before the comment started. Otherwise, the state switches back to
c-comment."
[ps res [c & cs]]
(cond (nil? c) (throw+ {:type ::unterminated-c-comment})
(= c \/) #(ps res cs)
:else #(c-comment ps res cs)))
(defn c-comment
"Handles characters in a C-style comment. If the character is a slash then
there may be a nested comment. In that case, we switch to the
c-comment-candidate state. If the character is an asterisk then we might
have encountered a comment terminator. In that case, we siwtch to the
c-comment-end-candidate state. Otherwise, we continue discarding
characters."
[ps res [c & cs]]
(cond (nil? c) (throw+ {:type ::unterminated-c-comment})
(= c \/) #(c-comment-candidate (partial c-comment ps) res cs)
(= c \*) #(c-comment-end-candidate ps res cs)
:else #(c-comment ps res cs)))
(defn c-comment-candidate
"Handles what may be the start of a C-style comment. If a comment is really
starting then we switch to the c-comment state. Otherwise, we go back to
the previous state."
[ps res [c & cs]]
(cond (nil? c) [(conj res \/) cs]
(= c \*) #(c-comment ps res cs)
:else #(ps (conj res \/ c) cs)))
(defn line-comment
"Handles a line comment, which continues until the end of the line is
reached. If the current line is a newline then the comment terminates.
Otherwise we continue discarding characters."
[ps res [c & cs]]
(cond (nil? c) [res cs]
(= c \newline) #(ps res cs)
:else #(line-comment ps res cs)))
(defn line-comment-candidate
"Handles what may be the beginning of a line comment. If the next character
is a hyphen then we switch to the line-comment state. Otherwise, we switch
back to the previous state."
[ps res [c & cs]]
(cond (nil? c) [(conj res \-) cs]
(= c \-) #(line-comment ps res cs)
:else #(ps (conj res \- c) cs)))
(defn escaped-char
"Handles an escaped character in a single-quoted string."
[res [c & cs]]
(cond (nil? c) (throw+ {:type ::unterminated-string})
:else #(single-quoted-string (conj res c) cs)))
(defn single-quoted-string
"Handles a single-quoted string. If the next character is a single quote
then the string is being terminated and we switch back to the statement-base
state. Otherwise, we continue accumulating characters in the string."
[res [c & cs]]
(cond (nil? c) (throw+ {:type ::unterminated-string})
(= c \\) #(escaped-char (conj res c) cs)
(= c \') #(statement-base (conj res c) cs)
:else #(single-quoted-string (conj res c) cs)))
(defn double-quoted-string
"Handles a double-quoted string. If the next character is a double quote
then the string is being terminated and we switch back to the statement-base
state. Otherwise, we continue accumulating characters in the string."
[res [c & cs]]
(cond (nil? c) (throw+ {:type ::unterminated-quoted-name})
(= c \") #(statement-base (conj res c) cs)
:else #(double-quoted-string (conj res c) cs)))
(defn statement-beginning
"Skips to the beginning of the next SQL statement."
[res [c & cs :as all]]
(cond (nil? c) [res cs]
(is-space c) #(statement-beginning res cs)
(= c \-) #(line-comment-candidate statement-beginning res cs)
(= c \/) #(c-comment-candidate statement-beginning res cs)
:else #(statement-base res all)))
(defn statement-base
"Handles the base state for identifying SQL statements. If there are no more
characters then we return whatever we've accumulated so far. If the next
character is a semicolon then the statement is complete, so we return it.
If the next candidate is a slash then we may be at the start of a C-style
comment, so we switch to the c-comment-candidate state. If the next
character is a hyphen then we might be at the beginning of a line comment,
so we switch to the line-comment-candidate state. If the next character is
a single or double quote then we switch to the single-quoted-string or
double-quoted-string state, respectively. Otherwise, we continue
accumulating characters in the statement."
[res [c & cs]]
(cond (nil? c) [res cs]
(= c \;) [res cs]
(= c \/) #(c-comment-candidate statement-base res cs)
(= c \-) #(line-comment-candidate statement-base res cs)
(= c \') #(single-quoted-string (conj res c) cs)
(= c \") #(double-quoted-string (conj res c) cs)
:else #(statement-base (conj res c) cs)))
(defn statement
"Extracts the next SQL statement from a character sequence, skipping any
leading whitespace."
[res cs]
#(statement-beginning res cs))
(defn sql-statements
"Returns a sequence of SQL statements in the data that a reader points to."
[rdr]
(loop [res [] [stmt cs] (trampoline #(statement [] (char-seq rdr)))]
(if (empty? stmt)
res
(recur (conj res (apply str stmt))
(trampoline #(statement [] cs))))))
(defn exec-sql-statement
"A wrapper around korma.core/exec-raw that logs the statement that is being
executed if debugging is enabled."
[& statements]
(let [statement (string/join " " statements)]
(log/debug "executing SQL statement:" statement)
(exec-raw statement)))
(defn load-sql-file
"Loads a single SQL file into the database."
[sql-file]
(let [sql-file (fs/file sql-file)]
(log/info (str "Loading " (.getName sql-file) "..."))
(with-open [rdr (reader sql-file)]
(dorun (map exec-sql-statement (sql-statements rdr))))))