storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/s3select/select.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2019-2021 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package s3select
    18  
    19  import (
    20  	"bufio"
    21  	"bytes"
    22  	"compress/bzip2"
    23  	"encoding/xml"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"io/ioutil"
    28  	"net/http"
    29  	"os"
    30  	"strings"
    31  	"sync"
    32  
    33  	"github.com/minio/simdjson-go"
    34  
    35  	"storj.io/minio/pkg/s3select/csv"
    36  	"storj.io/minio/pkg/s3select/json"
    37  	"storj.io/minio/pkg/s3select/parquet"
    38  	"storj.io/minio/pkg/s3select/simdj"
    39  	"storj.io/minio/pkg/s3select/sql"
    40  )
    41  
    42  type recordReader interface {
    43  	// Read a record.
    44  	// dst is optional but will be used if valid.
    45  	Read(dst sql.Record) (sql.Record, error)
    46  	Close() error
    47  }
    48  
    49  const (
    50  	csvFormat     = "csv"
    51  	jsonFormat    = "json"
    52  	parquetFormat = "parquet"
    53  )
    54  
    55  // CompressionType - represents value inside <CompressionType/> in request XML.
    56  type CompressionType string
    57  
    58  const (
    59  	noneType  CompressionType = "none"
    60  	gzipType  CompressionType = "gzip"
    61  	bzip2Type CompressionType = "bzip2"
    62  )
    63  
    64  const (
    65  	maxRecordSize = 1 << 20 // 1 MiB
    66  )
    67  
    68  var bufPool = sync.Pool{
    69  	New: func() interface{} {
    70  		// make a buffer with a reasonable capacity.
    71  		return bytes.NewBuffer(make([]byte, 0, maxRecordSize))
    72  	},
    73  }
    74  
    75  var bufioWriterPool = sync.Pool{
    76  	New: func() interface{} {
    77  		// ioutil.Discard is just used to create the writer. Actual destination
    78  		// writer is set later by Reset() before using it.
    79  		return bufio.NewWriter(ioutil.Discard)
    80  	},
    81  }
    82  
    83  // UnmarshalXML - decodes XML data.
    84  func (c *CompressionType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
    85  	var s string
    86  	if err := d.DecodeElement(&s, &start); err != nil {
    87  		return errMalformedXML(err)
    88  	}
    89  
    90  	parsedType := CompressionType(strings.ToLower(s))
    91  	if s == "" {
    92  		parsedType = noneType
    93  	}
    94  
    95  	switch parsedType {
    96  	case noneType, gzipType, bzip2Type:
    97  	default:
    98  		return errInvalidCompressionFormat(fmt.Errorf("invalid compression format '%v'", s))
    99  	}
   100  
   101  	*c = parsedType
   102  	return nil
   103  }
   104  
   105  // InputSerialization - represents elements inside <InputSerialization/> in request XML.
   106  type InputSerialization struct {
   107  	CompressionType CompressionType    `xml:"CompressionType"`
   108  	CSVArgs         csv.ReaderArgs     `xml:"CSV"`
   109  	JSONArgs        json.ReaderArgs    `xml:"JSON"`
   110  	ParquetArgs     parquet.ReaderArgs `xml:"Parquet"`
   111  	unmarshaled     bool
   112  	format          string
   113  }
   114  
   115  // IsEmpty - returns whether input serialization is empty or not.
   116  func (input *InputSerialization) IsEmpty() bool {
   117  	return !input.unmarshaled
   118  }
   119  
   120  // UnmarshalXML - decodes XML data.
   121  func (input *InputSerialization) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
   122  	// Make subtype to avoid recursive UnmarshalXML().
   123  	type subInputSerialization InputSerialization
   124  	parsedInput := subInputSerialization{}
   125  	if err := d.DecodeElement(&parsedInput, &start); err != nil {
   126  		return errMalformedXML(err)
   127  	}
   128  
   129  	// If no compression is specified, set to noneType
   130  	if parsedInput.CompressionType == CompressionType("") {
   131  		parsedInput.CompressionType = noneType
   132  	}
   133  
   134  	found := 0
   135  	if !parsedInput.CSVArgs.IsEmpty() {
   136  		parsedInput.format = csvFormat
   137  		found++
   138  	}
   139  	if !parsedInput.JSONArgs.IsEmpty() {
   140  		parsedInput.format = jsonFormat
   141  		found++
   142  	}
   143  	if !parsedInput.ParquetArgs.IsEmpty() {
   144  		if parsedInput.CompressionType != "" && parsedInput.CompressionType != noneType {
   145  			return errInvalidRequestParameter(fmt.Errorf("CompressionType must be NONE for Parquet format"))
   146  		}
   147  
   148  		parsedInput.format = parquetFormat
   149  		found++
   150  	}
   151  
   152  	if found != 1 {
   153  		return errInvalidDataSource(nil)
   154  	}
   155  
   156  	*input = InputSerialization(parsedInput)
   157  	input.unmarshaled = true
   158  	return nil
   159  }
   160  
   161  // OutputSerialization - represents elements inside <OutputSerialization/> in request XML.
   162  type OutputSerialization struct {
   163  	CSVArgs     csv.WriterArgs  `xml:"CSV"`
   164  	JSONArgs    json.WriterArgs `xml:"JSON"`
   165  	unmarshaled bool
   166  	format      string
   167  }
   168  
   169  // IsEmpty - returns whether output serialization is empty or not.
   170  func (output *OutputSerialization) IsEmpty() bool {
   171  	return !output.unmarshaled
   172  }
   173  
   174  // UnmarshalXML - decodes XML data.
   175  func (output *OutputSerialization) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
   176  	// Make subtype to avoid recursive UnmarshalXML().
   177  	type subOutputSerialization OutputSerialization
   178  	parsedOutput := subOutputSerialization{}
   179  	if err := d.DecodeElement(&parsedOutput, &start); err != nil {
   180  		return errMalformedXML(err)
   181  	}
   182  
   183  	found := 0
   184  	if !parsedOutput.CSVArgs.IsEmpty() {
   185  		parsedOutput.format = csvFormat
   186  		found++
   187  	}
   188  	if !parsedOutput.JSONArgs.IsEmpty() {
   189  		parsedOutput.format = jsonFormat
   190  		found++
   191  	}
   192  	if found != 1 {
   193  		return errObjectSerializationConflict(fmt.Errorf("either CSV or JSON should be present in OutputSerialization"))
   194  	}
   195  
   196  	*output = OutputSerialization(parsedOutput)
   197  	output.unmarshaled = true
   198  	return nil
   199  }
   200  
   201  // RequestProgress - represents elements inside <RequestProgress/> in request XML.
   202  type RequestProgress struct {
   203  	Enabled bool `xml:"Enabled"`
   204  }
   205  
   206  // S3Select - filters the contents on a simple structured query language (SQL) statement. It
   207  // represents elements inside <SelectRequest/> in request XML specified in detail at
   208  // https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectSELECTContent.html.
   209  type S3Select struct {
   210  	XMLName        xml.Name            `xml:"SelectRequest"`
   211  	Expression     string              `xml:"Expression"`
   212  	ExpressionType string              `xml:"ExpressionType"`
   213  	Input          InputSerialization  `xml:"InputSerialization"`
   214  	Output         OutputSerialization `xml:"OutputSerialization"`
   215  	Progress       RequestProgress     `xml:"RequestProgress"`
   216  
   217  	statement      *sql.SelectStatement
   218  	progressReader *progressReader
   219  	recordReader   recordReader
   220  	close          func() error
   221  }
   222  
   223  var (
   224  	legacyXMLName = "SelectObjectContentRequest"
   225  )
   226  
   227  // UnmarshalXML - decodes XML data.
   228  func (s3Select *S3Select) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
   229  	// S3 also supports the older SelectObjectContentRequest tag,
   230  	// though it is no longer found in documentation. This is
   231  	// checked and renamed below to allow older clients to also
   232  	// work.
   233  	if start.Name.Local == legacyXMLName {
   234  		start.Name = xml.Name{Space: "", Local: "SelectRequest"}
   235  	}
   236  
   237  	// Make subtype to avoid recursive UnmarshalXML().
   238  	type subS3Select S3Select
   239  	parsedS3Select := subS3Select{}
   240  	if err := d.DecodeElement(&parsedS3Select, &start); err != nil {
   241  		if _, ok := err.(*s3Error); ok {
   242  			return err
   243  		}
   244  
   245  		return errMalformedXML(err)
   246  	}
   247  
   248  	parsedS3Select.ExpressionType = strings.ToLower(parsedS3Select.ExpressionType)
   249  	if parsedS3Select.ExpressionType != "sql" {
   250  		return errInvalidExpressionType(fmt.Errorf("invalid expression type '%v'", parsedS3Select.ExpressionType))
   251  	}
   252  
   253  	if parsedS3Select.Input.IsEmpty() {
   254  		return errMissingRequiredParameter(fmt.Errorf("InputSerialization must be provided"))
   255  	}
   256  
   257  	if parsedS3Select.Output.IsEmpty() {
   258  		return errMissingRequiredParameter(fmt.Errorf("OutputSerialization must be provided"))
   259  	}
   260  
   261  	statement, err := sql.ParseSelectStatement(parsedS3Select.Expression)
   262  	if err != nil {
   263  		return err
   264  	}
   265  
   266  	parsedS3Select.statement = &statement
   267  
   268  	*s3Select = S3Select(parsedS3Select)
   269  	return nil
   270  }
   271  
   272  func (s3Select *S3Select) outputRecord() sql.Record {
   273  	switch s3Select.Output.format {
   274  	case csvFormat:
   275  		return csv.NewRecord()
   276  	case jsonFormat:
   277  		return json.NewRecord(sql.SelectFmtJSON)
   278  	}
   279  
   280  	panic(fmt.Errorf("unknown output format '%v'", s3Select.Output.format))
   281  }
   282  
   283  func (s3Select *S3Select) getProgress() (bytesScanned, bytesProcessed int64) {
   284  	if s3Select.progressReader != nil {
   285  		return s3Select.progressReader.Stats()
   286  	}
   287  
   288  	return -1, -1
   289  }
   290  
   291  // Open - opens S3 object by using callback for SQL selection query.
   292  // Currently CSV, JSON and Apache Parquet formats are supported.
   293  func (s3Select *S3Select) Open(getReader func(offset, length int64) (io.ReadCloser, error)) error {
   294  	switch s3Select.Input.format {
   295  	case csvFormat:
   296  		rc, err := getReader(0, -1)
   297  		if err != nil {
   298  			return err
   299  		}
   300  
   301  		s3Select.progressReader, err = newProgressReader(rc, s3Select.Input.CompressionType)
   302  		if err != nil {
   303  			rc.Close()
   304  			return err
   305  		}
   306  
   307  		s3Select.recordReader, err = csv.NewReader(s3Select.progressReader, &s3Select.Input.CSVArgs)
   308  		if err != nil {
   309  			rc.Close()
   310  			var stErr bzip2.StructuralError
   311  			if errors.As(err, &stErr) {
   312  				return errInvalidBZIP2CompressionFormat(err)
   313  			}
   314  			return err
   315  		}
   316  		s3Select.close = rc.Close
   317  		return nil
   318  	case jsonFormat:
   319  		rc, err := getReader(0, -1)
   320  		if err != nil {
   321  			return err
   322  		}
   323  
   324  		s3Select.progressReader, err = newProgressReader(rc, s3Select.Input.CompressionType)
   325  		if err != nil {
   326  			rc.Close()
   327  			return err
   328  		}
   329  
   330  		if strings.EqualFold(s3Select.Input.JSONArgs.ContentType, "lines") {
   331  			if simdjson.SupportedCPU() {
   332  				s3Select.recordReader = simdj.NewReader(s3Select.progressReader, &s3Select.Input.JSONArgs)
   333  			} else {
   334  				s3Select.recordReader = json.NewPReader(s3Select.progressReader, &s3Select.Input.JSONArgs)
   335  			}
   336  		} else {
   337  			s3Select.recordReader = json.NewReader(s3Select.progressReader, &s3Select.Input.JSONArgs)
   338  		}
   339  
   340  		s3Select.close = rc.Close
   341  		return nil
   342  	case parquetFormat:
   343  		if !strings.EqualFold(os.Getenv("MINIO_API_SELECT_PARQUET"), "on") {
   344  			return errors.New("parquet format parsing not enabled on server")
   345  		}
   346  		var err error
   347  		s3Select.recordReader, err = parquet.NewReader(getReader, &s3Select.Input.ParquetArgs)
   348  		return err
   349  	}
   350  
   351  	panic(fmt.Errorf("unknown input format '%v'", s3Select.Input.format))
   352  }
   353  
   354  func (s3Select *S3Select) marshal(buf *bytes.Buffer, record sql.Record) error {
   355  	switch s3Select.Output.format {
   356  	case csvFormat:
   357  		// Use bufio Writer to prevent csv.Writer from allocating a new buffer.
   358  		bufioWriter := bufioWriterPool.Get().(*bufio.Writer)
   359  		defer func() {
   360  			bufioWriter.Reset(ioutil.Discard)
   361  			bufioWriterPool.Put(bufioWriter)
   362  		}()
   363  
   364  		bufioWriter.Reset(buf)
   365  		opts := sql.WriteCSVOpts{
   366  			FieldDelimiter: []rune(s3Select.Output.CSVArgs.FieldDelimiter)[0],
   367  			Quote:          []rune(s3Select.Output.CSVArgs.QuoteCharacter)[0],
   368  			QuoteEscape:    []rune(s3Select.Output.CSVArgs.QuoteEscapeCharacter)[0],
   369  			AlwaysQuote:    strings.ToLower(s3Select.Output.CSVArgs.QuoteFields) == "always",
   370  		}
   371  		err := record.WriteCSV(bufioWriter, opts)
   372  		if err != nil {
   373  			return err
   374  		}
   375  		err = bufioWriter.Flush()
   376  		if err != nil {
   377  			return err
   378  		}
   379  		if buf.Bytes()[buf.Len()-1] == '\n' {
   380  			buf.Truncate(buf.Len() - 1)
   381  		}
   382  		buf.WriteString(s3Select.Output.CSVArgs.RecordDelimiter)
   383  
   384  		return nil
   385  	case jsonFormat:
   386  		err := record.WriteJSON(buf)
   387  		if err != nil {
   388  			return err
   389  		}
   390  		// Trim trailing newline from non-simd output
   391  		if buf.Bytes()[buf.Len()-1] == '\n' {
   392  			buf.Truncate(buf.Len() - 1)
   393  		}
   394  		buf.WriteString(s3Select.Output.JSONArgs.RecordDelimiter)
   395  
   396  		return nil
   397  	}
   398  
   399  	panic(fmt.Errorf("unknown output format '%v'", s3Select.Output.format))
   400  }
   401  
   402  // Evaluate - filters and sends records read from opened reader as per select statement to http response writer.
   403  func (s3Select *S3Select) Evaluate(w http.ResponseWriter) {
   404  	defer func() {
   405  		if s3Select.close != nil {
   406  			s3Select.close()
   407  		}
   408  	}()
   409  
   410  	getProgressFunc := s3Select.getProgress
   411  	if !s3Select.Progress.Enabled {
   412  		getProgressFunc = nil
   413  	}
   414  	writer := newMessageWriter(w, getProgressFunc)
   415  
   416  	var outputQueue []sql.Record
   417  
   418  	// Create queue based on the type.
   419  	if s3Select.statement.IsAggregated() {
   420  		outputQueue = make([]sql.Record, 0, 1)
   421  	} else {
   422  		outputQueue = make([]sql.Record, 0, 100)
   423  	}
   424  	var err error
   425  	sendRecord := func() bool {
   426  		buf := bufPool.Get().(*bytes.Buffer)
   427  		buf.Reset()
   428  
   429  		for _, outputRecord := range outputQueue {
   430  			if outputRecord == nil {
   431  				continue
   432  			}
   433  			before := buf.Len()
   434  			if err = s3Select.marshal(buf, outputRecord); err != nil {
   435  				bufPool.Put(buf)
   436  				return false
   437  			}
   438  			if buf.Len()-before > maxRecordSize {
   439  				writer.FinishWithError("OverMaxRecordSize", "The length of a record in the input or result is greater than maxCharsPerRecord of 1 MB.")
   440  				bufPool.Put(buf)
   441  				return false
   442  			}
   443  		}
   444  
   445  		if err = writer.SendRecord(buf); err != nil {
   446  			// FIXME: log this error.
   447  			err = nil
   448  			bufPool.Put(buf)
   449  			return false
   450  		}
   451  		outputQueue = outputQueue[:0]
   452  		return true
   453  	}
   454  
   455  	var rec sql.Record
   456  OuterLoop:
   457  	for {
   458  		if s3Select.statement.LimitReached() {
   459  			if !sendRecord() {
   460  				break
   461  			}
   462  			if err = writer.Finish(s3Select.getProgress()); err != nil {
   463  				// FIXME: log this error.
   464  				err = nil
   465  			}
   466  			break
   467  		}
   468  
   469  		if rec, err = s3Select.recordReader.Read(rec); err != nil {
   470  			if err != io.EOF {
   471  				break
   472  			}
   473  
   474  			if s3Select.statement.IsAggregated() {
   475  				outputRecord := s3Select.outputRecord()
   476  				if err = s3Select.statement.AggregateResult(outputRecord); err != nil {
   477  					break
   478  				}
   479  				outputQueue = append(outputQueue, outputRecord)
   480  			}
   481  
   482  			if !sendRecord() {
   483  				break
   484  			}
   485  
   486  			if err = writer.Finish(s3Select.getProgress()); err != nil {
   487  				// FIXME: log this error.
   488  				err = nil
   489  			}
   490  			break
   491  		}
   492  
   493  		var inputRecords []*sql.Record
   494  		if inputRecords, err = s3Select.statement.EvalFrom(s3Select.Input.format, rec); err != nil {
   495  			break
   496  		}
   497  
   498  		for _, inputRecord := range inputRecords {
   499  			if s3Select.statement.IsAggregated() {
   500  				if err = s3Select.statement.AggregateRow(*inputRecord); err != nil {
   501  					break OuterLoop
   502  				}
   503  			} else {
   504  				var outputRecord sql.Record
   505  				// We will attempt to reuse the records in the table.
   506  				// The type of these should not change.
   507  				// The queue should always have at least one entry left for this to work.
   508  				outputQueue = outputQueue[:len(outputQueue)+1]
   509  				if t := outputQueue[len(outputQueue)-1]; t != nil {
   510  					// If the output record is already set, we reuse it.
   511  					outputRecord = t
   512  					outputRecord.Reset()
   513  				} else {
   514  					// Create new one
   515  					outputRecord = s3Select.outputRecord()
   516  					outputQueue[len(outputQueue)-1] = outputRecord
   517  				}
   518  				outputRecord, err = s3Select.statement.Eval(*inputRecord, outputRecord)
   519  				if outputRecord == nil || err != nil {
   520  					// This should not be written.
   521  					// Remove it from the queue.
   522  					outputQueue = outputQueue[:len(outputQueue)-1]
   523  					if err != nil {
   524  						break OuterLoop
   525  					}
   526  					continue
   527  				}
   528  
   529  				outputQueue[len(outputQueue)-1] = outputRecord
   530  				if len(outputQueue) < cap(outputQueue) {
   531  					continue
   532  				}
   533  
   534  				if !sendRecord() {
   535  					break OuterLoop
   536  				}
   537  			}
   538  		}
   539  	}
   540  
   541  	if err != nil {
   542  		_ = writer.FinishWithError("InternalError", err.Error())
   543  	}
   544  }
   545  
   546  // Close - closes opened S3 object.
   547  func (s3Select *S3Select) Close() error {
   548  	return s3Select.recordReader.Close()
   549  }
   550  
   551  // NewS3Select - creates new S3Select by given request XML reader.
   552  func NewS3Select(r io.Reader) (*S3Select, error) {
   553  	s3Select := &S3Select{}
   554  	if err := xml.NewDecoder(r).Decode(s3Select); err != nil {
   555  		return nil, err
   556  	}
   557  
   558  	return s3Select, nil
   559  }