@@ -771,6 +771,9 @@ func (c *clientConn) handleCopyOut(query, upperQuery string) error {
771771
772772// handleCopyIn handles COPY ... FROM STDIN
773773func (c * clientConn ) handleCopyIn (query , upperQuery string ) error {
774+ copyStartTime := time .Now ()
775+ log .Printf ("[%s] COPY FROM STDIN: starting" , c .username )
776+
774777 matches := copyFromStdinRegex .FindStringSubmatch (query )
775778 if len (matches ) < 2 {
776779 c .sendError ("ERROR" , "42601" , "Invalid COPY FROM STDIN syntax" )
@@ -785,6 +788,7 @@ func (c *clientConn) handleCopyIn(query, upperQuery string) error {
785788 if len (matches ) > 2 && matches [2 ] != "" {
786789 columnList = fmt .Sprintf ("(%s)" , matches [2 ])
787790 }
791+ log .Printf ("[%s] COPY FROM STDIN: table=%s columns=%s" , c .username , tableName , columnList )
788792
789793 // Parse options
790794 delimiter := "\t "
@@ -819,78 +823,102 @@ func (c *clientConn) handleCopyIn(query, upperQuery string) error {
819823 return err
820824 }
821825 c .writer .Flush ()
826+ log .Printf ("[%s] COPY FROM STDIN: sent CopyInResponse, waiting for data..." , c .username )
822827
823828 // Read COPY data from client
824829 var allData bytes.Buffer
825830 rowCount := 0
826- headerSkipped := false
831+ copyDataMessages := 0
832+ dataReceiveStart := time .Now ()
827833
828834 for {
829835 msgType , body , err := readMessage (c .reader )
830836 if err != nil {
837+ log .Printf ("[%s] COPY FROM STDIN: error reading message: %v" , c .username , err )
831838 return err
832839 }
833840
834841 switch msgType {
835842 case msgCopyData :
836843 allData .Write (body )
844+ copyDataMessages ++
845+ if copyDataMessages % 10000 == 0 {
846+ log .Printf ("[%s] COPY FROM STDIN: received %d CopyData messages, %d bytes total" ,
847+ c .username , copyDataMessages , allData .Len ())
848+ }
837849
838850 case msgCopyDone :
839- // Process all data using proper CSV reader
840- // This correctly handles multi-line quoted fields (e.g., JSON with embedded newlines)
841- csvReader := csv .NewReader (& allData )
842- csvReader .Comma = rune (delimiter [0 ])
843- csvReader .LazyQuotes = true
844- csvReader .FieldsPerRecord = - 1 // Allow variable number of fields
845-
846- for {
847- values , err := csvReader .Read ()
848- if err == io .EOF {
849- break
850- }
851- if err != nil {
852- c .sendError ("ERROR" , "22P02" , fmt .Sprintf ("invalid CSV input: %v" , err ))
853- c .setTxError ()
854- writeReadyForQuery (c .writer , c .txStatus )
855- c .writer .Flush ()
856- return nil
857- }
851+ dataReceiveElapsed := time .Since (dataReceiveStart )
852+ log .Printf ("[%s] COPY FROM STDIN: CopyDone received - %d messages, %d bytes in %v" ,
853+ c .username , copyDataMessages , allData .Len (), dataReceiveElapsed )
854+
855+ // Write data to a temp file and use DuckDB's native COPY FROM for performance.
856+ // This approach leverages DuckDB's highly optimized CSV parser which handles
857+ // type conversions automatically and can load millions of rows in seconds.
858+ // The temp file is created, used, and deleted within this single request.
859+ tmpFile , err := os .CreateTemp ("" , "duckgres-copy-*.csv" )
860+ if err != nil {
861+ log .Printf ("[%s] COPY FROM STDIN: failed to create temp file: %v" , c .username , err )
862+ c .sendError ("ERROR" , "58000" , fmt .Sprintf ("failed to create temp file: %v" , err ))
863+ c .setTxError ()
864+ writeReadyForQuery (c .writer , c .txStatus )
865+ c .writer .Flush ()
866+ return nil
867+ }
868+ tmpPath := tmpFile .Name ()
869+ defer os .Remove (tmpPath )
870+
871+ // Write data to temp file
872+ writeStart := time .Now ()
873+ bytesWritten , err := tmpFile .Write (allData .Bytes ())
874+ tmpFile .Close ()
875+ if err != nil {
876+ log .Printf ("[%s] COPY FROM STDIN: failed to write temp file: %v" , c .username , err )
877+ c .sendError ("ERROR" , "58000" , fmt .Sprintf ("failed to write temp file: %v" , err ))
878+ c .setTxError ()
879+ writeReadyForQuery (c .writer , c .txStatus )
880+ c .writer .Flush ()
881+ return nil
882+ }
883+ log .Printf ("[%s] COPY FROM STDIN: wrote %d bytes to temp file in %v" ,
884+ c .username , bytesWritten , time .Since (writeStart ))
885+
886+ // Build DuckDB COPY FROM statement
887+ // DuckDB syntax: COPY table FROM 'file' (FORMAT CSV, HEADER, NULL 'value', DELIMITER ',')
888+ copyOptions := []string {"FORMAT CSV" }
889+ if hasHeader {
890+ copyOptions = append (copyOptions , "HEADER" )
891+ }
892+ if nullString != "\\ N" {
893+ copyOptions = append (copyOptions , fmt .Sprintf ("NULL '%s'" , nullString ))
894+ }
895+ if delimiter != "," {
896+ copyOptions = append (copyOptions , fmt .Sprintf ("DELIMITER '%s'" , delimiter ))
897+ }
858898
859- // Skip empty rows
860- if len (values ) == 0 || (len (values ) == 1 && values [0 ] == "" ) {
861- continue
862- }
899+ copySQL := fmt .Sprintf ("COPY %s %s FROM '%s' (%s)" ,
900+ tableName , columnList , tmpPath , strings .Join (copyOptions , ", " ))
863901
864- // Skip header if needed
865- if hasHeader && ! headerSkipped {
866- headerSkipped = true
867- continue
868- }
902+ log .Printf ("[%s] COPY FROM STDIN: executing native DuckDB COPY: %s" , c .username , copySQL )
903+ loadStart := time .Now ()
869904
870- // Build INSERT statement
871- placeholders := make ([]string , len (values ))
872- args := make ([]interface {}, len (values ))
873- for i , v := range values {
874- placeholders [i ] = "?"
875- if v == nullString || v == "\\ N" || v == "" {
876- args [i ] = nil
877- } else {
878- args [i ] = v
879- }
880- }
905+ result , err := c .db .Exec (copySQL )
906+ if err != nil {
907+ log .Printf ("[%s] COPY FROM STDIN: DuckDB COPY failed: %v" , c .username , err )
908+ c .sendError ("ERROR" , "22P02" , fmt .Sprintf ("COPY failed: %v" , err ))
909+ c .setTxError ()
910+ writeReadyForQuery (c .writer , c .txStatus )
911+ c .writer .Flush ()
912+ return nil
913+ }
881914
882- insertSQL := fmt . Sprintf ( "INSERT INTO %s %s VALUES (%s)" ,
883- tableName , columnList , strings . Join ( placeholders , ", " ) )
915+ rowCount64 , _ := result . RowsAffected ()
916+ rowCount = int ( rowCount64 )
884917
885- if _ , err := c .db .Exec (insertSQL , args ... ); err != nil {
886- c .sendError ("ERROR" , "22P02" , fmt .Sprintf ("invalid input: %v" , err ))
887- c .setTxError ()
888- writeReadyForQuery (c .writer , c .txStatus )
889- c .writer .Flush ()
890- return nil
891- }
892- rowCount ++
893- }
918+ totalElapsed := time .Since (copyStartTime )
919+ loadElapsed := time .Since (loadStart )
920+ log .Printf ("[%s] COPY FROM STDIN: completed - %d rows in %v (DuckDB load: %v)" ,
921+ c .username , rowCount , totalElapsed , loadElapsed )
894922
895923 writeCommandComplete (c .writer , fmt .Sprintf ("COPY %d" , rowCount ))
896924 writeReadyForQuery (c .writer , c .txStatus )
0 commit comments