Skip to content

Commit cccffde

Browse files
authored
Implementing the multiple query execution capability (#187)
1 parent b5c8b05 commit cccffde

9 files changed

Lines changed: 749 additions & 97 deletions

File tree

connection.go

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,11 @@ func (v *connection) PrepareContext(ctx context.Context, query string) (driver.S
164164
return nil, err
165165
}
166166

167-
if v.usePreparedStmts {
167+
if len(strings.TrimSpace(s.command)) == 0 {
168+
return s, nil
169+
}
170+
171+
if v.usePreparedStmts && !s.multiStatements {
168172
if err = s.prepareAndDescribe(); err != nil {
169173
return nil, err
170174
}
@@ -507,43 +511,34 @@ func (v *connection) initializeSession() error {
507511
return err
508512
}
509513

510-
result, err := stmt.QueryContextRaw(context.Background(), []driver.NamedValue{})
511-
514+
resultRows, err := stmt.QueryContextRaw(context.Background(), []driver.NamedValue{})
512515
if err != nil {
513516
return err
514517
}
518+
defer resultRows.Close()
515519

516-
firstRow := result.resultData.Peek()
517-
518-
if len(result.Columns()) != 1 && result.Columns()[1] != "now" || firstRow == nil {
520+
columns := resultRows.Columns()
521+
if len(columns) != 1 || strings.ToLower(columns[0]) != "now" {
519522
return fmt.Errorf("unable to initialize session; functionality may be unreliable")
520523
}
521524

522-
// Peek into the results manually.
523-
colData := firstRow.Columns()
524-
str := string(colData.Chunk())
525+
values := make([]driver.Value, len(columns))
526+
if err := resultRows.Next(values); err != nil {
527+
return fmt.Errorf("unable to read server time: %w", err)
528+
}
525529

526-
if len(str) < 23 {
527-
return fmt.Errorf("can't get server timezone: %s", str)
530+
timestamp, ok := values[0].(time.Time)
531+
if !ok {
532+
return fmt.Errorf("unexpected time value %T", values[0])
528533
}
529534

530-
v.serverTZOffset = getTimeZoneOffset(str)
535+
v.serverTZOffset = timestamp.Format("-07:00")
531536

532537
connectionLogger.Debug("Setting server timezone offset to %s", v.serverTZOffset)
533538

534539
return nil
535540
}
536541

537-
func getTimeZoneOffset(str string) string {
538-
for i := len(str) - 1; i >= 0 && i >= len(str)-8; i-- {
539-
ch := str[i]
540-
if ch == '+' || ch == '-' {
541-
return str[i:]
542-
}
543-
}
544-
return "+00"
545-
}
546-
547542
func (v *connection) defaultMessageHandler(bMsg msgs.BackEndMsg) (bool, error) {
548543

549544
handled := true
@@ -574,6 +569,8 @@ func (v *connection) defaultMessageHandler(bMsg msgs.BackEndMsg) (bool, error) {
574569
connectionLogger.Info("NOTICE: %s", msg.Message)
575570
case *msgs.BEParamStatusMsg:
576571
connectionLogger.Debug("%v", msg)
572+
case *msgs.BEParseCompleteMsg:
573+
connectionLogger.Trace("parse complete")
577574
default:
578575
handled = false
579576
err = fmt.Errorf("unhandled message: %v", msg)

driver_test.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -875,17 +875,20 @@ func TestTimestampParsers(t *testing.T) {
875875
assertEqual(t, fmt.Sprintf("%s", val)[0:33], "-2018-01-27 21:09:44.843913 -0400")
876876
}
877877

878-
func TestEmptyStatementError(t *testing.T) {
878+
func TestEmptyStatementNoOp(t *testing.T) {
879879
connDB := openConnection(t)
880880
defer closeConnection(t, connDB)
881881

882-
// Try as exec.
883882
_, err := connDB.ExecContext(ctx, "")
884-
assertErr(t, err, "empty statement")
883+
assertNoErr(t, err)
884+
885+
rows, err := connDB.QueryContext(ctx, "")
886+
assertNoErr(t, err)
887+
defer rows.Close()
885888

886-
// Try as query.
887-
_, err = connDB.QueryContext(ctx, "")
888-
assertErr(t, err, "empty statement")
889+
hasRow := rows.Next()
890+
assertEqual(t, hasRow, false)
891+
assertNoErr(t, rows.Err())
889892
}
890893

891894
func TestValueTypes(t *testing.T) {

go.mod

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,4 @@ module github.com/vertica/vertica-sql-go
22

33
go 1.13
44

5-
require (
6-
github.com/elastic/go-sysinfo v1.8.1
7-
github.com/pquerna/otp v1.5.0
8-
github.com/stretchr/testify v1.3.0
9-
)
5+
require github.com/elastic/go-sysinfo v1.8.1

go.sum

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
2-
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
31
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
42
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
53
github.com/elastic/go-sysinfo v1.8.1 h1:4Yhj+HdV6WjbCRgGdZpPJ8lZQlXZLKDAeIkmQ/VRvi4=
@@ -16,8 +14,6 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
1614
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
1715
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1816
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
19-
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
20-
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
2117
github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0 h1:c8R11WC8m7KNMkTv/0+Be8vvwo4I3/Ut9AC2FW8fX3U=
2218
github.com/prometheus/procfs v0.0.0-20190425082905-87a4384529e0/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
2319
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=

parse/statements.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package parse
2+
3+
import (
4+
"strings"
5+
"unicode"
6+
)
7+
8+
// Copyright (c) 2020-2026 Open Text.
9+
10+
// Licensed under the Apache License, Version 2.0 (the "License");
11+
// you may not use this file except in compliance with the License.
12+
// You may obtain a copy of the License at
13+
14+
// http://www.apache.org/licenses/LICENSE-2.0
15+
16+
// Unless required by applicable law or agreed to in writing, software
17+
// distributed under the License is distributed on an "AS IS" BASIS,
18+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19+
// See the License for the specific language governing permissions and
20+
// limitations under the License.
21+
22+
// SplitStatements breaks a SQL string into individual statements separated by semicolons
23+
// that are not contained within literals or comments.
24+
func SplitStatements(query string) []string {
25+
trimmed := strings.TrimSpace(query)
26+
if trimmed == "" {
27+
return nil
28+
}
29+
30+
var statements []string
31+
var current strings.Builder
32+
// Track our current lexical state so we can ignore semicolons that live
33+
// inside literals or comments.
34+
inSingleQuote := false
35+
inDoubleQuote := false
36+
inLineComment := false
37+
inBlockComment := false
38+
var dollarTag string
39+
statementHasContent := false
40+
41+
markNonWhitespace := func(b byte) {
42+
if !unicode.IsSpace(rune(b)) {
43+
statementHasContent = true
44+
}
45+
}
46+
flush := func() {
47+
statement := strings.TrimSpace(current.String())
48+
current.Reset()
49+
if statement != "" && statementHasContent {
50+
statements = append(statements, statement)
51+
}
52+
statementHasContent = false
53+
}
54+
55+
i := 0
56+
for i < len(query) {
57+
ch := query[i]
58+
59+
if inLineComment {
60+
// Swallow comment text but keep the newline terminator so tokens remain separated.
61+
if ch == '\n' || ch == '\r' {
62+
current.WriteByte(ch)
63+
inLineComment = false
64+
}
65+
i++
66+
continue
67+
}
68+
69+
if inBlockComment {
70+
// Traditional /* ... */ comments block statement splitting
71+
// until the closing marker is found.
72+
current.WriteByte(ch)
73+
if ch == '*' && i+1 < len(query) && query[i+1] == '/' {
74+
current.WriteByte('/')
75+
i += 2
76+
inBlockComment = false
77+
continue
78+
}
79+
i++
80+
continue
81+
}
82+
83+
if inSingleQuote {
84+
// Stay inside the literal, handling doubled single quotes.
85+
current.WriteByte(ch)
86+
markNonWhitespace(ch)
87+
if ch == '\'' {
88+
if i+1 < len(query) && query[i+1] == '\'' {
89+
current.WriteByte('\'')
90+
markNonWhitespace('\'')
91+
i += 2
92+
continue
93+
}
94+
inSingleQuote = false
95+
}
96+
i++
97+
continue
98+
}
99+
100+
if inDoubleQuote {
101+
// Identifiers can be quoted with double quotes; treat them like
102+
// strings for splitter purposes.
103+
current.WriteByte(ch)
104+
markNonWhitespace(ch)
105+
if ch == '"' {
106+
if i+1 < len(query) && query[i+1] == '"' {
107+
current.WriteByte('"')
108+
markNonWhitespace('"')
109+
i += 2
110+
continue
111+
}
112+
inDoubleQuote = false
113+
}
114+
i++
115+
continue
116+
}
117+
118+
if dollarTag != "" {
119+
statementHasContent = true
120+
// Inside a dollar-quoted literal; exit only when the exact tag is
121+
// observed again.
122+
if i+len(dollarTag) <= len(query) && query[i:i+len(dollarTag)] == dollarTag {
123+
current.WriteString(dollarTag)
124+
markNonWhitespace(dollarTag[0])
125+
i += len(dollarTag)
126+
dollarTag = ""
127+
continue
128+
}
129+
current.WriteByte(ch)
130+
i++
131+
continue
132+
}
133+
134+
if ch == '\'' {
135+
inSingleQuote = true
136+
current.WriteByte(ch)
137+
markNonWhitespace(ch)
138+
i++
139+
continue
140+
}
141+
142+
if ch == '"' {
143+
inDoubleQuote = true
144+
current.WriteByte(ch)
145+
markNonWhitespace(ch)
146+
i++
147+
continue
148+
}
149+
150+
if ch == '-' && i+1 < len(query) && query[i+1] == '-' {
151+
i += 2
152+
inLineComment = true
153+
continue
154+
}
155+
156+
if ch == '/' && i+1 < len(query) {
157+
next := query[i+1]
158+
if next == '*' {
159+
current.WriteByte('/')
160+
current.WriteByte('*')
161+
i += 2
162+
inBlockComment = true
163+
continue
164+
}
165+
if next == '/' {
166+
i += 2
167+
inLineComment = true
168+
continue
169+
}
170+
}
171+
172+
if ch == '$' {
173+
if tag, length, ok := readDollarTag(query, i); ok {
174+
dollarTag = tag
175+
current.WriteString(tag)
176+
markNonWhitespace(tag[0])
177+
i += length
178+
continue
179+
}
180+
}
181+
182+
if ch == ';' {
183+
flush()
184+
i++
185+
continue
186+
}
187+
188+
current.WriteByte(ch)
189+
markNonWhitespace(ch)
190+
i++
191+
}
192+
193+
flush()
194+
return statements
195+
}
196+
197+
func readDollarTag(query string, start int) (string, int, bool) {
198+
if query[start] != '$' {
199+
return "", 0, false
200+
}
201+
202+
end := start + 1
203+
for end < len(query) {
204+
r := rune(query[end])
205+
if query[end] == '$' {
206+
return query[start : end+1], end + 1 - start, true
207+
}
208+
if !isDollarTagRune(r) {
209+
break
210+
}
211+
end++
212+
}
213+
214+
return "", 0, false
215+
}
216+
217+
func isDollarTagRune(r rune) bool {
218+
return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_'
219+
}

0 commit comments

Comments
 (0)