Skip to content

Commit aaae14e

Browse files
EDsCODEclaude
andcommitted
Fix CSV COPY parsing to handle quoted fields with embedded delimiters
The simple strings.Split() was incorrectly splitting CSV fields that contained the delimiter character within quoted strings. For example, a URL like "https://example.com?a=1,b=2" would be split into multiple values instead of being treated as a single field. This caused "expected X columns but Y values were supplied" errors when COPY FROM STDIN data contained commas in quoted fields. Fix: Use Go's encoding/csv package which properly handles RFC 4180 CSV parsing with quoted fields. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 63a2747 commit aaae14e

File tree

2 files changed

+89
-2
lines changed

2 files changed

+89
-2
lines changed

server/conn.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"crypto/tls"
77
"database/sql"
88
"encoding/binary"
9+
"encoding/csv"
910
"fmt"
1011
"io"
1112
"log"
@@ -914,8 +915,17 @@ func (c *clientConn) formatCopyValue(v interface{}) string {
914915

915916
// parseCopyLine parses a line of COPY input
916917
func (c *clientConn) parseCopyLine(line, delimiter string) []string {
917-
// Simple split - doesn't handle quoted values yet
918-
return strings.Split(line, delimiter)
918+
// Use encoding/csv for proper handling of quoted values
919+
reader := csv.NewReader(strings.NewReader(line))
920+
reader.Comma = rune(delimiter[0])
921+
reader.LazyQuotes = true // Be lenient with quotes
922+
923+
fields, err := reader.Read()
924+
if err != nil {
925+
// Fall back to simple split if CSV parsing fails
926+
return strings.Split(line, delimiter)
927+
}
928+
return fields
919929
}
920930

921931
func (c *clientConn) sendRowDescription(cols []string, colTypes []*sql.ColumnType) error {

server/conn_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,3 +565,80 @@ func TestQueryReturnsResultsWithComments(t *testing.T) {
565565

566566
// Note: isIgnoredSetParameter tests have been moved to transpiler/transpiler_test.go.
567567
// The transpiler package now handles SET parameter filtering via AST transformation.
568+
569+
func TestParseCopyLine(t *testing.T) {
570+
c := &clientConn{}
571+
572+
tests := []struct {
573+
name string
574+
line string
575+
delimiter string
576+
expected []string
577+
}{
578+
{
579+
name: "simple CSV values",
580+
line: `a,b,c`,
581+
delimiter: ",",
582+
expected: []string{"a", "b", "c"},
583+
},
584+
{
585+
name: "quoted value with embedded comma",
586+
line: `"hello, world",normal,value`,
587+
delimiter: ",",
588+
expected: []string{"hello, world", "normal", "value"},
589+
},
590+
{
591+
name: "multiple quoted values with commas",
592+
line: `"value, one","value, two","value, three"`,
593+
delimiter: ",",
594+
expected: []string{"value, one", "value, two", "value, three"},
595+
},
596+
{
597+
name: "mixed quoted and unquoted",
598+
line: `id,"url with, comma",status`,
599+
delimiter: ",",
600+
expected: []string{"id", "url with, comma", "status"},
601+
},
602+
{
603+
name: "tab-separated values",
604+
line: "a\tb\tc",
605+
delimiter: "\t",
606+
expected: []string{"a", "b", "c"},
607+
},
608+
{
609+
name: "quoted value with embedded tab",
610+
line: "\"hello\tworld\"\tnormal",
611+
delimiter: "\t",
612+
expected: []string{"hello\tworld", "normal"},
613+
},
614+
{
615+
name: "empty values",
616+
line: `a,,c`,
617+
delimiter: ",",
618+
expected: []string{"a", "", "c"},
619+
},
620+
{
621+
name: "URL with comma in quoted field",
622+
line: `"cs_123","https://example.com/success?a=1,b=2",active`,
623+
delimiter: ",",
624+
expected: []string{"cs_123", "https://example.com/success?a=1,b=2", "active"},
625+
},
626+
}
627+
628+
for _, tt := range tests {
629+
t.Run(tt.name, func(t *testing.T) {
630+
result := c.parseCopyLine(tt.line, tt.delimiter)
631+
if len(result) != len(tt.expected) {
632+
t.Errorf("parseCopyLine(%q, %q) returned %d values, want %d\nGot: %v\nWant: %v",
633+
tt.line, tt.delimiter, len(result), len(tt.expected), result, tt.expected)
634+
return
635+
}
636+
for i, v := range result {
637+
if v != tt.expected[i] {
638+
t.Errorf("parseCopyLine(%q, %q)[%d] = %q, want %q",
639+
tt.line, tt.delimiter, i, v, tt.expected[i])
640+
}
641+
}
642+
})
643+
}
644+
}

0 commit comments

Comments
 (0)