-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathphrasebook.go
117 lines (102 loc) · 3.13 KB
/
phrasebook.go
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
// Package phrasebook implements the pattern for SQL queries.
//
// TODO(shane): Add a -debug mode for use in development like a lot of the bindata packages do.
package phrasebook
import (
"bufio"
"encoding/json"
"fmt"
"io"
"os"
"regexp"
"strings"
)
// commentRE matches leading comments before an export.
var commentRE = regexp.MustCompile(`(?m)(?:^|\n)(?P<comment>(?:(?:--|#[\t ])\s*[^\n]*\n)+)(?:--|#[\t ])\s*export[: ]`)
// commentTrimRE is a subexpression matches that can be used to extract comment text without the delimiter.
var commentTrimRE = regexp.MustCompile(`(?m)(?:^|\n)(?:--|#[\t ])\s*([^\n]*\n)`)
// ExportRE expression matches a query to be exported.
//
// -- List staff with a birthday for @date so you can buy them a beer.
// -- export: ListStaffBirthday, {"json":"optional","meta":"data"}
// select
// u.*
// from users u
// where
// role_in(u.id, 'staff')
// and u.birthday = coalesce(@date, current_date)::date;
// -- end
var ExportRE = regexp.MustCompile(`(?m)(?:^|\n)(?:(?:--|#[\t ])\s*[^\n]*\n)*(?:--|#[\t ])\s*export[: ]\s*(?P<name>[\p{L}][\p{L}\p{N}_]*)(?:\s*[, ]\s*(?P<meta>[^\n]+))?(?P<query>(?:\n.+)+)\n(?:--|#[\t ])\s*end`) // Hello from no (?x) land :P
// Exports phrasebook collection.
type Exports map[string]*Export
// Export SQL query.
type Export struct {
Name string
Comment string
Query string
Meta json.RawMessage
}
func (e Export) String() string {
return e.Query
}
// TODO: Advance to the next comment where possible or it will fill the buffer
// if it never matches anything which isn't ideal. Not that I expect many > 65kb
// phrasebooks without a single match.
func split(data []byte, eof bool) (advance int, token []byte, err error) {
match := ExportRE.FindIndex(data)
switch {
case match == nil:
return 0, nil, nil
case eof:
return len(data), data[:len(data)], nil
default:
return match[1] - 1, data[match[0]:match[1]], nil
}
}
// Parse an sql phrasebook.
// -- List staff with a birthday for @date so you can buy them a beer.
// -- export: ListStaffBirthday, {"json":"optional","meta":"data"}
// select
// u.*
// from users u
// where
// role_in(u.id, 'staff')
// and u.birthday = coalesce(@date, current_date)::date;
// -- end
func Parse(sql io.Reader) (Exports, error) {
exports := make(Exports, 0)
scan := bufio.NewScanner(sql)
scan.Split(split)
for scan.Scan() {
if len(scan.Bytes()) == 0 {
continue
}
export := &Export{}
if match := commentRE.FindStringSubmatch(scan.Text()); match != nil {
export.Comment = commentTrimRE.ReplaceAllString(match[1], "$1")
}
match := ExportRE.FindSubmatch(scan.Bytes())
export.Name = strings.TrimSpace(string(match[1]))
export.Query = strings.TrimSpace(string(match[3]))
json.Unmarshal(match[2], &export.Meta)
if _, ok := exports[export.Name]; ok {
return nil, fmt.Errorf("export %q appears twice in source", export.Name)
}
exports[export.Name] = export
}
return exports, nil
}
func MustParse(sql io.Reader) Exports {
exports, err := Parse(sql)
if err != nil {
panic(err)
}
return exports
}
func MustParseFile(file string) Exports {
sql, err := os.Open(file)
if err != nil {
panic(err)
}
return MustParse(sql)
}