-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathparse.go
More file actions
203 lines (187 loc) · 4.57 KB
/
parse.go
File metadata and controls
203 lines (187 loc) · 4.57 KB
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package dotpgx
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/jackc/pgx"
)
type query struct {
sql string
ps *pgx.PreparedStatement
}
func (q *query) isPrepared() bool {
return q != nil && q.ps != nil
}
func (q *query) getSQL() string {
if q.isPrepared() {
return q.ps.Name
}
return q.sql
}
type queryMap map[string]*query
// Merge one or more queryMaps into the current.
// Any tags that are declared multiple times get overwritten
// and set to the last occurence.
func merge(maps ...queryMap) (qm queryMap) {
qm = make(queryMap)
for _, m := range maps {
for k, v := range m {
qm[k] = v
}
}
return
}
func (qm queryMap) getQuery(name string) (*query, error) {
if qm[name] == nil {
s := []string{"Unknown query", name}
return nil, errors.New(strings.Join(s, ": "))
}
return qm[name], nil
}
func (qm queryMap) sort() (index []string) {
for k := range qm {
index = append(index, k)
}
sort.Strings(index)
return
}
var mutex = &sync.Mutex{}
// ParseSQL parses and stores SQL queries from a io.Reader.
// Queries should end with a semi-colon.
// It stores queries by their "--name: <name>" tag.
// If no name tag is specified, an incremental number will be appointed.
// This might come in handy for sequential execution (like migrations).ParseSql
// Parsed queries get appended to the current map.
// If a name tag was already present, it will get overwritten by the new one parsed.
// The serial value is stored inside the DB object,
// so it is safe to call this function multiple times.
//
// If the input conains a double dollar sign "$$", the parser will ignore semi-colon
// untill another occurance of "$$". This makes parsing of functions possible.
func (db *DB) ParseSQL(r io.Reader) error {
sc := bufio.NewScanner(r)
comment := false
var tag string
var function bool
qm := make(queryMap)
for sc.Scan() {
// Read the line
line := sc.Text()
if err := sc.Err(); err != nil {
return err
}
// Sanetize leading and trailing whitespace
line = strings.TrimSpace(line)
// Line with name tag?
if strings.HasPrefix(line, "-- name:") || strings.HasPrefix(line, "--name:") {
tag = strings.TrimSpace(strings.Split(line, ":")[1])
// Initialise to empty query body, overwites any previous query with the same name
if err := db.DropQuery(tag); err != nil {
return err
}
qm[tag] = &query{}
continue
}
// Skip empty and comment lines
if len(line) == 0 || strings.HasPrefix(line, "--") {
continue
}
// Still inside comment block?
if comment {
if strings.HasSuffix(line, "*/") {
comment = false
}
continue
}
// Beginning of comment block?
if strings.HasPrefix(line, "/*") {
comment = true
continue
}
// Not in comment block and no tag set?
if len(tag) == 0 {
// Default to an auto-incremented tag number.
tag = fmt.Sprintf("%06d", db.qn)
db.qn++
qm[tag] = &query{}
}
// Inside of query body?
if len(tag) > 0 {
// Cut away inline comments
sql := strings.TrimSpace(strings.Split(line, "--")[0])
if len(qm[tag].sql) == 0 {
qm[tag].sql = sql
} else {
// Join with the existing body
j := []string{qm[tag].sql, sql}
qm[tag].sql = strings.Join(j, " ")
}
if function {
// End of function body?
if strings.Contains(sql, "$$") {
function = false
}
} else {
// Start of function body?
if strings.Contains(sql, "$$") {
function = true
}
}
// End of query body reached? (not in function body)
if strings.HasSuffix(line, ";") && !function {
tag = ""
}
continue
}
}
if len(qm) == 0 {
return errors.New("Nothing parsed")
}
mutex.Lock()
db.qm = merge(db.qm, qm)
mutex.Unlock()
return nil
}
// ParseFiles opens one or more files and feeds them to ParseSql
func (db *DB) ParseFiles(files ...string) error {
if len(files) == 0 {
return errors.New("No files to parse")
}
for _, v := range files {
f, err := os.Open(v)
if err != nil {
return err
}
err = db.ParseSQL(f)
f.Close()
if err != nil {
return err
}
}
return nil
}
// ParseFileGlob passes all files that match glob to ParseFiles.
// Subsequently those files get fed into DB.ParseSql.
// See filepath.glob for behavior.
func (db *DB) ParseFileGlob(glob string) error {
files, err := filepath.Glob(glob)
if err != nil {
return err
}
return db.ParseFiles(files...)
}
// ParsePath is a convenience wrapper.
// It uses ParseFileGlob to load all files in path, with a .sql suffix.
func (db *DB) ParsePath(path string) error {
s := []string{
path,
"*.sql",
}
return db.ParseFileGlob(strings.Join(s, "/"))
}