Skip to content

Commit 0470849

Browse files
committed
extended sql parser
1 parent 91cc282 commit 0470849

5 files changed

Lines changed: 77 additions & 4 deletions

File tree

lex.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const (
2828
boolLiteral
2929
nullKeyword
3030
singleQuotedStringLiteral
31+
rawSingleQuotedStringLiteral
3132
doubleQuotedStringLiteral
3233
caseBlock
3334
betweenToken
@@ -145,6 +146,7 @@ var rangeOperatorMatcher = parsly.NewToken(rangeOperator, ".. AND .. ", matcher.
145146
var nullKeywordMatcher = parsly.NewToken(nullKeyword, "NULL", matcher.NewKeyword("null", &option.Case{}))
146147
var boolLiteralMatcher = parsly.NewToken(boolLiteral, "true|false", matcher.NewSet([]string{"true", "false"}, &option.Case{}))
147148
var singleQuotedStringLiteralMatcher = parsly.NewToken(singleQuotedStringLiteral, `'...'`, matcher.NewByteQuote('\'', '\\'))
149+
var rawSingleQuotedStringLiteralMatcher = parsly.NewToken(rawSingleQuotedStringLiteral, `r'...'`, smatcher.NewPrefixedQuote([]byte{'r', 'R'}, '\'', '\\'))
148150
var doubleQuotedStringLiteralMatcher = parsly.NewToken(doubleQuotedStringLiteral, `"..."`, matcher.NewByteQuote('\'', '\\'))
149151
var intLiteralMatcher = parsly.NewToken(intLiteral, `INT`, smatcher.NewIntMatcher())
150152
var numericLiteralMatcher = parsly.NewToken(numericLiteral, `NUMERIC`, matcher.NewNumber())

literal.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import (
55
"github.com/viant/sqlparser/expr"
66
)
77

8-
//ParseLiteral parses literal
8+
// ParseLiteral parses literal
99
func ParseLiteral(cursor *parsly.Cursor) (*expr.Literal, error) {
1010
return parseLiteral(cursor, true)
1111
}
1212

13-
//TryParseLiteral tries to parse literal
13+
// TryParseLiteral tries to parse literal
1414
func TryParseLiteral(cursor *parsly.Cursor) (*expr.Literal, error) {
1515
return parseLiteral(cursor, false)
1616
}
@@ -20,6 +20,7 @@ var literalTokens = []*parsly.Token{
2020
nextMatcher,
2121
nullKeywordMatcher,
2222
boolLiteralMatcher,
23+
rawSingleQuotedStringLiteralMatcher,
2324
doubleQuotedStringLiteralMatcher,
2425
singleQuotedStringLiteralMatcher,
2526
intLiteralMatcher,
@@ -34,7 +35,7 @@ func parseLiteral(cursor *parsly.Cursor, shallRaiseInvalidToken bool) (*expr.Lit
3435
return nil, nil
3536
case nullKeyword:
3637
return expr.NewNullLiteral(match.Text(cursor)), nil
37-
case singleQuotedStringLiteral, doubleQuotedStringLiteral:
38+
case singleQuotedStringLiteral, rawSingleQuotedStringLiteral, doubleQuotedStringLiteral:
3839
return expr.NewStringLiteral(match.Text(cursor)), nil
3940
case boolLiteral:
4041
return expr.NewBoolLiteral(match.Text(cursor)), nil

matcher/prefixed_quote.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package matcher
2+
3+
import "github.com/viant/parsly"
4+
5+
type prefixedQuote struct {
6+
prefixes []byte
7+
quote byte
8+
escape byte
9+
}
10+
11+
// Match matches quoted strings prefixed by a single-byte marker (for example r'...').
12+
func (m *prefixedQuote) Match(cursor *parsly.Cursor) (matched int) {
13+
input := cursor.Input
14+
pos := cursor.Pos
15+
inputSize := len(input)
16+
if pos+1 >= inputSize {
17+
return 0
18+
}
19+
if !hasBytePrefix(m.prefixes, input[pos]) || input[pos+1] != m.quote {
20+
return 0
21+
}
22+
23+
matched = 2
24+
for i := pos + matched; i < inputSize; i++ {
25+
value := input[i]
26+
matched++
27+
if value == m.escape {
28+
if i+1 < inputSize {
29+
i++
30+
matched++
31+
}
32+
continue
33+
}
34+
if value == m.quote {
35+
return matched
36+
}
37+
}
38+
39+
return 0
40+
}
41+
42+
func hasBytePrefix(prefixes []byte, value byte) bool {
43+
for _, candidate := range prefixes {
44+
if value == candidate {
45+
return true
46+
}
47+
}
48+
return false
49+
}
50+
51+
// NewPrefixedQuote returns a matcher for prefixed quote literals.
52+
func NewPrefixedQuote(prefixes []byte, quote, escape byte) parsly.Matcher {
53+
return &prefixedQuote{
54+
prefixes: prefixes,
55+
quote: quote,
56+
escape: escape,
57+
}
58+
}

operand.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func expectOperand(cursor *parsly.Cursor) (node.Node, error) {
125125
list = append(list, &expr.Placeholder{Name: matched.Text(exprCursor)})
126126
case nullKeyword:
127127
list = append(list, expr.NewNullLiteral(matched.Text(exprCursor)))
128-
case singleQuotedStringLiteral, doubleQuotedStringLiteral:
128+
case singleQuotedStringLiteral, rawSingleQuotedStringLiteral, doubleQuotedStringLiteral:
129129
list = append(list, expr.NewStringLiteral(matched.Text(exprCursor)))
130130
case boolLiteral:
131131
list = append(list, expr.NewBoolLiteral(matched.Text(exprCursor)))

query_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,3 +420,15 @@ func TestParseSelect(t *testing.T) {
420420
}
421421
}
422422
}
423+
424+
func TestParseSelect_BigQueryRawRegexLiteral(t *testing.T) {
425+
sql := `SELECT LOWER(REGEXP_REPLACE(COALESCE(NULLIF(TRIM(s.NAME), ''), NULLIF(TRIM(s.MOBILE_URL), ''), ''),r'^(?:https?://)?(?:www\.)?', '')) AS site_domain FROM CI_SITE s`
426+
427+
parsed, err := ParseQuery(sql)
428+
if !assert.NoError(t, err) {
429+
return
430+
}
431+
432+
actual := strings.TrimSpace(Stringify(parsed))
433+
assert.Equal(t, sql, actual)
434+
}

0 commit comments

Comments
 (0)