Skip to content

Commit c006596

Browse files
authored
Merge pull request #10 from VictoriaMetrics/json-value
Implement JSON_VALUE function from ANSI SQL (#9)
2 parents dd3ffdf + 733b2b0 commit c006596

File tree

6 files changed

+580
-2
lines changed

6 files changed

+580
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ Supported highlights:
135135
- arithmetic expressions,
136136
- string helpers (`SUBSTR`, `CONCAT`, `TRIM`, `REPLACE`, `LOWER`, `UPPER`),
137137
- math (`ABS`, `CEIL`, `FLOOR`, `ROUND`, `LEAST`, `GREATEST`),
138+
- JSON (`JSON_VALUE`),
138139
- and date helpers (`CURRENT_DATE`, `CURRENT_TIMESTAMP`).
139140
- `WHERE` with comparison operators, `BETWEEN`, `IN`, `LIKE`, `IS (NOT) NULL`
140141
- `ORDER BY`, `LIMIT`, `OFFSET`, `DISTINCT`

cmd/sql-to-logsql/web/ui/src/components/docs/Docs.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,12 @@ export function Docs() {
8282
<p>
8383
<ul className={"list-disc pl-4 pt-2"}>
8484
<li><code>SUBSTR, CONCAT, LOWER, UPPER, TRIM, LTRIM, RTRIM, REPLACE</code></li>
85-
<li><code>LIKE, NOT LIKE</code></li>
86-
<li><code>BETWEEN</code></li>
85+
<li><code>LIKE, NOT LIKE, =, !=, &lt;, &gt;, &lt;=, &gt;=, BETWEEN</code></li>
8786
<li><code>+,-, *, /, %, ^</code></li>
8887
<li><code>ABS, GREATEST, LEAST, ROUND, FLOOR, CEIL, POW, LN, EXP</code></li>
8988
<li><code>SUM, COUNT, MAX, MIN, AVG</code></li>
9089
<li><code>CURRENT_TIMESTAMP, CURREN_DATE</code></li>
90+
<li><code>JSON_VALUE</code></li>
9191
</ul>
9292
</p>
9393
</AccordionContent>

lib/logsql/jsonpath.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package logsql
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strconv"
7+
"strings"
8+
"unicode"
9+
)
10+
11+
type jsonPath struct {
12+
Strict bool
13+
Steps []jsonPathStep
14+
}
15+
16+
type jsonPathStep struct {
17+
Key string
18+
Index *int
19+
}
20+
21+
func parseJSONPath(raw string) (*jsonPath, error) {
22+
if strings.TrimSpace(raw) == "" {
23+
return nil, &TranslationError{
24+
Code: http.StatusBadRequest,
25+
Message: "translator: JSON path cannot be empty",
26+
}
27+
}
28+
29+
path := strings.TrimSpace(raw)
30+
lower := strings.ToLower(path)
31+
result := &jsonPath{}
32+
33+
switch {
34+
case strings.HasPrefix(lower, "strict "):
35+
result.Strict = true
36+
path = strings.TrimSpace(path[len("strict "):])
37+
case strings.HasPrefix(lower, "lax "):
38+
result.Strict = false
39+
path = strings.TrimSpace(path[len("lax "):])
40+
}
41+
42+
if path == "" || path[0] != '$' {
43+
return nil, &TranslationError{
44+
Code: http.StatusBadRequest,
45+
Message: "translator: JSON path must start with $",
46+
}
47+
}
48+
path = path[1:]
49+
runes := []rune(path)
50+
51+
i := 0
52+
for {
53+
skipSpaces(runes, &i)
54+
if i >= len(runes) {
55+
break
56+
}
57+
58+
switch runes[i] {
59+
case '.':
60+
i++
61+
skipSpaces(runes, &i)
62+
if i >= len(runes) {
63+
return nil, &TranslationError{
64+
Code: http.StatusBadRequest,
65+
Message: "translator: JSON path ends unexpectedly after .",
66+
}
67+
}
68+
start := i
69+
for i < len(runes) {
70+
r := runes[i]
71+
if r == '.' || r == '[' {
72+
break
73+
}
74+
i++
75+
}
76+
if start == i {
77+
return nil, &TranslationError{
78+
Code: http.StatusBadRequest,
79+
Message: "translator: JSON path contains empty segment",
80+
}
81+
}
82+
key := strings.TrimSpace(string(runes[start:i]))
83+
if key == "" {
84+
return nil, &TranslationError{
85+
Code: http.StatusBadRequest,
86+
Message: "translator: JSON path contains empty segment",
87+
}
88+
}
89+
result.Steps = append(result.Steps, jsonPathStep{Key: key})
90+
case '[':
91+
i++
92+
skipSpaces(runes, &i)
93+
if i >= len(runes) {
94+
return nil, &TranslationError{
95+
Code: http.StatusBadRequest,
96+
Message: "translator: JSON path has unterminated []",
97+
}
98+
}
99+
step, err := parseBracketStep(runes, &i)
100+
if err != nil {
101+
return nil, err
102+
}
103+
result.Steps = append(result.Steps, step)
104+
skipSpaces(runes, &i)
105+
if i >= len(runes) || runes[i] != ']' {
106+
return nil, &TranslationError{
107+
Code: http.StatusBadRequest,
108+
Message: "translator: JSON path has unterminated []",
109+
}
110+
}
111+
i++
112+
default:
113+
return nil, &TranslationError{
114+
Code: http.StatusBadRequest,
115+
Message: fmt.Sprintf("translator: unsupported JSON path token %q", string(runes[i])),
116+
}
117+
}
118+
}
119+
120+
if len(result.Steps) == 0 {
121+
return nil, &TranslationError{
122+
Code: http.StatusBadRequest,
123+
Message: "translator: JSON path must reference nested field",
124+
}
125+
}
126+
return result, nil
127+
}
128+
129+
func (p *jsonPath) HasOnlyKeys() ([]string, bool) {
130+
if p == nil {
131+
return nil, false
132+
}
133+
keys := make([]string, 0, len(p.Steps))
134+
for _, step := range p.Steps {
135+
if step.Index != nil || step.Key == "" {
136+
return nil, false
137+
}
138+
keys = append(keys, step.Key)
139+
}
140+
return keys, true
141+
}
142+
143+
func parseBracketStep(runes []rune, pos *int) (jsonPathStep, error) {
144+
if *pos >= len(runes) {
145+
return jsonPathStep{}, &TranslationError{
146+
Code: http.StatusBadRequest,
147+
Message: "translator: JSON path has unterminated []",
148+
}
149+
}
150+
switch runes[*pos] {
151+
case '\'', '"':
152+
quote := runes[*pos]
153+
*pos++
154+
var builder strings.Builder
155+
for *pos < len(runes) {
156+
r := runes[*pos]
157+
*pos++
158+
if r == '\\' {
159+
if *pos >= len(runes) {
160+
return jsonPathStep{}, &TranslationError{
161+
Code: http.StatusBadRequest,
162+
Message: "translator: JSON path has invalid escape sequence",
163+
}
164+
}
165+
builder.WriteRune(runes[*pos])
166+
*pos++
167+
continue
168+
}
169+
if r == quote {
170+
break
171+
}
172+
builder.WriteRune(r)
173+
}
174+
key := strings.TrimSpace(builder.String())
175+
if key == "" {
176+
return jsonPathStep{}, &TranslationError{
177+
Code: http.StatusBadRequest,
178+
Message: "translator: JSON path contains empty segment",
179+
}
180+
}
181+
return jsonPathStep{Key: key}, nil
182+
default:
183+
start := *pos
184+
for *pos < len(runes) && unicode.IsDigit(runes[*pos]) {
185+
*pos++
186+
}
187+
if start == *pos {
188+
return jsonPathStep{}, &TranslationError{
189+
Code: http.StatusBadRequest,
190+
Message: "translator: JSON path contains unsupported token inside []",
191+
}
192+
}
193+
value := string(runes[start:*pos])
194+
index, err := strconv.Atoi(value)
195+
if err != nil {
196+
return jsonPathStep{}, &TranslationError{
197+
Code: http.StatusBadRequest,
198+
Message: "translator: JSON path contains unsupported index",
199+
Err: err,
200+
}
201+
}
202+
return jsonPathStep{Index: &index}, nil
203+
}
204+
}
205+
206+
func skipSpaces(runes []rune, pos *int) {
207+
for *pos < len(runes) && unicode.IsSpace(runes[*pos]) {
208+
*pos++
209+
}
210+
}

0 commit comments

Comments
 (0)