Skip to content

Commit f23aaba

Browse files
lmanganiqxip
andauthored
case insensitive matches (#32)
Co-authored-by: qxip <qxip@mini-ams.local>
1 parent 00eef42 commit f23aaba

2 files changed

Lines changed: 552 additions & 46 deletions

File tree

querier/queryClient.go

Lines changed: 165 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@ import (
1313
"os"
1414
"path/filepath"
1515
"regexp"
16+
"strconv"
1617
"strings"
1718
"time"
1819

1920
"github.com/gigapi/gigapi-querier/core"
2021
_ "github.com/marcboeker/go-duckdb/v2"
22+
"github.com/apache/arrow-go/v18/arrow/array"
23+
"github.com/apache/arrow-go/v18/arrow"
2124
)
2225

2326
var db *sql.DB
@@ -115,14 +118,19 @@ func (q *QueryClient) ParseQuery(sql, dbName string) (*ParsedQuery, error) {
115118

116119
// Extract WHERE clause
117120
whereClause := ""
118-
whereParts := strings.Split(sql, " WHERE ")
121+
whereParts := strings.Split(strings.ToUpper(sql), " WHERE ")
119122
if len(whereParts) >= 2 {
120-
whereClause = whereParts[1]
121-
122-
// Remove other clauses
123-
for _, clause := range []string{" GROUP BY ", " ORDER BY ", " LIMIT ", " HAVING "} {
124-
if idx := strings.Index(strings.ToUpper(whereClause), clause); idx != -1 {
125-
whereClause = whereClause[:idx]
123+
// Convert back to original case for the WHERE clause part
124+
originalSql := sql
125+
whereStart := strings.Index(strings.ToUpper(originalSql), " WHERE ")
126+
if whereStart != -1 {
127+
whereClause = originalSql[whereStart+6:] // Skip " WHERE "
128+
129+
// Remove other clauses
130+
for _, clause := range []string{" GROUP BY ", " ORDER BY ", " LIMIT ", " HAVING "} {
131+
if idx := strings.Index(strings.ToUpper(whereClause), clause); idx != -1 {
132+
whereClause = whereClause[:idx]
133+
}
126134
}
127135
}
128136
}
@@ -132,9 +140,15 @@ func (q *QueryClient) ParseQuery(sql, dbName string) (*ParsedQuery, error) {
132140
// Extract time range
133141
timeRange := q.extractTimeRange(whereClause)
134142
if timeRange.Start != nil || timeRange.End != nil {
135-
log.Printf("Detected time range: %v to %v",
136-
time.Unix(0, *timeRange.Start).Format(time.RFC3339Nano),
137-
time.Unix(0, *timeRange.End).Format(time.RFC3339Nano))
143+
startStr := "nil"
144+
endStr := "nil"
145+
if timeRange.Start != nil {
146+
startStr = time.Unix(0, *timeRange.Start).Format(time.RFC3339Nano)
147+
}
148+
if timeRange.End != nil {
149+
endStr = time.Unix(0, *timeRange.End).Format(time.RFC3339Nano)
150+
}
151+
log.Printf("Detected time range: %v to %v", startStr, endStr)
138152
} else {
139153
log.Printf("No time range detected in WHERE clause")
140154
}
@@ -181,6 +195,34 @@ func (q *QueryClient) ParseQuery(sql, dbName string) (*ParsedQuery, error) {
181195
}, nil
182196
}
183197

198+
// detectTimeColumn finds the time column name in a WHERE clause
199+
func (q *QueryClient) detectTimeColumn(whereClause string) string {
200+
// Common time column names to look for
201+
timeColumnPatterns := []*regexp.Regexp{
202+
regexp.MustCompile(`(?i)(\w+)\s*(>=|<=|=|>|<)\s*'([^']+)'`), // any column with timestamp comparison
203+
regexp.MustCompile(`(?i)(\w+)\s*(>=|<=|=|>|<)\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)`), // any column with cast timestamp
204+
regexp.MustCompile(`(?i)(\w+)\s*(>=|<=|=|>|<)\s*epoch_ns\s*\(\s*'([^']+)'`), // any column with epoch_ns
205+
regexp.MustCompile(`(?i)(\w+)\s+BETWEEN\s+'([^']+)'\s+AND\s+'([^']+)'`), // any column with BETWEEN
206+
regexp.MustCompile(`(?i)(\w+)\s*(>=|<=|=|>|<)\s*(\d+)`), // any column with numeric comparison
207+
regexp.MustCompile(`(?i)(\w+)\s+BETWEEN\s+(\d+)\s+AND\s+(\d+)`), // any column with numeric BETWEEN
208+
}
209+
210+
for _, pattern := range timeColumnPatterns {
211+
matches := pattern.FindStringSubmatch(whereClause)
212+
if len(matches) > 1 {
213+
columnName := matches[1]
214+
// Check if this looks like a time column
215+
if strings.Contains(strings.ToLower(columnName), "time") ||
216+
strings.Contains(strings.ToLower(columnName), "timestamp") ||
217+
strings.Contains(strings.ToLower(columnName), "date") ||
218+
columnName == "__timestamp" {
219+
return columnName
220+
}
221+
}
222+
}
223+
return "time" // fallback to default
224+
}
225+
184226
// Extract time range from WHERE clause
185227
func (q *QueryClient) extractTimeRange(whereClause string) TimeRange {
186228
timeRange := TimeRange{
@@ -193,33 +235,41 @@ func (q *QueryClient) extractTimeRange(whereClause string) TimeRange {
193235
return timeRange
194236
}
195237

196-
// log.Printf("Extracting time range from WHERE clause: %s", whereClause)
238+
// Detect the time column name
239+
timeColumn := q.detectTimeColumn(whereClause)
240+
log.Printf("Detected time column: %s", timeColumn)
197241

198-
// Match time patterns including both simple timestamps and epoch_ns with various formats
242+
// Create dynamic patterns based on the detected time column
199243
timePatterns := []*regexp.Regexp{
200244
// Simple timestamp format
201-
regexp.MustCompile(`time\s*(>=|>)\s*'([^']+)'`), // time >= '2023-01-01T00:00:00Z'
202-
regexp.MustCompile(`time\s*(<=|<)\s*'([^']+)'`), // time <= '2023-01-01T00:00:00Z'
203-
regexp.MustCompile(`time\s*=\s*'([^']+)'`), // time = '2023-01-01T00:00:00Z'
204-
regexp.MustCompile(`time\s+BETWEEN\s+'([^']+)'\s+AND\s+'([^']+)'`), // time BETWEEN '...' AND '...'
245+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s*(>=|>)\s*'([^']+)'`, regexp.QuoteMeta(timeColumn))), // column >= '2023-01-01T00:00:00Z'
246+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s*(<=|<)\s*'([^']+)'`, regexp.QuoteMeta(timeColumn))), // column <= '2023-01-01T00:00:00Z'
247+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s*=\s*'([^']+)'`, regexp.QuoteMeta(timeColumn))), // column = '2023-01-01T00:00:00Z'
248+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s+BETWEEN\s+'([^']+)'\s+AND\s+'([^']+)'`, regexp.QuoteMeta(timeColumn))), // column BETWEEN '...' AND '...'
205249

206250
// Cast format
207-
regexp.MustCompile(`time\s*(>=|>)\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)`), // time >= cast('2023-01-01T00:00:00' as timestamp)
208-
regexp.MustCompile(`time\s*(<=|<)\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)`), // time <= cast('2023-01-01T00:00:00' as timestamp)
209-
regexp.MustCompile(`time\s*=\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)`), // time = cast('2023-01-01T00:00:00' as timestamp)
210-
regexp.MustCompile(`time\s+BETWEEN\s+cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)\s+AND\s+cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)`), // time BETWEEN cast('...') AND cast('...')
251+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s*(>=|>)\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)`, regexp.QuoteMeta(timeColumn))), // column >= cast('2023-01-01T00:00:00' as timestamp)
252+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s*(<=|<)\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)`, regexp.QuoteMeta(timeColumn))), // column <= cast('2023-01-01T00:00:00' as timestamp)
253+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s*=\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)`, regexp.QuoteMeta(timeColumn))), // column = cast('2023-01-01T00:00:00' as timestamp)
254+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s+BETWEEN\s+cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)\s+AND\s+cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)`, regexp.QuoteMeta(timeColumn))), // column BETWEEN cast('...') AND cast('...')
211255

212256
// Epoch_ns format
213-
regexp.MustCompile(`time\s*(>=|>)\s*epoch_ns\s*\(\s*'([^']+)'(?:::TIMESTAMP)?\s*\)`), // time >= epoch_ns('2023-01-01T00:00:00'::TIMESTAMP)
214-
regexp.MustCompile(`time\s*(<=|<)\s*epoch_ns\s*\(\s*'([^']+)'(?:::TIMESTAMP)?\s*\)`), // time <= epoch_ns('2023-01-01T00:00:00'::TIMESTAMP)
215-
regexp.MustCompile(`time\s*=\s*epoch_ns\s*\(\s*'([^']+)'(?:::TIMESTAMP)?\s*\)`), // time = epoch_ns('2023-01-01T00:00:00'::TIMESTAMP)
216-
regexp.MustCompile(`time\s+BETWEEN\s+epoch_ns\s*\(\s*'([^']+)'(?:::TIMESTAMP)?\s*\)\s+AND\s+epoch_ns\s*\(\s*'([^']+)'(?:::TIMESTAMP)?\s*\)`), // time BETWEEN epoch_ns('...') AND epoch_ns('...')
257+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s*(>=|>)\s*epoch_ns\s*\(\s*'([^']+)'(?:::TIMESTAMP)?\s*\)`, regexp.QuoteMeta(timeColumn))), // column >= epoch_ns('2023-01-01T00:00:00'::TIMESTAMP)
258+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s*(<=|<)\s*epoch_ns\s*\(\s*'([^']+)'(?:::TIMESTAMP)?\s*\)`, regexp.QuoteMeta(timeColumn))), // column <= epoch_ns('2023-01-01T00:00:00'::TIMESTAMP)
259+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s*=\s*epoch_ns\s*\(\s*'([^']+)'(?:::TIMESTAMP)?\s*\)`, regexp.QuoteMeta(timeColumn))), // column = epoch_ns('2023-01-01T00:00:00'::TIMESTAMP)
260+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s+BETWEEN\s+epoch_ns\s*\(\s*'([^']+)'(?:::TIMESTAMP)?\s*\)\s+AND\s+epoch_ns\s*\(\s*'([^']+)'(?:::TIMESTAMP)?\s*\)`, regexp.QuoteMeta(timeColumn))), // column BETWEEN epoch_ns('...') AND epoch_ns('...')
217261

218262
// Epoch_ns with cast format
219-
regexp.MustCompile(`time\s*(>=|>)\s*epoch_ns\s*\(\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)(?:::TIMESTAMP)?\s*\)`), // time >= epoch_ns(cast('2023-01-01T00:00:00' as timestamp)::TIMESTAMP)
220-
regexp.MustCompile(`time\s*(<=|<)\s*epoch_ns\s*\(\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)(?:::TIMESTAMP)?\s*\)`), // time <= epoch_ns(cast('2023-01-01T00:00:00' as timestamp)::TIMESTAMP)
221-
regexp.MustCompile(`time\s*=\s*epoch_ns\s*\(\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)(?:::TIMESTAMP)?\s*\)`), // time = epoch_ns(cast('2023-01-01T00:00:00' as timestamp)::TIMESTAMP)
222-
regexp.MustCompile(`time\s+BETWEEN\s+epoch_ns\s*\(\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)(?:::TIMESTAMP)?\s*\)\s+AND\s+epoch_ns\s*\(\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)(?:::TIMESTAMP)?\s*\)`), // time BETWEEN epoch_ns(cast('...')::TIMESTAMP) AND epoch_ns(cast('...')::TIMESTAMP)
263+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s*(>=|>)\s*epoch_ns\s*\(\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)(?:::TIMESTAMP)?\s*\)`, regexp.QuoteMeta(timeColumn))), // column >= epoch_ns(cast('2023-01-01T00:00:00' as timestamp)::TIMESTAMP)
264+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s*(<=|<)\s*epoch_ns\s*\(\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)(?:::TIMESTAMP)?\s*\)`, regexp.QuoteMeta(timeColumn))), // column <= epoch_ns(cast('2023-01-01T00:00:00' as timestamp)::TIMESTAMP)
265+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s*=\s*epoch_ns\s*\(\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)(?:::TIMESTAMP)?\s*\)`, regexp.QuoteMeta(timeColumn))), // column = epoch_ns(cast('2023-01-01T00:00:00' as timestamp)::TIMESTAMP)
266+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s+BETWEEN\s+epoch_ns\s*\(\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)(?:::TIMESTAMP)?\s*\)\s+AND\s+epoch_ns\s*\(\s*cast\s*\(\s*'([^']+)'\s+as\s+timestamp\s*\)(?:::TIMESTAMP)?\s*\)`, regexp.QuoteMeta(timeColumn))), // column BETWEEN epoch_ns(cast('...')::TIMESTAMP) AND epoch_ns(cast('...')::TIMESTAMP)
267+
268+
// Also handle numeric timestamp comparisons (like __timestamp >= 1747803600000000000)
269+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s*(>=|>)\s*(\d+)`, regexp.QuoteMeta(timeColumn))), // column >= 1747803600000000000
270+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s*(<=|<)\s*(\d+)`, regexp.QuoteMeta(timeColumn))), // column <= 1747825200000000000
271+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s*=\s*(\d+)`, regexp.QuoteMeta(timeColumn))), // column = 1747803600000000000
272+
regexp.MustCompile(fmt.Sprintf(`(?i)%s\s+BETWEEN\s+(\d+)\s+AND\s+(\d+)`, regexp.QuoteMeta(timeColumn))), // column BETWEEN 1747803600000000000 AND 1747825200000000000
223273
}
224274

225275
var startTime, endTime time.Time
@@ -228,15 +278,13 @@ func (q *QueryClient) extractTimeRange(whereClause string) TimeRange {
228278

229279
for i, pattern := range timePatterns {
230280
matches := pattern.FindStringSubmatch(whereClause)
231-
// log.Printf("Trying pattern: %s", pattern.String())
232281
if len(matches) > 0 {
233282
log.Printf("Found matches: %v", matches)
234283

235-
// Handle BETWEEN patterns
236-
if i == 3 || i == 7 || i == 11 || i == 15 { // BETWEEN patterns
284+
// Handle BETWEEN patterns (string timestamps)
285+
if i == 3 || i == 7 || i == 11 || i == 15 { // BETWEEN patterns with string timestamps
237286
startTimestamp := matches[1]
238287
endTimestamp := matches[2]
239-
// log.Printf("BETWEEN clause: start=%s, end=%s", startTimestamp, endTimestamp)
240288

241289
startTime, err = time.Parse(time.RFC3339Nano, startTimestamp)
242290
if err != nil {
@@ -261,10 +309,33 @@ func (q *QueryClient) extractTimeRange(whereClause string) TimeRange {
261309
break
262310
}
263311

264-
// Handle = patterns
265-
if i == 2 || i == 6 || i == 10 || i == 14 { // = patterns
312+
// Handle BETWEEN patterns (numeric timestamps)
313+
if i == 19 { // BETWEEN patterns with numeric timestamps
314+
startTimestamp := matches[1]
315+
endTimestamp := matches[2]
316+
317+
startNano, err := strconv.ParseInt(startTimestamp, 10, 64)
318+
if err != nil {
319+
log.Printf("Error parsing start timestamp %s: %v", startTimestamp, err)
320+
continue
321+
}
322+
startTime = time.Unix(0, startNano)
323+
324+
endNano, err := strconv.ParseInt(endTimestamp, 10, 64)
325+
if err != nil {
326+
log.Printf("Error parsing end timestamp %s: %v", endTimestamp, err)
327+
continue
328+
}
329+
endTime = time.Unix(0, endNano)
330+
331+
startOp = ">="
332+
endOp = "<="
333+
break
334+
}
335+
336+
// Handle = patterns (string timestamps)
337+
if i == 2 || i == 6 || i == 10 || i == 14 { // = patterns with string timestamps
266338
timestamp := matches[1]
267-
// log.Printf("Equal timestamp: %s", timestamp)
268339

269340
parsedTime, err := time.Parse(time.RFC3339Nano, timestamp)
270341
if err != nil {
@@ -282,11 +353,28 @@ func (q *QueryClient) extractTimeRange(whereClause string) TimeRange {
282353
break
283354
}
284355

285-
// Handle >= and <= patterns
286-
if len(matches) == 3 {
356+
// Handle = patterns (numeric timestamps)
357+
if i == 18 { // = patterns with numeric timestamps
358+
timestamp := matches[1]
359+
360+
nano, err := strconv.ParseInt(timestamp, 10, 64)
361+
if err != nil {
362+
log.Printf("Error parsing timestamp %s: %v", timestamp, err)
363+
continue
364+
}
365+
parsedTime := time.Unix(0, nano)
366+
367+
startTime = parsedTime
368+
endTime = parsedTime
369+
startOp = ">="
370+
endOp = "<="
371+
break
372+
}
373+
374+
// Handle >= and <= patterns (string timestamps)
375+
if len(matches) == 3 && (i == 0 || i == 1 || i == 4 || i == 5 || i == 8 || i == 9 || i == 12 || i == 13) {
287376
timestamp := matches[2]
288377
op := matches[1]
289-
// log.Printf("Single timestamp comparison: op=%s, timestamp=%s", op, timestamp)
290378

291379
parsedTime, err := time.Parse(time.RFC3339Nano, timestamp)
292380
if err != nil {
@@ -305,24 +393,45 @@ func (q *QueryClient) extractTimeRange(whereClause string) TimeRange {
305393
endOp = op
306394
}
307395
}
396+
397+
// Handle >= and <= patterns (numeric timestamps)
398+
if len(matches) == 3 && (i == 16 || i == 17) {
399+
timestamp := matches[2]
400+
op := matches[1]
401+
402+
nano, err := strconv.ParseInt(timestamp, 10, 64)
403+
if err != nil {
404+
log.Printf("Error parsing timestamp %s: %v", timestamp, err)
405+
continue
406+
}
407+
parsedTime := time.Unix(0, nano)
408+
409+
if op == ">=" || op == ">" {
410+
startTime = parsedTime
411+
startOp = op
412+
} else if op == "<=" || op == "<" {
413+
endTime = parsedTime
414+
endOp = op
415+
}
416+
}
308417
}
309418
}
310419

311420
if !startTime.IsZero() {
312421
startNano := startTime.UnixNano()
313422
timeRange.Start = &startNano
314-
timeRange.TimeCondition = fmt.Sprintf("time %s epoch_ns('%s'::TIMESTAMP)", startOp, startTime.Format(time.RFC3339))
423+
timeRange.TimeCondition = fmt.Sprintf("%s %s epoch_ns('%s'::TIMESTAMP)", timeColumn, startOp, startTime.Format(time.RFC3339))
315424
}
316425

317426
if !endTime.IsZero() {
318427
endNano := endTime.UnixNano()
319428
timeRange.End = &endNano
320429
if timeRange.TimeCondition != "" {
321-
timeRange.TimeCondition = fmt.Sprintf("%s AND time %s epoch_ns('%s'::TIMESTAMP)",
322-
timeRange.TimeCondition, endOp, endTime.Format(time.RFC3339))
430+
timeRange.TimeCondition = fmt.Sprintf("%s AND %s %s epoch_ns('%s'::TIMESTAMP)",
431+
timeRange.TimeCondition, timeColumn, endOp, endTime.Format(time.RFC3339))
323432
} else {
324-
timeRange.TimeCondition = fmt.Sprintf("time %s epoch_ns('%s'::TIMESTAMP)",
325-
endOp, endTime.Format(time.RFC3339))
433+
timeRange.TimeCondition = fmt.Sprintf("%s %s epoch_ns('%s'::TIMESTAMP)",
434+
timeColumn, endOp, endTime.Format(time.RFC3339))
326435
}
327436
}
328437

@@ -836,7 +945,12 @@ func (c *QueryClient) Query(ctx context.Context, query, dbName string) ([]map[st
836945
}
837946

838947
// Split the original query and rebuild with file list
839-
originalParts := strings.SplitN(query, " FROM ", 2)
948+
// Use case-insensitive split for FROM clause
949+
fromIndex := strings.Index(upperQuery, " FROM ")
950+
if fromIndex == -1 {
951+
return nil, fmt.Errorf("invalid query: FROM clause not found")
952+
}
953+
originalParts := []string{query[:fromIndex], query[fromIndex+6:]} // 6 is length of " FROM "
840954
var duckdbQuery string
841955

842956
if len(originalParts) >= 2 {
@@ -846,7 +960,7 @@ func (c *QueryClient) Query(ctx context.Context, query, dbName string) ([]map[st
846960
restOfQuery := tableRegex.ReplaceAllString(originalParts[1], "")
847961

848962
// Replace any simple timestamp comparisons with epoch_ns
849-
timestampRegex := regexp.MustCompile(`time\s*(>=|<=|=|>|<)\s*cast\('([^']+)'\s+as\s+timestamp\)`)
963+
timestampRegex := regexp.MustCompile(`(?i)time\s*(>=|<=|=|>|<)\s*cast\('([^']+)'\s+as\s+timestamp\)`)
850964
restOfQuery = timestampRegex.ReplaceAllString(restOfQuery, "time $1 epoch_ns('$2'::TIMESTAMP)")
851965

852966
log.Printf("Modified query part: %s", restOfQuery)
@@ -928,6 +1042,12 @@ func (c *QueryClient) Query(ctx context.Context, query, dbName string) ([]map[st
9281042
return result, nil
9291043
}
9301044

1045+
// QueryArrow executes a query against DuckDB and returns an Arrow RecordReader and schema
1046+
func (c *QueryClient) QueryArrow(ctx context.Context, query string) (array.RecordReader, *arrow.Schema, error) {
1047+
// TODO: Implement Arrow streaming for the current DuckDB Go driver version
1048+
return nil, nil, fmt.Errorf("Arrow streaming not implemented for this DuckDB Go driver version")
1049+
}
1050+
9311051
// Close releases resources
9321052
func (q *QueryClient) Close() error {
9331053
if q.DB != nil {

0 commit comments

Comments
 (0)