github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/s3select/csv/record.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package csv
    19  
    20  import (
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"strconv"
    26  	"strings"
    27  
    28  	"github.com/bcicen/jstream"
    29  	csv "github.com/minio/csvparser"
    30  	"github.com/minio/minio/internal/s3select/sql"
    31  )
    32  
    33  // Record - is a CSV record.
    34  type Record struct {
    35  	columnNames  []string
    36  	csvRecord    []string
    37  	nameIndexMap map[string]int64
    38  }
    39  
    40  // Get - gets the value for a column name. CSV fields do not have any
    41  // defined type (other than the default string). So this function
    42  // always returns fields using sql.FromBytes so that the type
    43  // specified/implied by the query can be used, or can be automatically
    44  // converted based on the query.
    45  func (r *Record) Get(name string) (*sql.Value, error) {
    46  	index, found := r.nameIndexMap[name]
    47  	if !found {
    48  		// Check if index.
    49  		if strings.HasPrefix(name, "_") {
    50  			idx, err := strconv.Atoi(strings.TrimPrefix(name, "_"))
    51  			if err != nil {
    52  				return nil, fmt.Errorf("column %v not found", name)
    53  			}
    54  			// The position count starts at 1.
    55  			idx--
    56  			if idx >= len(r.csvRecord) || idx < 0 {
    57  				// If field index > number of columns, return null
    58  				return sql.FromNull(), nil
    59  			}
    60  			return sql.FromBytes([]byte(r.csvRecord[idx])), nil
    61  		}
    62  		// TODO: Return Missing?
    63  		return nil, fmt.Errorf("column %v not found", name)
    64  	}
    65  
    66  	if index >= int64(len(r.csvRecord)) {
    67  		// No value found for column 'name', hence return null
    68  		// value
    69  		return sql.FromNull(), nil
    70  	}
    71  
    72  	return sql.FromBytes([]byte(r.csvRecord[index])), nil
    73  }
    74  
    75  // Set - sets the value for a column name.
    76  func (r *Record) Set(name string, value *sql.Value) (sql.Record, error) {
    77  	r.columnNames = append(r.columnNames, name)
    78  	r.csvRecord = append(r.csvRecord, value.CSVString())
    79  	return r, nil
    80  }
    81  
    82  // Reset data in record.
    83  func (r *Record) Reset() {
    84  	if len(r.columnNames) > 0 {
    85  		r.columnNames = r.columnNames[:0]
    86  	}
    87  	if len(r.csvRecord) > 0 {
    88  		r.csvRecord = r.csvRecord[:0]
    89  	}
    90  	for k := range r.nameIndexMap {
    91  		delete(r.nameIndexMap, k)
    92  	}
    93  }
    94  
    95  // Clone the record.
    96  func (r *Record) Clone(dst sql.Record) sql.Record {
    97  	other, ok := dst.(*Record)
    98  	if !ok {
    99  		other = &Record{}
   100  	}
   101  	if len(other.columnNames) > 0 {
   102  		other.columnNames = other.columnNames[:0]
   103  	}
   104  	if len(other.csvRecord) > 0 {
   105  		other.csvRecord = other.csvRecord[:0]
   106  	}
   107  	other.columnNames = append(other.columnNames, r.columnNames...)
   108  	other.csvRecord = append(other.csvRecord, r.csvRecord...)
   109  	return other
   110  }
   111  
   112  // WriteCSV - encodes to CSV data.
   113  func (r *Record) WriteCSV(writer io.Writer, opts sql.WriteCSVOpts) error {
   114  	w := csv.NewWriter(writer)
   115  	w.Comma = opts.FieldDelimiter
   116  	w.AlwaysQuote = opts.AlwaysQuote
   117  	w.Quote = opts.Quote
   118  	w.QuoteEscape = opts.QuoteEscape
   119  	if err := w.Write(r.csvRecord); err != nil {
   120  		return err
   121  	}
   122  	w.Flush()
   123  	return w.Error()
   124  }
   125  
   126  // WriteJSON - encodes to JSON data.
   127  func (r *Record) WriteJSON(writer io.Writer) error {
   128  	var kvs jstream.KVS = make([]jstream.KV, 0, len(r.columnNames))
   129  	for i, cn := range r.columnNames {
   130  		if i < len(r.csvRecord) {
   131  			kvs = append(kvs, jstream.KV{Key: cn, Value: r.csvRecord[i]})
   132  		}
   133  	}
   134  	return json.NewEncoder(writer).Encode(kvs)
   135  }
   136  
   137  // Raw - returns the underlying data with format info.
   138  func (r *Record) Raw() (sql.SelectObjectFormat, interface{}) {
   139  	return sql.SelectFmtCSV, r
   140  }
   141  
   142  // Replace - is not supported for CSV
   143  func (r *Record) Replace(_ interface{}) error {
   144  	return errors.New("Replace is not supported for CSV")
   145  }
   146  
   147  // NewRecord - creates new CSV record.
   148  func NewRecord() *Record {
   149  	return &Record{}
   150  }