// Copyright 2024 Dolthub, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package dataloader import ( "bufio" "bytes" "context" "encoding/csv" "io" "unicode/utf8" "github.com/dolthub/dolt/go/libraries/doltcore/table" "github.com/dolthub/go-mysql-server/sql" textunicode "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" ) // csvReadBufSize is the size of the buffer used when reading the csv file. var csvReadBufSize = 256 * 1024 // partialLineError is an error type that is returned when an incomplete record is read from a CSV // file. This can occur when a CSV document is split across multiple messages and the message // boundaries don't line up with CSV record boundaries. Callers should use this error to record the // partial line, so that it can be prepended to the next message. type partialLineError struct { partialLine string } var _ error = partialLineError{} func (ple partialLineError) Error() string { return "incomplete record found at end of CSV data: " + ple.partialLine } // csvReader implements TableReader. It reads csv files and returns rows. // // This implementation is adapted from the CSVReader in dolt, which is a fork // of the standard Golang CSV reader. The main differences with the Golang std // library implementation are that this parser has been adapted to differentiate // between quoted and unquoted empty strings (for distinguishing between the empty // string and NULL), and to use multi-rune delimiters. This adaptation removes the // comment feature and the lazyQuotes option. // // Additionally, this fork of the dolt implementation removes some dolt specific // features and adds support for a few Postgres requirements, such as allowing for // the full CSV document to be arbitrarily split into multiple messages and for // incomplete/partial lines to be communicated to the caller. type csvReader struct { closer io.Closer bRd *bufio.Reader isDone bool delim []byte numLine int fieldsPerRecord int } // NewCsvReader creates a csvReader from a given ReadCloser. // // The interpretation of the bytes of the supplied reader is a little murky. If // there is a UTF8, UTF16LE or UTF16BE BOM as the first bytes read, then the // BOM is stripped and the remaining contents of the reader are treated as that // encoding. If we are not in any of those marked encodings, then some of the // bytes go uninterpreted until we get to the SQL layer. It is currently the // case that newlines must be encoded as a '0xa' byte. func NewCsvReader(r io.ReadCloser) (*csvReader, error) { return newCsvReaderWithDelimiter(r, ",") } // newCsvReaderWithDelimiter creates a csvReader from a given ReadCloser, |r|, using // the |delimiter| as the field delimiter in the parsed data. func newCsvReaderWithDelimiter(r io.ReadCloser, delimiter string) (*csvReader, error) { textReader := transform.NewReader(r, textunicode.BOMOverride(transform.Nop)) br := bufio.NewReaderSize(textReader, csvReadBufSize) return &csvReader{ closer: r, bRd: br, isDone: false, delim: []byte(delimiter), }, nil } func (csvr *csvReader) ReadSqlRow() (sql.Row, error) { if csvr.isDone { return nil, io.EOF } rowVals, err := csvr.csvReadRecords(nil) if err == io.EOF { csvr.isDone = true return nil, io.EOF } sqlRows := rowValsToSQLRows(rowVals) if err != nil { if _, ok := err.(*partialLineError); ok { return nil, err } return sqlRows, table.NewBadRow(nil, err.Error()) } return sqlRows, nil } func rowValsToSQLRows(rowVals []*string) sql.Row { var sqlRow sql.Row for _, rowVal := range rowVals { if rowVal == nil { sqlRow = append(sqlRow, nil) } else { sqlRow = append(sqlRow, *rowVal) } } return sqlRow } // Close should release resources being held func (csvr *csvReader) Close(ctx context.Context) error { if csvr.closer != nil { err := csvr.closer.Close() csvr.closer = nil return err } return nil } // Functions below this line are borrowed or adapted from encoding/csv/reader.go // lengthNL returns 1 if the last byte in b is a newline, 0 otherwise. func lengthNL(b []byte) int { if len(b) > 0 && b[len(b)-1] == '\n' { return 1 } return 0 } // readLine reads the next line (with the trailing endline). // If EOF is hit without a trailing endline, it will be omitted. // If some bytes were read, then the error is never io.EOF. // The result is only valid until the next call to readLine. func (csvr *csvReader) readLine() ([]byte, error) { var rawBuffer []byte line, err := csvr.bRd.ReadSlice('\n') if err == bufio.ErrBufferFull { rawBuffer = append(rawBuffer[:0], line...) for err == bufio.ErrBufferFull { line, err = csvr.bRd.ReadSlice('\n') rawBuffer = append(rawBuffer, line...) } line = rawBuffer } if len(line) > 0 && err == io.EOF { err = nil // For backwards compatibility, drop trailing \r before EOF. if line[len(line)-1] == '\r' { line = line[:len(line)-1] } } csvr.numLine++ // Normalize \r\n to \n on all input lines. if n := len(line); n >= 2 && line[n-2] == '\r' && line[n-1] == '\n' { line[n-2] = '\n' line = line[:n-1] } // If the line does NOT end with a newline, then we must have read a partial record if len(line) > 0 && lengthNL(line) == 0 { return nil, &partialLineError{string(line)} } return line, err } type recordState struct { line []byte // recordBuffer holds the unescaped fields, one after another. // The fields can be accessed by using the indexes in fieldIndexes. // E.g., For the row `a,"b","c""d",e`, recordBuffer will contain `abc"de` // and fieldIndexes will contain the indexes [1, 2, 5, 6]. recordBuffer []byte fieldIndexes []int rawData []byte } func (csvr *csvReader) csvReadRecords(dst []*string) ([]*string, error) { recordStartline := csvr.numLine // Starting line for record var rs recordState var err error for err == nil { rs = recordState{} rs.line, err = csvr.readLine() rs.rawData = append(rs.rawData, rs.line...) if err == nil && len(rs.line) == lengthNL(rs.line) { continue // Skip empty lines } break } if err != nil { return nil, err } // nullString indicates whether to interpret an empty string as a NULL // only empty strings escaped with double quotes will be non-null nullString := make(map[int]bool) fieldIdx := 0 kontinue := true for kontinue { // Parse each field in the record. keep := true if len(rs.line) == 0 || rs.line[0] != '"' { kontinue, keep, err = csvr.parseField(&rs) if !keep { nullString[fieldIdx] = true } } else { kontinue, err = csvr.parseQuotedField(&rs) if err != nil { return nil, err } } fieldIdx++ } // Create a single string and create slices out of it. // This pins the memory of the fields together, but allocates once. str := string(rs.recordBuffer) // Convert to string once to batch allocations dst = dst[:0] if cap(dst) < len(rs.fieldIndexes) { dst = make([]*string, len(rs.fieldIndexes)) } dst = dst[:len(rs.fieldIndexes)] var preIdx int for i, idx := range rs.fieldIndexes { _, ok := nullString[i] if ok { dst[i] = nil } else { s := str[preIdx:idx] dst[i] = &s } preIdx = idx } // Check or update the expected fields per record. if csvr.fieldsPerRecord > 0 { if len(dst) != csvr.fieldsPerRecord && err == nil { err = &csv.ParseError{StartLine: recordStartline, Line: csvr.numLine, Err: csv.ErrFieldCount} } } else if csvr.fieldsPerRecord == 0 { csvr.fieldsPerRecord = len(dst) } return dst, err } func (csvr *csvReader) parseField(rs *recordState) (kontinue bool, keep bool, err error) { i := bytes.Index(rs.line, csvr.delim) field := rs.line if i >= 0 { field = field[:i] } else { field = field[:len(field)-lengthNL(field)] } rs.recordBuffer = append(rs.recordBuffer, field...) rs.fieldIndexes = append(rs.fieldIndexes, len(rs.recordBuffer)) keep = len(field) != 0 // discard unquoted empty strings if i >= 0 { dl := len(csvr.delim) rs.line = rs.line[i+dl:] return true, keep, err } return false, keep, err } func (csvr *csvReader) parseQuotedField(rs *recordState) (kontinue bool, err error) { const quoteLen = len(`"`) dl := len(csvr.delim) recordStartLine := csvr.numLine // full copy needed here because we append rs.line to fullField, and this can result in buffer corruption in // some cases (namely when windows line endings are present) fullField := make([]byte, len(rs.line)) copy(fullField, rs.line) // Quoted string field rs.line = rs.line[quoteLen:] for { i := bytes.IndexByte(rs.line, '"') if i >= 0 { // Hit next quote. rs.recordBuffer = append(rs.recordBuffer, rs.line[:i]...) rs.line = rs.line[i+quoteLen:] atDelimiter := len(rs.line) >= dl && bytes.Equal(rs.line[:dl], csvr.delim) nextRune, _ := utf8.DecodeRune(rs.line) switch { case atDelimiter: // `"` sequence (end of field). rs.line = rs.line[dl:] rs.fieldIndexes = append(rs.fieldIndexes, len(rs.recordBuffer)) return true, err case nextRune == '"': // `""` sequence (append quote). rs.recordBuffer = append(rs.recordBuffer, '"') rs.line = rs.line[quoteLen:] case lengthNL(rs.line) == len(rs.line): // `"\n` sequence (end of line). rs.fieldIndexes = append(rs.fieldIndexes, len(rs.recordBuffer)) return false, err default: // `"*` sequence (invalid non-escaped quote). col := utf8.RuneCount(fullField[:len(fullField)-len(rs.line)-quoteLen]) err = &csv.ParseError{StartLine: recordStartLine, Line: csvr.numLine, Column: col, Err: csv.ErrQuote} return false, err } } else if len(rs.line) > 0 { // Hit end of line (copy all data so far). rs.recordBuffer = append(rs.recordBuffer, rs.line...) if err != nil { return false, err } rs.line, err = csvr.readLine() rs.rawData = append(rs.rawData, rs.line...) if err == io.EOF { err = nil } // If we get a partialLineError, populate the partialLine field with the full record data // since quoted fields can span multiple lines, otherwise we wouldn't capture the initial // lines of this record. if ple, ok := err.(*partialLineError); ok { ple.partialLine = string(rs.rawData) return true, ple } fullField = append(fullField, rs.line...) } else { // Abrupt end of file if err == nil { return false, &partialLineError{string(rs.rawData)} } rs.fieldIndexes = append(rs.fieldIndexes, len(rs.recordBuffer)) return false, err } } }