chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:32:25 +08:00
commit e014feafe1
2285 changed files with 1131979 additions and 0 deletions
+212
View File
@@ -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
}
+364
View File
@@ -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
}
}
}
+73
View File
@@ -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
}
+59
View File
@@ -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
}
+215
View File
@@ -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
}