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 }