chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
// 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"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/dolthub/dolt/go/libraries/doltcore/table"
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
|
||||
"github.com/dolthub/doltgresql/server/types"
|
||||
)
|
||||
|
||||
// CsvDataLoader is an implementation of DataLoader that reads data from chunks of CSV files and inserts them into a table.
|
||||
type CsvDataLoader struct {
|
||||
results LoadDataResults
|
||||
partialRecord string
|
||||
nextDataChunk *bufio.Reader
|
||||
colTypes []*types.DoltgresType
|
||||
sch sql.Schema
|
||||
removeHeader bool
|
||||
delimiter string
|
||||
}
|
||||
|
||||
func (cdl *CsvDataLoader) SetNextDataChunk(ctx *sql.Context, data *bufio.Reader) error {
|
||||
cdl.nextDataChunk = data
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ DataLoader = (*CsvDataLoader)(nil)
|
||||
|
||||
const defaultCsvDelimiter = ","
|
||||
|
||||
// NewCsvDataLoader creates a new DataLoader instance that will produce rows for the schema provided.
|
||||
// |header| is true, the first line of the data will be treated as a header and ignored. If |delimiter| is not the empty
|
||||
// string, it will be used as the delimiter separating value.
|
||||
func NewCsvDataLoader(colNames []string, sch sql.Schema, delimiter string, header bool) (*CsvDataLoader, error) {
|
||||
colTypes, reducedSch, err := getColumnTypes(colNames, sch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if delimiter == "" {
|
||||
delimiter = defaultCsvDelimiter
|
||||
}
|
||||
|
||||
return &CsvDataLoader{
|
||||
colTypes: colTypes,
|
||||
sch: reducedSch,
|
||||
removeHeader: header,
|
||||
delimiter: delimiter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// nextRow attempts to read the next row from the data and return it, and returns true if a row was read
|
||||
func (cdl *CsvDataLoader) nextRow(ctx *sql.Context, reader *csvReader) (sql.Row, bool, error) {
|
||||
if cdl.removeHeader {
|
||||
_, err := reader.readLine()
|
||||
cdl.removeHeader = false
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
record, err := reader.ReadSqlRow()
|
||||
if err != nil {
|
||||
if ple, ok := err.(*partialLineError); ok {
|
||||
cdl.partialRecord = ple.partialLine
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// csvReader will return a BadRow error if it encounters an input line without the
|
||||
// correct number of columns. If we see the end of data marker, then break out of the
|
||||
// loop and return from this function without returning an error.
|
||||
if _, ok := err.(*table.BadRow); ok {
|
||||
if len(record) == 1 && record[0] == "\\." {
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err != io.EOF {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
recordValues := make([]string, 0, len(record))
|
||||
for _, v := range record {
|
||||
recordValues = append(recordValues, fmt.Sprintf("%v", v))
|
||||
}
|
||||
cdl.partialRecord = strings.Join(recordValues, ",")
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// If we see the end of data marker, then break out of the loop. Normally this will happen in the code
|
||||
// above when we receive a BadRow error, since there won't be enough values, but if a table only has
|
||||
// one column, we won't get a BadRow error, and we'll handle the end of data marker here.
|
||||
if len(record) == 1 && record[0] == "\\." {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
if len(record) > len(cdl.colTypes) {
|
||||
return nil, false, errors.Errorf("extra data after last expected column")
|
||||
} else if len(record) < len(cdl.colTypes) {
|
||||
return nil, false, errors.Errorf(`missing data for column "%s"`, cdl.sch[len(record)].Name)
|
||||
}
|
||||
|
||||
// Cast the values using I/O input
|
||||
row := make(sql.Row, len(cdl.colTypes))
|
||||
for i := range cdl.colTypes {
|
||||
if record[i] == nil {
|
||||
row[i] = nil
|
||||
} else {
|
||||
row[i], err = cdl.colTypes[i].IoInput(ctx, fmt.Sprintf("%v", record[i]))
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return row, true, nil
|
||||
}
|
||||
|
||||
// Finish implements the DataLoader interface
|
||||
func (cdl *CsvDataLoader) Finish(ctx *sql.Context) (*LoadDataResults, error) {
|
||||
// If there is partial data from the last chunk that hasn't been inserted, return an error.
|
||||
if cdl.partialRecord != "" {
|
||||
return nil, errors.Errorf("partial record (%s) found at end of data load", cdl.partialRecord)
|
||||
}
|
||||
|
||||
return &cdl.results, nil
|
||||
}
|
||||
|
||||
func (cdl *CsvDataLoader) Resolved() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (cdl *CsvDataLoader) String() string {
|
||||
return "CsvDataLoader"
|
||||
}
|
||||
|
||||
func (cdl *CsvDataLoader) Schema(ctx *sql.Context) sql.Schema {
|
||||
return cdl.sch
|
||||
}
|
||||
|
||||
func (cdl *CsvDataLoader) Children() []sql.Node {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cdl *CsvDataLoader) WithChildren(ctx *sql.Context, children ...sql.Node) (sql.Node, error) {
|
||||
if len(children) != 0 {
|
||||
return nil, sql.ErrInvalidChildrenNumber.New(cdl, len(children), 0)
|
||||
}
|
||||
return cdl, nil
|
||||
}
|
||||
|
||||
func (cdl *CsvDataLoader) IsReadOnly() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type csvRowIter struct {
|
||||
cdl *CsvDataLoader
|
||||
reader *csvReader
|
||||
}
|
||||
|
||||
func (c csvRowIter) Next(ctx *sql.Context) (sql.Row, error) {
|
||||
row, hasNext, err := c.cdl.nextRow(ctx, c.reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: this isn't the best way to handle the count of rows, something like a RowUpdateAccumulator would be better
|
||||
if hasNext {
|
||||
c.cdl.results.RowsLoaded++
|
||||
} else {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
return row, nil
|
||||
}
|
||||
|
||||
func (c csvRowIter) Close(context *sql.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ sql.RowIter = (*csvRowIter)(nil)
|
||||
|
||||
func (cdl *CsvDataLoader) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, error) {
|
||||
combinedReader := NewStringPrefixReader(cdl.partialRecord, cdl.nextDataChunk)
|
||||
cdl.partialRecord = ""
|
||||
|
||||
csvReader, err := newCsvReaderWithDelimiter(combinedReader, cdl.delimiter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &csvRowIter{cdl: cdl, reader: csvReader}, nil
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
// 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:
|
||||
// `"<delimiter>` 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// 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"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
|
||||
"github.com/dolthub/doltgresql/server/types"
|
||||
)
|
||||
|
||||
// DataLoader allows callers to insert rows from multiple chunks into a table. Rows encoded in each chunk will not
|
||||
// necessarily end cleanly on a chunk boundary, so DataLoader implementations must handle recognizing partial, or
|
||||
// incomplete records, and saving that partial record until the next call to LoadChunk, so that it may be prefixed
|
||||
// with the incomplete record.
|
||||
type DataLoader interface {
|
||||
sql.ExecSourceRel
|
||||
|
||||
// SetNextDataChunk sets the next data chunk to be processed by the DataLoader. Data records
|
||||
// are not guaranteed to start and end cleanly on chunk boundaries, so implementations must recognize incomplete
|
||||
// records and save them to prepend on the next processed chunk.
|
||||
SetNextDataChunk(ctx *sql.Context, data *bufio.Reader) error
|
||||
|
||||
// Finish finalizes the current load operation and cleans up any resources used. Implementations should check that
|
||||
// the last call to LoadChunk did not end with an incomplete record and return an error to the caller if so. The
|
||||
// returned LoadDataResults describe the load operation, including how many rows were inserted.
|
||||
Finish(ctx *sql.Context) (*LoadDataResults, error)
|
||||
}
|
||||
|
||||
// LoadDataResults contains the results of a load data operation, including the number of rows loaded.
|
||||
type LoadDataResults struct {
|
||||
// RowsLoaded contains the total number of rows inserted during a load data operation.
|
||||
RowsLoaded int32
|
||||
}
|
||||
|
||||
// getColumnTypes returns the types of the columns in the schema that match the provided column names, in the order
|
||||
// they are provided. If a subset of column names are provided, the returned types will only contain those columns.
|
||||
// If the column names are not found in the schema, an error is returned.
|
||||
func getColumnTypes(colNames []string, sch sql.Schema) ([]*types.DoltgresType, sql.Schema, error) {
|
||||
colTypes := make([]*types.DoltgresType, len(colNames))
|
||||
reducedSch := make(sql.Schema, len(colNames))
|
||||
for i, colName := range colNames {
|
||||
colIdx := sch.IndexOfColName(colName)
|
||||
if colIdx < 0 {
|
||||
// should be impossible
|
||||
return nil, nil, errors.Errorf("column %s not found in schema", colName)
|
||||
}
|
||||
col := sch[colIdx]
|
||||
var ok bool
|
||||
colTypes[i], ok = col.Type.(*types.DoltgresType)
|
||||
if !ok {
|
||||
return nil, nil, errors.Errorf("unsupported column type: name: %s, type: %T", col.Name, col.Type)
|
||||
}
|
||||
|
||||
reducedSch[i] = col
|
||||
}
|
||||
|
||||
return colTypes, reducedSch, nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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 "io"
|
||||
|
||||
// stringPrefixReader is an io.ReadCloser that reads from a string prefix before reading from
|
||||
// another io.Reader. This is used for reassembling partial records across multi-message
|
||||
// exchanges, since the end of a wire message does not typically line up with the end of a record.
|
||||
type stringPrefixReader struct {
|
||||
prefix string
|
||||
prefixPosition uint
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
var _ io.ReadCloser = (*stringPrefixReader)(nil)
|
||||
|
||||
// NewStringPrefixReader creates a new stringPrefixReader that first returns the data in |prefix| and
|
||||
// then returns data from |reader|.
|
||||
func NewStringPrefixReader(prefix string, reader io.Reader) *stringPrefixReader {
|
||||
return &stringPrefixReader{
|
||||
prefix: prefix,
|
||||
reader: reader,
|
||||
}
|
||||
}
|
||||
|
||||
// Read implements the io.Reader interface
|
||||
func (spr *stringPrefixReader) Read(p []byte) (n int, err error) {
|
||||
if spr.prefixPosition < uint(len(spr.prefix)) {
|
||||
n = copy(p, spr.prefix[spr.prefixPosition:])
|
||||
spr.prefixPosition += uint(n)
|
||||
if n == len(p) {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
|
||||
read, err := spr.reader.Read(p[n:])
|
||||
return n + read, err
|
||||
}
|
||||
|
||||
// Close implements the io.Closer interface
|
||||
func (spr *stringPrefixReader) Close() error {
|
||||
if closer, ok := spr.reader.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// 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"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/dolthub/go-mysql-server/sql"
|
||||
|
||||
"github.com/dolthub/doltgresql/server/types"
|
||||
)
|
||||
|
||||
const defaultTextDelimiter = "\t"
|
||||
const defaultNullChar = "\\N"
|
||||
|
||||
// TabularDataLoader tracks the state of a load data operation from a tabular data source.
|
||||
type TabularDataLoader struct {
|
||||
results LoadDataResults
|
||||
partialLine strings.Builder
|
||||
nextDataChunk *bufio.Reader
|
||||
colTypes []*types.DoltgresType
|
||||
sch sql.Schema
|
||||
delimiterChar string
|
||||
nullChar string
|
||||
removeHeader bool
|
||||
}
|
||||
|
||||
var _ DataLoader = (*TabularDataLoader)(nil)
|
||||
|
||||
// NewTabularDataLoader creates a new TabularDataLoader to insert into the specified |table| using the specified
|
||||
// |delimiterChar| and |nullChar|. If |header| is true, the first line of the data will be treated as a header and
|
||||
// ignored.
|
||||
func NewTabularDataLoader(colNames []string, tableSch sql.Schema, delimiterChar, nullChar string, header bool) (*TabularDataLoader, error) {
|
||||
colTypes, reducedSch, err := getColumnTypes(colNames, tableSch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if delimiterChar == "" {
|
||||
delimiterChar = defaultTextDelimiter
|
||||
}
|
||||
|
||||
if nullChar == "" {
|
||||
nullChar = defaultNullChar
|
||||
}
|
||||
|
||||
return &TabularDataLoader{
|
||||
colTypes: colTypes,
|
||||
sch: reducedSch,
|
||||
delimiterChar: delimiterChar,
|
||||
nullChar: nullChar,
|
||||
removeHeader: header,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// nextRow returns the next SQL row from the reader provided, using any previously saved partial line. Returns true if
|
||||
// there was another row.
|
||||
func (tdl *TabularDataLoader) nextRow(ctx *sql.Context, data *bufio.Reader) (sql.Row, bool, error) {
|
||||
if tdl.removeHeader {
|
||||
_, err := data.ReadString('\n')
|
||||
tdl.removeHeader = false
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
// Read the next line from the file
|
||||
line, err := data.ReadString('\n')
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// bufio.Reader.ReadString will return an error AND a line
|
||||
// if the final contents of the data does NOT end in the
|
||||
// delimiter. In this case, that means that we need to save
|
||||
// the partial line and use it in the next chunk.
|
||||
tdl.partialLine.WriteString(line)
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// If we've not reached EOF, then there will be a newline appended to the end that we must remove.
|
||||
line = strings.TrimSuffix(line, "\n")
|
||||
// Data with windows line endings will also have a carriage return character that we need to remove.
|
||||
line = strings.TrimSuffix(line, "\r")
|
||||
|
||||
if tdl.partialLine.Len() > 0 {
|
||||
tdl.partialLine.WriteString(line)
|
||||
line = tdl.partialLine.String()
|
||||
tdl.partialLine.Reset()
|
||||
}
|
||||
|
||||
// If we see the end of data marker, return early
|
||||
if line == `\.` {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// Skip over empty lines
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Split the values by the delimiter, ensuring the correct number of values have been read
|
||||
values := strings.Split(line, tdl.delimiterChar)
|
||||
if len(values) > len(tdl.colTypes) {
|
||||
return nil, false, errors.Errorf("extra data after last expected column")
|
||||
} else if len(values) < len(tdl.colTypes) {
|
||||
return nil, false, errors.Errorf(`missing data for column "%s"`, tdl.sch[len(values)].Name)
|
||||
}
|
||||
|
||||
// Cast the values using I/O input
|
||||
row := make(sql.Row, len(tdl.colTypes))
|
||||
for i := range tdl.colTypes {
|
||||
if values[i] == tdl.nullChar {
|
||||
row[i] = nil
|
||||
} else {
|
||||
// We must un-escape strings here since we're receiving everything verbatim
|
||||
values[i] = strings.ReplaceAll(values[i], `\\`, `\`)
|
||||
row[i], err = tdl.colTypes[i].IoInput(ctx, values[i])
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return row, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (tdl *TabularDataLoader) SetNextDataChunk(ctx *sql.Context, data *bufio.Reader) error {
|
||||
tdl.nextDataChunk = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// Finish completes the current load data operation and finalizes the data that has been inserted.
|
||||
func (tdl *TabularDataLoader) Finish(ctx *sql.Context) (*LoadDataResults, error) {
|
||||
// If there is partial data from the last chunk that hasn't been inserted, return an error.
|
||||
if tdl.partialLine.Len() > 0 {
|
||||
return nil, errors.Errorf("partial line found at end of data load")
|
||||
}
|
||||
|
||||
return &tdl.results, nil
|
||||
}
|
||||
|
||||
func (tdl *TabularDataLoader) Resolved() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (tdl *TabularDataLoader) String() string {
|
||||
return "TabularDataLoader"
|
||||
}
|
||||
|
||||
func (tdl *TabularDataLoader) Schema(ctx *sql.Context) sql.Schema {
|
||||
return tdl.sch
|
||||
}
|
||||
|
||||
func (tdl *TabularDataLoader) Children() []sql.Node {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tdl *TabularDataLoader) WithChildren(ctx *sql.Context, children ...sql.Node) (sql.Node, error) {
|
||||
if len(children) != 0 {
|
||||
return nil, sql.ErrInvalidChildrenNumber.New(tdl, len(children), 0)
|
||||
}
|
||||
return tdl, nil
|
||||
}
|
||||
|
||||
func (tdl *TabularDataLoader) IsReadOnly() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type tabularRowIter struct {
|
||||
tdl *TabularDataLoader
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
func (t tabularRowIter) Next(ctx *sql.Context) (sql.Row, error) {
|
||||
row, hasNext, err := t.tdl.nextRow(ctx, t.reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: this isn't the best way to handle the count of rows, something like a RowUpdateAccumulator would be better
|
||||
if hasNext {
|
||||
t.tdl.results.RowsLoaded++
|
||||
} else {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
return row, nil
|
||||
}
|
||||
|
||||
func (t tabularRowIter) Close(context *sql.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tdl *TabularDataLoader) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, error) {
|
||||
return &tabularRowIter{tdl: tdl, reader: tdl.nextDataChunk}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user