Repository: influxdata/influxdb-client-go
Pull request #232 (2021-02-19 14:00:00)

feat(v3): Query part I. - annotatedscv/reader
Rework of #230.

## Proposed Changes
First PR for query: Adding `annotatedcsv.Reader` imported from https://github.com/rogpeppe/annotatedcsv and edited.
Update 3/1: Based on suggestions, added `QueryResultReader` to keep `annotatedcsv.Reader` as general as possible and move some features here:
 - treating error definition in section as a Go error
 - `NextRow` advances to the first row of the first section

## Checklist

<!-- Checkboxes below this note can be erased if not applicable to your Pull Request. -->

- [X] CHANGELOG.md updated
- [X] Rebased/mergeable
- [X] A test has been added if appropriate
- [X] Tests pass
- [X] Commit messages are in [semantic format](https://seesparkbox.com/foundry/semantic_commit_messages)




Patch:
diff --git a/LICENSE b/LICENSE
index 746804e..78ba679 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2020 Influxdata, Inc.
+Copyright (c) 2021 Influxdata, Inc.
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/TODO.md b/TODO.md
index c2c2e3e..dcb392a 100644
--- a/TODO.md
+++ b/TODO.md
@@ -399,37 +399,6 @@ type Filter struct {
 	AfterTime time.Time
 }
 
-type QueryResult struct {
-	// unexported fields.
-}
-
-// NextTable advances to the next table in the result.
-// Any remaining data in the current table is discarded.
-//
-// When there are no more tables, it returns false.
-func (r *QueryResult) NextTable() bool
-
-// NextRow advances to the next row in the current table.
-// When there are no more rows in the current table, it
-// returns false.
-func (r *QueryResult) NextRow() bool
-
-// Columns returns information on the columns in the current
-// table. It returns nil if there is no current table (for example
-// before NextTable has been called, or after NextTable returns false).
-func (r *QueryResult) Columns() []TableColumn
-
-// Err returns any error encountered. This should be called after NextTable
-// returns false to check that all the results were correctly received.
-func (r *QueryResult) Err() error
-
-// Values returns the values in the current row.
-// It returns nil if there is no current row.
-// All rows in a table have the same number of values.
-// The caller should not use the slice after NextRow
-// has been called again, because it's re-used.
-func (r *QueryResult) Values() []interface{}
-
 // Decode decodes the current row into x, which should be
 // a pointer to a struct. Columns in the row are decoded into
 // appropriate fields in the struct, using the tag conventions
diff --git a/annotatedcsv/reader.go b/annotatedcsv/reader.go
new file mode 100644
index 0000000..c1c37c1
--- /dev/null
+++ b/annotatedcsv/reader.go
@@ -0,0 +1,293 @@
+// Copyright 2021 InfluxData, Inc. All rights reserved.
+// Use of this source code is governed by MIT
+// license that can be found in the LICENSE file.
+
+// Package annotatedcsv provides a reader for annotated CSV sources.
+// Annotated CSV must contain at least an annotation with data types definition.
+// The first row after annotations must be a line with column names.
+//
+// Annotated CSV with multiple sections example:
+//		#datatype,string,long,dateTime:RFC3339,double,string,string
+//		#group,false,false,false,false,true,true
+//		#default,_result,,,,,
+//		,result,table,_time,_value,_field,location
+//		,,0,2020-02-18T10:34:08.135814545Z,23.4,temp,livingroom
+//		,,0,2020-02-18T22:08:44.850214724Z,21.6,temp,livingroom
+//
+//		#datatype,string,long,dateTime:RFC3339,double,string,string,string
+//		#group,false,false,false,false,true,true,true
+//		#default,_result,,,,,,
+//		,result,table,_time,_value,_field,location,sensor
+//		,,0,2020-02-18T10:34:08.135814545Z,20.5,temp,bedroom,SHT31
+//		,,0,2020-02-18T22:08:44.850214724Z,19.7,temp,bedroom,SHT31
+//
+// The set of rows following the annotation header is referred to in this documentation
+// as a "section". A single CSV stream can contain multiple sections, each with its own
+// column metadata.
+// CSV source can contain multiple sections, each section can have different columns (schema).
+//
+// Read more info about annotated CSV at https://docs.influxdata.com/influxdb/v2.0/reference/syntax/annotated-csv/.
+package annotatedcsv
+
+import (
+	"encoding/base64"
+	"fmt"
+	"io"
+	"math"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/influxdata/influxdb-client-go/internal/csv"
+)
+
+// Column represents a column in one section of CSV.
+type Column struct {
+	// Name holds the name of the column.
+	Name string
+	// Group specifies whether this column is a group key.
+	Group bool
+	// Default holds the default value for items in this column.
+	// It will be nil if no default is specified.
+	Default interface{}
+	// Type holds the type of the column.
+	Type string
+}
+
+// NewReader returns new Reader for parsing a stream in annotated CSV format.
+func NewReader(r io.Reader) *Reader {
+	r1 := &Reader{
+		r: csv.NewReader(r),
+	}
+	r1.r.FieldsPerRecord = -1
+	return r1
+}
+
+// Reader parses an annotated CSV stream.
+// Successive calls to NextSection will start reading each section of the CSV in turn.
+// The Columns method returns information on the columns (schema) for the section.
+//
+// Within a section, successive calls to NextRow will advance to each row.
+// If NextRow is called without first calling NextSection, the rows from the first section will be returned.
+//
+// The Row method returns all the data for a given row; ValueByName can be used
+// to get a value for a column with a particular name.
+type Reader struct {
+	cols []Column
+	row  []interface{}
+	err  error
+
+	hasPeeked bool
+	peekRow   []string
+	peekErr   error
+	r         *csv.Reader
+	// columnIndexes maps column names to their indexes.
+	columnIndexes map[string]int
+}
+
+// NextSection advances to the next section in the csv stream and reports whether
+// there is one.
+// Any remaining data in the current section is discarded.
+// When there are no more sections, it returns false.
+func (r *Reader) NextSection() bool {
+	if r.err != nil {
+		return false
+	}
+	// Read all the current rows.
+	for r.NextRow() {
+	}
+	_, err := r.peek()
+	if err != nil {
+		r.err = err
+		return false
+	}
+	cols, err := r.readHeader()
+	if err != nil {
+		r.err = err
+		return false
+	}
+	r.cols = cols
+	return true
+}
+
+// Err returns any error encountered when parsing.
+func (r *Reader) Err() error {
+	if r.err == io.EOF {
+		return nil
+	}
+	return r.err
+}
+
+// Columns returns information on the columns in the current
+// section. It returns nil if there is no current section (for example
+// before NextSection or NextRow has been called, or after NextSection returns false).
+func (r *Reader) Columns() []Column {
+	return r.cols
+}
+
+// NextRow advances to the next row in the current section.
+// When there are no more rows in the current section, it returns false.
+func (r *Reader) NextRow() bool {
+	if r.cols == nil || r.err != nil {
+		return false
+	}
+	row, err := r.readRow()
+	r.row = row
+	if row == nil {
+		r.err = err
+		r.cols = nil
+		r.columnIndexes = nil
+		return false
+	}
+	return true
+}
+
+// Row returns the values in the current row of the current section.
+// It returns nil if there is no current row.
+// All rows in a section have the same number of values.
+func (r *Reader) Row() []interface{} {
+	return r.row
+}
+
+// ValueByName returns the value for the column with the given name.
+// It returns nil if section has no value for such column.
+func (r *Reader) ValueByName(name string) interface{} {
+	if r.columnIndexes != nil {
+		if i, ok := r.columnIndexes[name]; ok {
+			return r.row[i]
+		}
+	}
+	return nil
+}
+
+func (r *Reader) readRow() ([]interface{}, error) {
+	row, err := r.peek()
+	if err != nil {
+		return nil, err
+	}
+	if len(row) > 0 && strings.HasPrefix(row[0], "#") {
+		// Start of next table.
+		return nil, nil
+	}
+	_, _ = r.read()
+	colsCount := len(row) - 1
+	if colsCount != len(r.cols) {
+		return nil, fmt.Errorf("inconsistent number of columns at line %d (got %d items want %d)", r.r.Line(), colsCount, len(r.cols))
+	}
+	rowVals := make([]interface{}, colsCount)
+	for i, val := range row[1:] {
+		col := r.cols[i]
+		if col.Default != nil && val == "" {
+			rowVals[i] = col.Default
+			continue
+		}
+		if val == "" && col.Name == "" {
+			continue
+		}
+		x, err := convertToType(val, col.Type)
+		if err != nil {
+			return nil, fmt.Errorf("cannot convert value %q to type %q at line %d: %v", val, col.Type, r.r.Line(), err)
+		}
+		rowVals[i] = x
+	}
+	return rowVals, nil
+}
+
+func (r *Reader) readHeader() ([]Column, error) {
+	var cols []Column
+	var defaults []string
+	for {
+		row, err := r.read()
+		if err != nil {
+			return cols, err
+		}
+		colsCount := len(row) - 1
+		if cols == nil {
+			cols = make([]Column, colsCount)
+		} else if colsCount != len(cols) {
+			return nil, fmt.Errorf("inconsistent table header (got %d items want %d)", colsCount, len(cols))
+		}
+		r.columnIndexes = map[string]int{}
+		if !strings.HasPrefix(row[0], "#") {
+			for i, col := range row[1:] {
+				cols[i].Name = col
+				r.columnIndexes[col] = i
+			}
+			break
+		}
+		switch row[0] {
+		case "#datatype":
+			for i, t := range row[1:] {
+				cols[i].Type = t
+			}
+		case "#group":
+			for i, c := range row[1:] {
+				cols[i].Group = c == "true"
+			}
+		case "#default":
+			defaults = row[1:]
+		}
+	}
+	for i, d := range defaults {
+		if d == "" {
+			continue
+		}
+		x, err := convertToType(d, cols[i].Type)
+		if err != nil {
+			return nil, fmt.Errorf("cannot convert default value %q to type %q: %v", d, cols[i].Type, err)
+		}
+		cols[i].Default = x
+	}
+	return cols, nil
+}
+
+func convertToType(s string, typ string) (interface{}, error) {
+	switch typ {
+	case "boolean":
+		return strconv.ParseBool(s)
+	case "long":
+		return strconv.ParseInt(s, 10, 64)
+	case "unsignedLong":
+		return strconv.ParseUint(s, 10, 64)
+	case "double":
+		x, err := strconv.ParseFloat(s, 64)
+		if err != nil {
+			return nil, err
+		}
+		if math.IsInf(x, 0) || math.IsNaN(x) {
+			return s, nil
+		}
+		return x, nil
+	case "string", "tag", "":
+		return s, nil
+	case "duration":
+		return time.ParseDuration(s)
+	case "base64Binary":
+		return base64.StdEncoding.DecodeString(s)
+	case "dateTime", "dateTime:RFC3339", "dateTime:RFC3339Nano":
+		return time.Parse(time.RFC3339, s)
+	}
+	return s, nil
+}
+
+// read consumes the next line from the CSV,
+// discarding any peeked data.
+func (r *Reader) read() ([]string, error) {
+	if r.hasPeeked {
+		row, err := r.peekRow, r.peekErr
+		r.peekRow, r.peekErr, r.hasPeeked = nil, nil, false
+		return row, err
+	}
+	return r.r.Read()
+}
+
+// peek returns the next line without consuming it.
+// Successive calls to peek will return the same line.
+func (r *Reader) peek() ([]string, error) {
+	if r.hasPeeked {
+		return r.peekRow, r.peekErr
+	}
+	row, err := r.r.Read()
+	r.peekRow, r.peekErr, r.hasPeeked = row, err, true
+	return row, err
+}
diff --git a/inluxclient/query.go b/inluxclient/query.go
new file mode 100644
index 0000000..7a8ce5c
--- /dev/null
+++ b/inluxclient/query.go
@@ -0,0 +1,122 @@
+// Copyright 2021 InfluxData, Inc. All rights reserved.
+// Use of this source code is governed by MIT
+// license that can be found in the LICENSE file.
+
+package influxclient
+
+import (
+	"errors"
+	"fmt"
+	"io"
+
+	"github.com/influxdata/influxdb-client-go/annotatedcsv"
+)
+
+// QueryError defines the information of Flux query error
+type QueryError struct {
+	// Message is a Flux query error message
+	Message string
+	// Code is an Flux query error code
+	Code int64
+}
+
+func (e *QueryError) Error() string {
+	return fmt.Sprintf("flux query error (code %d): %s", e.Code, e.Message)
+}
+
+// NewQueryResultReader returns new QueryResultReader for parsing Flux query result stream.
+func NewQueryResultReader(r io.ReadCloser) *QueryResultReader {
+	return &QueryResultReader{
+		Reader:  annotatedcsv.NewReader(r),
+		closer:  r,
+		initial: true,
+	}
+}
+
+// QueryResultReader enhances annotatedcsv.Reader
+// by allowing NextRow to go straight to the first data line
+// and by treating error section as an error.
+// QueryResultReader must be closed by calling Close at the end of reading.
+type QueryResultReader struct {
+	*annotatedcsv.Reader
+	err     error
+	initial bool
+	closer  io.Closer
+}
+
+// NextSection is like annotatedcsv.Reader.NextSection
+// except that it treats error sections as a terminating section.
+// When an error section is encountered, NextSection will
+// return false and Err will return the error.
+func (r *QueryResultReader) NextSection() bool {
+	r.initial = false
+	if r.err != nil {
+		return false
+	}
+	if !r.Reader.NextSection() {
+		return false
+	}
+	if err := r.errorSection(); err != nil {
+		r.err = err
+		return false
+	}
+	return true
+}
+
+// NextRow is like annotatedcsv.Reader.NextRow
+// except if it is called in the beginning it advances to the first section.
+// When an error section is encountered, NextRow will
+// return false and Err will return the error.
+func (r *QueryResultReader) NextRow() bool {
+	if r.err != nil {
+		return false
+	}
+	if r.initial {
+		// Invoke NextSection explicitly so that we'll immediately
+		// parse an error result.
+		if !r.NextSection() {
+			return false
+		}
+	}
+	return r.Reader.NextRow()
+}
+
+// errorSection checks if current section annotation defines
+// a query error statement and returns error containing the error message.
+func (r *QueryResultReader) errorSection() error {
+	if r.Columns()[0].Name == "error" {
+		cols := r.Columns()
+		if len(cols) != 2 || cols[0].Name != "error" || cols[1].Name != "reference" {
+			return nil
+		}
+		// next row is error definition
+		if !r.NextRow() {
+			if r.err != nil {
+				return r.err
+			} else {
+				return errors.New("no row found in error section")
+			}
+		}
+		row := r.Row()
+		message, ok1 := row[0].(string)
+		reference, ok2 := row[1].(int64)
+		if !ok1 || !ok2 {
+			return fmt.Errorf("unexpected column types (%T, %T) in error section", row[0], row[1])
+		}
+		return &QueryError{Message: message, Code: reference}
+	}
+	return nil
+}
+
+// Err returns any error encountered when parsing.
+func (r *QueryResultReader) Err() error {
+	if r.err != nil {
+		return r.err
+	}
+	return r.Reader.Err()
+}
+
+// Close closes the underlying reader
+func (r *QueryResultReader) Close() error {
+	return r.closer.Close()
+}
diff --git a/internal/csv/reader.go b/internal/csv/reader.go
new file mode 100644
index 0000000..108b221
--- /dev/null
+++ b/internal/csv/reader.go
@@ -0,0 +1,417 @@
+// Note: this package is a fork of the Go standard library's encoding/csv
+// package that adds the capability to find the current line number.
+// If/when https://github.com/golang/go/issues/44221 is accepted and implemented,
+// this can be removed.
+
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package csv reads and writes comma-separated values (CSV) files.
+// There are many kinds of CSV files; this package supports the format
+// described in RFC 4180.
+//
+// A csv file contains zero or more records of one or more fields per record.
+// Each record is separated by the newline character. The final record may
+// optionally be followed by a newline character.
+//
+//	field1,field2,field3
+//
+// White space is considered part of a field.
+//
+// Carriage returns before newline characters are silently removed.
+//
+// Blank lines are ignored. A line with only whitespace characters (excluding
+// the ending newline character) is not considered a blank line.
+//
+// Fields which start and stop with the quote character " are called
+// quoted-fields. The beginning and ending quote are not part of the
+// field.
+//
+// The source:
+//
+//	normal string,"quoted-field"
+//
+// results in the fields
+//
+//	{`normal string`, `quoted-field`}
+//
+// Within a quoted-field a quote character followed by a second quote
+// character is considered a single quote.
+//
+//	"the ""word"" is true","a ""quoted-field"""
+//
+// results in
+//
+//	{`the "word" is true`, `a "quoted-field"`}
+//
+// Newlines and commas may be included in a quoted-field
+//
+//	"Multi-line
+//	field","comma is ,"
+//
+// results in
+//
+//	{`Multi-line
+//	field`, `comma is ,`}
+package csv
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"unicode"
+	"unicode/utf8"
+)
+
+// A ParseError is returned for parsing errors.
+// Line numbers are 1-indexed and columns are 0-indexed.
+type ParseError struct {
+	StartLine int   // Line where the record starts
+	Line      int   // Line where the error occurred
+	Column    int   // Column (rune index) where the error occurred
+	Err       error // The actual error
+}
+
+func (e *ParseError) Error() string {
+	if e.Err == ErrFieldCount {
+		return fmt.Sprintf("record on line %d: %v", e.Line, e.Err)
+	}
+	if e.StartLine != e.Line {
+		return fmt.Sprintf("record on line %d; parse error on line %d, column %d: %v", e.StartLine, e.Line, e.Column, e.Err)
+	}
+	return fmt.Sprintf("parse error on line %d, column %d: %v", e.Line, e.Column, e.Err)
+}
+
+func (e *ParseError) Unwrap() error { return e.Err }
+
+// These are the errors that can be returned in ParseError.Err.
+var (
+	ErrTrailingComma = errors.New("extra delimiter at end of line") // Deprecated: No longer used.
+	ErrBareQuote     = errors.New("bare \" in non-quoted-field")
+	ErrQuote         = errors.New("extraneous or missing \" in quoted-field")
+	ErrFieldCount    = errors.New("wrong number of fields")
+)
+
+var errInvalidDelim = errors.New("csv: invalid field or comment delimiter")
+
+func validDelim(r rune) bool {
+	return r != 0 && r != '"' && r != '\r' && r != '\n' && utf8.ValidRune(r) && r != utf8.RuneError
+}
+
+// A Reader reads records from a CSV-encoded file.
+//
+// As returned by NewReader, a Reader expects input conforming to RFC 4180.
+// The exported fields can be changed to customize the details before the
+// first call to Read or ReadAll.
+//
+// The Reader converts all \r\n sequences in its input to plain \n,
+// including in multiline field values, so that the returned data does
+// not depend on which line-ending convention an input file uses.
+type Reader struct {
+	// Comma is the field delimiter.
+	// It is set to comma (',') by NewReader.
+	// Comma must be a valid rune and must not be \r, \n,
+	// or the Unicode replacement character (0xFFFD).
+	Comma rune
+
+	// Comment, if not 0, is the comment character. Lines beginning with the
+	// Comment character without preceding whitespace are ignored.
+	// With leading whitespace the Comment character becomes part of the
+	// field, even if TrimLeadingSpace is true.
+	// Comment must be a valid rune and must not be \r, \n,
+	// or the Unicode replacement character (0xFFFD).
+	// It must also not be equal to Comma.
+	Comment rune
+
+	// FieldsPerRecord is the number of expected fields per record.
+	// If FieldsPerRecord is positive, Read requires each record to
+	// have the given number of fields. If FieldsPerRecord is 0, Read sets it to
+	// the number of fields in the first record, so that future records must
+	// have the same field count. If FieldsPerRecord is negative, no check is
+	// made and records may have a variable number of fields.
+	FieldsPerRecord int
+
+	// If LazyQuotes is true, a quote may appear in an unquoted field and a
+	// non-doubled quote may appear in a quoted field.
+	LazyQuotes bool
+
+	// If TrimLeadingSpace is true, leading white space in a field is ignored.
+	// This is done even if the field delimiter, Comma, is white space.
+	TrimLeadingSpace bool
+
+	// ReuseRecord controls whether calls to Read may return a slice sharing
+	// the backing array of the previous call's returned slice for performance.
+	// By default, each call to Read returns newly allocated memory owned by the caller.
+	ReuseRecord bool
+
+	TrailingComma bool // Deprecated: No longer used.
+
+	r *bufio.Reader
+
+	// numLine is the current line being read in the CSV file.
+	numLine int
+
+	// startLine is the line of the start of the most recently returned
+	// record.
+	startLine int
+
+	// rawBuffer is a line buffer only used by the readLine method.
+	rawBuffer []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 is an index of fields inside recordBuffer.
+	// The i'th field ends at offset fieldIndexes[i] in recordBuffer.
+	fieldIndexes []int
+
+	// lastRecord is a record cache and only used when ReuseRecord == true.
+	lastRecord []string
+}
+
+// NewReader returns a new Reader that reads from r.
+func NewReader(r io.Reader) *Reader {
+	return &Reader{
+		Comma: ',',
+		r:     bufio.NewReader(r),
+	}
+}
+
+// Read reads one record (a slice of fields) from r.
+// If the record has an unexpected number of fields,
+// Read returns the record along with the error ErrFieldCount.
+// Except for that case, Read always returns either a non-nil
+// record or a non-nil error, but not both.
+// If there is no data left to be read, Read returns nil, io.EOF.
+// If ReuseRecord is true, the returned slice may be shared
+// between multiple calls to Read.
+func (r *Reader) Read() (record []string, err error) {
+	if r.ReuseRecord {
+		record, r.startLine, err = r.readRecord(r.lastRecord)
+		r.lastRecord = record
+	} else {
+		record, r.startLine, err = r.readRecord(nil)
+	}
+	return record, err
+}
+
+// Line returns the line number of the start of the most
+// recently returned record. If no records have been returned,
+// it returns zero.
+func (r *Reader) Line() int {
+	return r.startLine
+}
+
+// ReadAll reads all the remaining records from r.
+// Each record is a slice of fields.
+// A successful call returns err == nil, not err == io.EOF. Because ReadAll is
+// defined to read until EOF, it does not treat end of file as an error to be
+// reported.
+func (r *Reader) ReadAll() (records [][]string, err error) {
+	for {
+		record, _, err := r.readRecord(nil)
+		if err == io.EOF {
+			return records, nil
+		}
+		if err != nil {
+			return nil, err
+		}
+		records = append(records, record)
+	}
+}
+
+// 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 (r *Reader) readLine() ([]byte, error) {
+	line, err := r.r.ReadSlice('\n')
+	if err == bufio.ErrBufferFull {
+		r.rawBuffer = append(r.rawBuffer[:0], line...)
+		for err == bufio.ErrBufferFull {
+			line, err = r.r.ReadSlice('\n')
+			r.rawBuffer = append(r.rawBuffer, line...)
+		}
+		line = r.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]
+		}
+	}
+	r.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]
+	}
+	return line, err
+}
+
+// lengthNL reports the number of bytes for the trailing \n.
+func lengthNL(b []byte) int {
+	if len(b) > 0 && b[len(b)-1] == '\n' {
+		return 1
+	}
+	return 0
+}
+
+// nextRune returns the next rune in b or utf8.RuneError.
+func nextRune(b []byte) rune {
+	r, _ := utf8.DecodeRune(b)
+	return r
+}
+
+func (r *Reader) readRecord(dst []string) ([]string, int, error) {
+	if r.Comma == r.Comment || !validDelim(r.Comma) || (r.Comment != 0 && !validDelim(r.Comment)) {
+		return nil, r.numLine, errInvalidDelim
+	}
+
+	// Read line (automatically skipping past empty lines and any comments).
+	var line, fullLine []byte
+	var errRead error
+	for errRead == nil {
+		line, errRead = r.readLine()
+		if r.Comment != 0 && nextRune(line) == r.Comment {
+			line = nil
+			continue // Skip comment lines
+		}
+		if errRead == nil && len(line) == lengthNL(line) {
+			line = nil
+			continue // Skip empty lines
+		}
+		fullLine = line
+		break
+	}
+	if errRead == io.EOF {
+		return nil, r.numLine, errRead
+	}
+
+	// Parse each field in the record.
+	var err error
+	const quoteLen = len(`"`)
+	commaLen := utf8.RuneLen(r.Comma)
+	recLine := r.numLine // Starting line for record
+	r.recordBuffer = r.recordBuffer[:0]
+	r.fieldIndexes = r.fieldIndexes[:0]
+parseField:
+	for {
+		if r.TrimLeadingSpace {
+			line = bytes.TrimLeftFunc(line, unicode.IsSpace)
+		}
+		if len(line) == 0 || line[0] != '"' {
+			// Non-quoted string field
+			i := bytes.IndexRune(line, r.Comma)
+			field := line
+			if i >= 0 {
+				field = field[:i]
+			} else {
+				field = field[:len(field)-lengthNL(field)]
+			}
+			// Check to make sure a quote does not appear in field.
+			if !r.LazyQuotes {
+				if j := bytes.IndexByte(field, '"'); j >= 0 {
+					col := utf8.RuneCount(fullLine[:len(fullLine)-len(line[j:])])
+					err = &ParseError{StartLine: recLine, Line: r.numLine, Column: col, Err: ErrBareQuote}
+					break parseField
+				}
+			}
+			r.recordBuffer = append(r.recordBuffer, field...)
+			r.fieldIndexes = append(r.fieldIndexes, len(r.recordBuffer))
+			if i >= 0 {
+				line = line[i+commaLen:]
+				continue parseField
+			}
+			break parseField
+		} else {
+			// Quoted string field
+			line = line[quoteLen:]
+			for {
+				i := bytes.IndexByte(line, '"')
+				if i >= 0 {
+					// Hit next quote.
+					r.recordBuffer = append(r.recordBuffer, line[:i]...)
+					line = line[i+quoteLen:]
+					switch rn := nextRune(line); {
+					case rn == '"':
+						// `""` sequence (append quote).
+						r.recordBuffer = append(r.recordBuffer, '"')
+						line = line[quoteLen:]
+					case rn == r.Comma:
+						// `",` sequence (end of field).
+						line = line[commaLen:]
+						r.fieldIndexes = append(r.fieldIndexes, len(r.recordBuffer))
+						continue parseField
+					case lengthNL(line) == len(line):
+						// `"\n` sequence (end of line).
+						r.fieldIndexes = append(r.fieldIndexes, len(r.recordBuffer))
+						break parseField
+					case r.LazyQuotes:
+						// `"` sequence (bare quote).
+						r.recordBuffer = append(r.recordBuffer, '"')
+					default:
+						// `"*` sequence (invalid non-escaped quote).
+						col := utf8.RuneCount(fullLine[:len(fullLine)-len(line)-quoteLen])
+						err = &ParseError{StartLine: recLine, Line: r.numLine, Column: col, Err: ErrQuote}
+						break parseField
+					}
+				} else if len(line) > 0 {
+					// Hit end of line (copy all data so far).
+					r.recordBuffer = append(r.recordBuffer, line...)
+					if errRead != nil {
+						break parseField
+					}
+					line, errRead = r.readLine()
+					if errRead == io.EOF {
+						errRead = nil
+					}
+					fullLine = line
+				} else {
+					// Abrupt end of file (EOF or error).
+					if !r.LazyQuotes && errRead == nil {
+						col := utf8.RuneCount(fullLine)
+						err = &ParseError{StartLine: recLine, Line: r.numLine, Column: col, Err: ErrQuote}
+						break parseField
+					}
+					r.fieldIndexes = append(r.fieldIndexes, len(r.recordBuffer))
+					break parseField
+				}
+			}
+		}
+	}
+	if err == nil {
+		err = errRead
+	}
+
+	// Create a single string and create slices out of it.
+	// This pins the memory of the fields together, but allocates once.
+	str := string(r.recordBuffer) // Convert to string once to batch allocations
+	dst = dst[:0]
+	if cap(dst) < len(r.fieldIndexes) {
+		dst = make([]string, len(r.fieldIndexes))
+	}
+	dst = dst[:len(r.fieldIndexes)]
+	var preIdx int
+	for i, idx := range r.fieldIndexes {
+		dst[i] = str[preIdx:idx]
+		preIdx = idx
+	}
+
+	// Check or update the expected fields per record.
+	if r.FieldsPerRecord > 0 {
+		if len(dst) != r.FieldsPerRecord && err == nil {
+			err = &ParseError{StartLine: recLine, Line: recLine, Err: ErrFieldCount}
+		}
+	} else if r.FieldsPerRecord == 0 {
+		r.FieldsPerRecord = len(dst)
+	}
+	return dst, recLine, err
+}


Test patch:
diff --git a/annotatedcsv/reader_test.go b/annotatedcsv/reader_test.go
new file mode 100644
index 0000000..d38c568
--- /dev/null
+++ b/annotatedcsv/reader_test.go
@@ -0,0 +1,1213 @@
+// Copyright 2021 InfluxData, Inc. All rights reserved.
+// Use of this source code is governed by MIT
+// license that can be found in the LICENSE file.
+
+package annotatedcsv_test
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/influxdata/influxdb-client-go/annotatedcsv"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestSingleTable(t *testing.T) {
+	csvTable := `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+
+`
+	tables := []expectedTable{
+		{columns: []annotatedcsv.Column{
+			{Type: "string", Default: "_result", Name: "result", Group: false},
+			{Type: "long", Default: nil, Name: "table", Group: false},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_start", Group: true},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_stop", Group: true},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_time", Group: false},
+			{Type: "double", Default: nil, Name: "_value", Group: false},
+			{Type: "string", Default: nil, Name: "_field", Group: true},
+			{Type: "string", Default: nil, Name: "_measurement", Group: true},
+			{Type: "string", Default: nil, Name: "a", Group: true},
+			{Type: "string", Default: nil, Name: "b", Group: true},
+		},
+			rows: [][]interface{}{
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T10:34:08.135814545Z"),
+					1.4,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.850214724Z"),
+					6.6,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+			},
+		},
+	}
+
+	verifyTables(t, csvTable, tables)
+}
+
+func TestMultiTables(t *testing.T) {
+	csvTableMultiStructure := `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+
+#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,long,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,1,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,4,i,test,1,adsfasdf
+,,1,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,-1,i,test,1,adsfasdf
+
+#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,boolean,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,2,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.62797864Z,false,f,test,0,adsfasdf
+,,2,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.969100374Z,true,f,test,0,adsfasdf
+
+#datatype,string,long,dateTime:RFC3339Nano,dateTime:RFC3339Nano,dateTime:RFC3339Nano,unsignedLong,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,3,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.62797864Z,0,i,test,0,adsfasdf
+,,3,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.969100374Z,2,i,test,0,adsfasdf
+
+`
+	tablesMultiStructure := []expectedTable{
+		{ // Table 1
+			columns: []annotatedcsv.Column{
+				{Type: "string", Default: "_result", Name: "result", Group: false},
+				{Type: "long", Default: nil, Name: "table", Group: false},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_start", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_stop", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_time", Group: false},
+				{Type: "double", Default: nil, Name: "_value", Group: false},
+				{Type: "string", Default: nil, Name: "_field", Group: true},
+				{Type: "string", Default: nil, Name: "_measurement", Group: true},
+				{Type: "string", Default: nil, Name: "a", Group: true},
+				{Type: "string", Default: nil, Name: "b", Group: true},
+			},
+			rows: [][]interface{}{
+				{"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T10:34:08.135814545Z"),
+					1.4,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.850214724Z"),
+					6.6,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+			},
+		},
+		{ //Table 2
+			columns: []annotatedcsv.Column{
+				{Type: "string", Default: "_result", Name: "result", Group: false},
+				{Type: "long", Default: nil, Name: "table", Group: false},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_start", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_stop", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_time", Group: false},
+				{Type: "long", Default: nil, Name: "_value", Group: false},
+				{Type: "string", Default: nil, Name: "_field", Group: true},
+				{Type: "string", Default: nil, Name: "_measurement", Group: true},
+				{Type: "string", Default: nil, Name: "a", Group: true},
+				{Type: "string", Default: nil, Name: "b", Group: true},
+			},
+			rows: [][]interface{}{
+				{"_result",
+					int64(1),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T10:34:08.135814545Z"),
+					int64(4),
+					"i",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(1),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.850214724Z"),
+					int64(-1),
+					"i",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+			},
+		},
+		{ // Table 3
+			columns: []annotatedcsv.Column{
+				{Type: "string", Default: "_result", Name: "result", Group: false},
+				{Type: "long", Default: nil, Name: "table", Group: false},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_start", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_stop", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_time", Group: false},
+				{Type: "boolean", Default: nil, Name: "_value", Group: false},
+				{Type: "string", Default: nil, Name: "_field", Group: true},
+				{Type: "string", Default: nil, Name: "_measurement", Group: true},
+				{Type: "string", Default: nil, Name: "a", Group: true},
+				{Type: "string", Default: nil, Name: "b", Group: true},
+			},
+			rows: [][]interface{}{
+				{"_result",
+					int64(2),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.62797864Z"),
+					false,
+					"f",
+					"test",
+					"0",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(2),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.969100374Z"),
+					true,
+					"f",
+					"test",
+					"0",
+					"adsfasdf",
+				},
+			},
+		},
+		{ //Table 4
+			columns: []annotatedcsv.Column{
+				{Type: "string", Default: "_result", Name: "result", Group: false},
+				{Type: "long", Default: nil, Name: "table", Group: false},
+				{Type: "dateTime:RFC3339Nano", Default: nil, Name: "_start", Group: true},
+				{Type: "dateTime:RFC3339Nano", Default: nil, Name: "_stop", Group: true},
+				{Type: "dateTime:RFC3339Nano", Default: nil, Name: "_time", Group: false},
+				{Type: "unsignedLong", Default: nil, Name: "_value", Group: false},
+				{Type: "string", Default: nil, Name: "_field", Group: true},
+				{Type: "string", Default: nil, Name: "_measurement", Group: true},
+				{Type: "string", Default: nil, Name: "a", Group: true},
+				{Type: "string", Default: nil, Name: "b", Group: true},
+			},
+			rows: [][]interface{}{
+				{"_result",
+					int64(3),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.62797864Z"),
+					uint64(0),
+					"i",
+					"test",
+					"0",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(3),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.969100374Z"),
+					uint64(2),
+					"i",
+					"test",
+					"0",
+					"adsfasdf",
+				},
+			},
+		},
+	}
+	verifyTables(t, csvTableMultiStructure, tablesMultiStructure)
+
+	// test advancing
+	reader := strings.NewReader(csvTableMultiStructure)
+	res := annotatedcsv.NewReader(reader)
+
+	require.False(t, res.NextRow())
+	require.Nil(t, res.Err())
+	require.True(t, res.NextSection())
+	require.Nil(t, res.Err())
+	require.True(t, res.NextRow())
+	require.Nil(t, res.Err())
+	require.Equal(t, tablesMultiStructure[0].rows[0], res.Row())
+
+	reader = strings.NewReader(csvTableMultiStructure)
+	res = annotatedcsv.NewReader(reader)
+
+	//test skip tables
+	require.True(t, res.NextSection())
+	require.Nil(t, res.Err())
+	require.True(t, res.NextSection())
+	require.Nil(t, res.Err())
+	require.True(t, res.NextRow())
+	require.Nil(t, res.Err())
+	require.True(t, res.NextSection())
+	require.Nil(t, res.Err())
+	require.True(t, res.NextRow())
+	require.Nil(t, res.Err())
+	require.Equal(t, tablesMultiStructure[2].rows[0], res.Row())
+
+	csvTableMultiTables := `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+,,1,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,4.3,i,test,1,xyxyxyxy
+,,1,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,-1.2,i,test,1,xyxyxyxy
+,,2,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.62797864Z,0.1,f,test,0,adsfasdf
+,,2,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.969100374Z,0.3,f,test,0,adsfasdf
+,,3,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.62797864Z,10,i,test,0,xyxyxyxy
+,,3,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.969100374Z,2,i,test,0,xyxyxyxy
+
+`
+	tablesMultiTables := []expectedTable{
+		{
+			columns: []annotatedcsv.Column{
+				{Type: "string", Default: "_result", Name: "result", Group: false},
+				{Type: "long", Default: nil, Name: "table", Group: false},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_start", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_stop", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_time", Group: false},
+				{Type: "double", Default: nil, Name: "_value", Group: false},
+				{Type: "string", Default: nil, Name: "_field", Group: true},
+				{Type: "string", Default: nil, Name: "_measurement", Group: true},
+				{Type: "string", Default: nil, Name: "a", Group: true},
+				{Type: "string", Default: nil, Name: "b", Group: true},
+			},
+			rows: [][]interface{}{
+				{"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T10:34:08.135814545Z"),
+					1.4,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.850214724Z"),
+					6.6,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+				{"_result",
+					int64(1),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T10:34:08.135814545Z"),
+					4.3,
+					"i",
+					"test",
+					"1",
+					"xyxyxyxy",
+				},
+				{
+					"_result",
+					int64(1),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.850214724Z"),
+					-1.2,
+					"i",
+					"test",
+					"1",
+					"xyxyxyxy",
+				},
+				{"_result",
+					int64(2),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.62797864Z"),
+					0.1,
+					"f",
+					"test",
+					"0",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(2),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.969100374Z"),
+					0.3,
+					"f",
+					"test",
+					"0",
+					"adsfasdf",
+				},
+				{"_result",
+					int64(3),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.62797864Z"),
+					float64(10),
+					"i",
+					"test",
+					"0",
+					"xyxyxyxy",
+				},
+				{
+					"_result",
+					int64(3),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.969100374Z"),
+					float64(2),
+					"i",
+					"test",
+					"0",
+					"xyxyxyxy",
+				},
+			},
+		},
+	}
+	verifyTables(t, csvTableMultiTables, tablesMultiTables)
+
+	//test advancing
+	reader = strings.NewReader(csvTableMultiTables)
+	res = annotatedcsv.NewReader(reader)
+	require.True(t, res.NextSection())
+	require.Nil(t, res.Err())
+	require.False(t, res.NextSection())
+	require.Nil(t, res.Err())
+}
+
+func TestValueByName(t *testing.T) {
+	csvTable := `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,long,string,duration,base64Binary,dateTime:RFC3339
+#group,false,false,true,true,false,true,true,false,false,false
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,deviceId,sensor,elapsed,note,start
+,,0,2020-04-28T12:36:50.990018157Z,2020-04-28T12:51:50.990018157Z,2020-04-28T12:38:11.480545389Z,1467463,BME280,1m1s,ZGF0YWluYmFzZTY0,2020-04-27T00:00:00Z
+,,1,2020-04-28T12:36:50.990018157Z,2020-04-28T12:51:50.990018157Z,2020-04-28T12:39:36.330153686Z,1467463,BME280,1h20m30.13245s,eHh4eHhjY2NjY2NkZGRkZA==,2020-04-28T00:00:00Z
+
+#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+
+`
+	tables := []expectedTable{
+		{ // Table 1
+			columns: []annotatedcsv.Column{
+				{Type: "string", Default: "_result", Name: "result", Group: false},
+				{Type: "long", Default: nil, Name: "table", Group: false},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_start", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_stop", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_time", Group: false},
+				{Type: "long", Default: nil, Name: "deviceId", Group: true},
+				{Type: "string", Default: nil, Name: "sensor", Group: true},
+				{Type: "duration", Default: nil, Name: "elapsed", Group: false},
+				{Type: "base64Binary", Default: nil, Name: "note", Group: false},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "start", Group: false},
+			},
+			rows: [][]interface{}{
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-04-28T12:36:50.990018157Z"),
+					mustParseTime("2020-04-28T12:51:50.990018157Z"),
+					mustParseTime("2020-04-28T12:38:11.480545389Z"),
+					int64(1467463),
+					"BME280",
+					time.Minute + time.Second,
+					[]byte("datainbase64"),
+					time.Date(2020, 4, 27, 0, 0, 0, 0, time.UTC),
+				},
+				{
+					"_result",
+					int64(1),
+					mustParseTime("2020-04-28T12:36:50.990018157Z"),
+					mustParseTime("2020-04-28T12:51:50.990018157Z"),
+					mustParseTime("2020-04-28T12:39:36.330153686Z"),
+					int64(1467463),
+					"BME280",
+					time.Hour + 20*time.Minute + 30*time.Second + 132450000*time.Nanosecond,
+					[]byte("xxxxxccccccddddd"),
+					time.Date(2020, 4, 28, 0, 0, 0, 0, time.UTC),
+				},
+			},
+		},
+		{
+			columns: []annotatedcsv.Column{
+				{Type: "string", Default: "_result", Name: "result", Group: false},
+				{Type: "long", Default: nil, Name: "table", Group: false},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_start", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_stop", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_time", Group: false},
+				{Type: "double", Default: nil, Name: "_value", Group: false},
+				{Type: "string", Default: nil, Name: "_field", Group: true},
+				{Type: "string", Default: nil, Name: "_measurement", Group: true},
+				{Type: "string", Default: nil, Name: "a", Group: true},
+				{Type: "string", Default: nil, Name: "b", Group: true},
+			},
+			rows: [][]interface{}{
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T10:34:08.135814545Z"),
+					1.4,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.850214724Z"),
+					6.6,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+			},
+		},
+	}
+
+	verifyTables(t, csvTable, tables)
+
+	reader := strings.NewReader(csvTable)
+	res := annotatedcsv.NewReader(reader)
+
+	require.True(t, res.NextSection() && res.NextRow(), res.Err())
+	require.Nil(t, res.Err())
+	assert.Equal(t, []byte("datainbase64"), res.ValueByName("note"))
+	assert.Nil(t, res.ValueByName(""))
+	assert.Nil(t, res.ValueByName("invalid"))
+	assert.Nil(t, res.ValueByName("a"))
+
+	require.True(t, res.NextSection() && res.NextRow(), res.Err())
+	assert.Equal(t, "1", res.ValueByName("a"))
+	assert.Nil(t, res.ValueByName("note"))
+
+}
+
+func TestErrorInRow(t *testing.T) {
+	csvTableError := `#datatype,string,string
+#group,true,true
+#default,,
+,error,reference
+,failed to create physical plan: invalid time bounds from procedure from: bounds contain zero time,897
+
+`
+
+	tables := []expectedTable{
+		{ // Table 1
+			[]annotatedcsv.Column{
+				{Type: "string", Default: nil, Name: "error", Group: true},
+				{Type: "string", Default: nil, Name: "reference", Group: true},
+			},
+			[][]interface{}{
+				{
+					"failed to create physical plan: invalid time bounds from procedure from: bounds contain zero time",
+					"897",
+				},
+			},
+		},
+	}
+	verifyTables(t, csvTableError, tables)
+
+	csvTableErrorNoReference := `#datatype,string,string
+#group,true,true
+#default,,
+,error,reference
+,failed to create physical plan: invalid time bounds from procedure from: bounds contain zero time,
+
+`
+	tables = []expectedTable{
+		{ // Table 1
+			[]annotatedcsv.Column{
+				{Type: "string", Default: nil, Name: "error", Group: true},
+				{Type: "string", Default: nil, Name: "reference", Group: true},
+			},
+			[][]interface{}{
+				{
+					"failed to create physical plan: invalid time bounds from procedure from: bounds contain zero time",
+					"",
+				},
+			},
+		},
+	}
+	verifyTables(t, csvTableErrorNoReference, tables)
+
+	csvTableErrorNoMessage := `#datatype,string,string
+#group,true,true
+#default,,
+,error,reference
+,,
+
+`
+	tables = []expectedTable{
+		{ // Table 1
+			[]annotatedcsv.Column{
+				{Type: "string", Default: nil, Name: "error", Group: true},
+				{Type: "string", Default: nil, Name: "reference", Group: true},
+			},
+			[][]interface{}{
+				{
+					"",
+					"",
+				},
+			},
+		},
+	}
+	verifyTables(t, csvTableErrorNoMessage, tables)
+}
+
+func TestInvalidDataType(t *testing.T) {
+	csvTable := `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,int,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6,f,test,1,adsfasdf
+
+`
+	tables := []expectedTable{
+		{[]annotatedcsv.Column{
+			{Type: "string", Default: "_result", Name: "result", Group: false},
+			{Type: "long", Default: nil, Name: "table", Group: false},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_start", Group: true},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_stop", Group: true},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_time", Group: false},
+			{Type: "int", Default: nil, Name: "_value", Group: false},
+			{Type: "string", Default: nil, Name: "_field", Group: true},
+			{Type: "string", Default: nil, Name: "_measurement", Group: true},
+			{Type: "string", Default: nil, Name: "a", Group: true},
+			{Type: "string", Default: nil, Name: "b", Group: true},
+		},
+			[][]interface{}{
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T10:34:08.135814545Z"),
+					"1",
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.850214724Z"),
+					"6",
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+			},
+		},
+	}
+
+	verifyTables(t, csvTable, tables)
+}
+
+func TestReorderedAnnotations(t *testing.T) {
+	csvTable1 := `#group,false,false,true,true,false,false,true,true,true,true
+#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+
+`
+	tables := []expectedTable{
+		{[]annotatedcsv.Column{
+			{Type: "string", Default: "_result", Name: "result", Group: false},
+			{Type: "long", Default: nil, Name: "table", Group: false},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_start", Group: true},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_stop", Group: true},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_time", Group: false},
+			{Type: "double", Default: nil, Name: "_value", Group: false},
+			{Type: "string", Default: nil, Name: "_field", Group: true},
+			{Type: "string", Default: nil, Name: "_measurement", Group: true},
+			{Type: "string", Default: nil, Name: "a", Group: true},
+			{Type: "string", Default: nil, Name: "b", Group: true},
+		},
+			[][]interface{}{
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T10:34:08.135814545Z"),
+					1.4,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.850214724Z"),
+					6.6,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+			},
+		},
+	}
+
+	verifyTables(t, csvTable1, tables)
+
+	csvTable2 := `#default,_result,,,,,,,,,
+#group,false,false,true,true,false,false,true,true,true,true
+#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+
+`
+	verifyTables(t, csvTable2, tables)
+}
+
+func TestDatatypeOnlyAnnotation(t *testing.T) {
+	csvTable1 := `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+
+`
+	tables := []expectedTable{
+		{[]annotatedcsv.Column{
+			{Type: "string", Default: nil, Name: "result", Group: false},
+			{Type: "long", Default: nil, Name: "table", Group: false},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_start", Group: false},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_stop", Group: false},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_time", Group: false},
+			{Type: "double", Default: nil, Name: "_value", Group: false},
+			{Type: "string", Default: nil, Name: "_field", Group: false},
+			{Type: "string", Default: nil, Name: "_measurement", Group: false},
+			{Type: "string", Default: nil, Name: "a", Group: false},
+			{Type: "string", Default: nil, Name: "b", Group: false},
+		},
+			[][]interface{}{
+				{
+					"",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T10:34:08.135814545Z"),
+					1.4,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+				{
+					"",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.850214724Z"),
+					6.6,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+			},
+		},
+	}
+
+	verifyTables(t, csvTable1, tables)
+}
+
+func TestMissingDatatypeAnnotation(t *testing.T) {
+	csvTable1 := `
+#group,false,false,true,true,false,true,true,false,false,false
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,deviceId,sensor,elapsed,note,start
+,,0,2020-04-28T12:36:50.990018157Z,2020-04-28T12:51:50.990018157Z,2020-04-28T12:38:11.480545389Z,1467463,BME280,1m1s,ZGF0YWluYmFzZTY0,2020-04-27T00:00:00Z
+,,0,2020-04-28T12:36:50.990018157Z,2020-04-28T12:51:50.990018157Z,2020-04-28T12:39:36.330153686Z,1467463,BME280,1h20m30.13245s,eHh4eHhjY2NjY2NkZGRkZA==,2020-04-28T00:00:00Z
+`
+	tables := []expectedTable{
+		{ // Table 1
+			columns: []annotatedcsv.Column{
+				{Type: "", Default: "_result", Name: "result", Group: false},
+				{Type: "", Default: nil, Name: "table", Group: false},
+				{Type: "", Default: nil, Name: "_start", Group: true},
+				{Type: "", Default: nil, Name: "_stop", Group: true},
+				{Type: "", Default: nil, Name: "_time", Group: false},
+				{Type: "", Default: nil, Name: "deviceId", Group: true},
+				{Type: "", Default: nil, Name: "sensor", Group: true},
+				{Type: "", Default: nil, Name: "elapsed", Group: false},
+				{Type: "", Default: nil, Name: "note", Group: false},
+				{Type: "", Default: nil, Name: "start", Group: false},
+			},
+			rows: [][]interface{}{
+				{
+					"_result",
+					"0",
+					"2020-04-28T12:36:50.990018157Z",
+					"2020-04-28T12:51:50.990018157Z",
+					"2020-04-28T12:38:11.480545389Z",
+					"1467463",
+					"BME280",
+					"1m1s",
+					"ZGF0YWluYmFzZTY0",
+					"2020-04-27T00:00:00Z",
+				},
+				{
+					"_result",
+					"0",
+					"2020-04-28T12:36:50.990018157Z",
+					"2020-04-28T12:51:50.990018157Z",
+					"2020-04-28T12:39:36.330153686Z",
+					"1467463",
+					"BME280",
+					"1h20m30.13245s",
+					"eHh4eHhjY2NjY2NkZGRkZA==",
+					"2020-04-28T00:00:00Z",
+				},
+			},
+		},
+	}
+	verifyTables(t, csvTable1, tables)
+
+	csvTable2 := `
+#default,_result,,,,,,,,,
+#group,false,false,true,true,false,true,true,false,false,false
+,result,table,_start,_stop,_time,deviceId,sensor,elapsed,note,start
+,,0,2020-04-28T12:36:50.990018157Z,2020-04-28T12:51:50.990018157Z,2020-04-28T12:38:11.480545389Z,1467463,BME280,1m1s,ZGF0YWluYmFzZTY0,2020-04-27T00:00:00Z
+,,0,2020-04-28T12:36:50.990018157Z,2020-04-28T12:51:50.990018157Z,2020-04-28T12:39:36.330153686Z,1467463,BME280,1h20m30.13245s,eHh4eHhjY2NjY2NkZGRkZA==,2020-04-28T00:00:00Z
+`
+	verifyTables(t, csvTable2, tables)
+}
+
+func TestMissingAnnotations(t *testing.T) {
+	csvTable := `
+,result,table,_start,_stop,_time,deviceId,sensor,elapsed,note,start
+,,0,2020-04-28T12:36:50.990018157Z,2020-04-28T12:51:50.990018157Z,2020-04-28T12:38:11.480545389Z,1467463,BME280,1m1s,ZGF0YWluYmFzZTY0,2020-04-27T00:00:00Z
+,,0,2020-04-28T12:36:50.990018157Z,2020-04-28T12:51:50.990018157Z,2020-04-28T12:39:36.330153686Z,1467463,BME280,1h20m30.13245s,eHh4eHhjY2NjY2NkZGRkZA==,2020-04-28T00:00:00Z
+
+`
+	tables := []expectedTable{
+		{ // Table 1
+			columns: []annotatedcsv.Column{
+				{Type: "", Default: nil, Name: "result", Group: false},
+				{Type: "", Default: nil, Name: "table", Group: false},
+				{Type: "", Default: nil, Name: "_start", Group: false},
+				{Type: "", Default: nil, Name: "_stop", Group: false},
+				{Type: "", Default: nil, Name: "_time", Group: false},
+				{Type: "", Default: nil, Name: "deviceId", Group: false},
+				{Type: "", Default: nil, Name: "sensor", Group: false},
+				{Type: "", Default: nil, Name: "elapsed", Group: false},
+				{Type: "", Default: nil, Name: "note", Group: false},
+				{Type: "", Default: nil, Name: "start", Group: false},
+			},
+			rows: [][]interface{}{
+				{
+					"",
+					"0",
+					"2020-04-28T12:36:50.990018157Z",
+					"2020-04-28T12:51:50.990018157Z",
+					"2020-04-28T12:38:11.480545389Z",
+					"1467463",
+					"BME280",
+					"1m1s",
+					"ZGF0YWluYmFzZTY0",
+					"2020-04-27T00:00:00Z",
+				},
+				{
+					"",
+					"0",
+					"2020-04-28T12:36:50.990018157Z",
+					"2020-04-28T12:51:50.990018157Z",
+					"2020-04-28T12:39:36.330153686Z",
+					"1467463",
+					"BME280",
+					"1h20m30.13245s",
+					"eHh4eHhjY2NjY2NkZGRkZA==",
+					"2020-04-28T00:00:00Z",
+				},
+			},
+		},
+	}
+	verifyTables(t, csvTable, tables)
+}
+
+func TestUnexpectedAnnotation(t *testing.T) {
+	csvTable := `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#extra,1,1,1,1,1,1,1,1,1,1
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+
+`
+	tables := []expectedTable{
+		{[]annotatedcsv.Column{
+			{Type: "string", Default: "_result", Name: "result", Group: false},
+			{Type: "long", Default: nil, Name: "table", Group: false},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_start", Group: true},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_stop", Group: true},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_time", Group: false},
+			{Type: "double", Default: nil, Name: "_value", Group: false},
+			{Type: "string", Default: nil, Name: "_field", Group: true},
+			{Type: "string", Default: nil, Name: "_measurement", Group: true},
+			{Type: "string", Default: nil, Name: "a", Group: true},
+			{Type: "string", Default: nil, Name: "b", Group: true},
+		},
+			[][]interface{}{
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T10:34:08.135814545Z"),
+					1.4,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.850214724Z"),
+					6.6,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+			},
+		},
+	}
+
+	verifyTables(t, csvTable, tables)
+}
+
+func TestDatetimeConversion(t *testing.T) {
+	csvTable := `#datatype,string,long,dateTime:RFC3339,dateTime,dateTime:RFC3339Nano,double,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+
+`
+	tables := []expectedTable{
+		{[]annotatedcsv.Column{
+			{Type: "string", Default: "_result", Name: "result", Group: false},
+			{Type: "long", Default: nil, Name: "table", Group: false},
+			{Type: "dateTime:RFC3339", Default: nil, Name: "_start", Group: true},
+			{Type: "dateTime", Default: nil, Name: "_stop", Group: true},
+			{Type: "dateTime:RFC3339Nano", Default: nil, Name: "_time", Group: false},
+			{Type: "double", Default: nil, Name: "_value", Group: false},
+			{Type: "string", Default: nil, Name: "_field", Group: true},
+			{Type: "string", Default: nil, Name: "_measurement", Group: true},
+			{Type: "string", Default: nil, Name: "a", Group: true},
+			{Type: "string", Default: nil, Name: "b", Group: true},
+		},
+			[][]interface{}{
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T10:34:08.135814545Z"),
+					1.4,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.850214724Z"),
+					6.6,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+			},
+		},
+	}
+
+	verifyTables(t, csvTable, tables)
+
+	//invalid datetime layout
+	csvTable = `#datatype,string,long,dateTime:wrongLayout,dateTime,dateTime:RFC3339Nano,double,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+
+`
+	tables = []expectedTable{
+		{[]annotatedcsv.Column{
+			{Type: "string", Default: "_result", Name: "result", Group: false},
+			{Type: "long", Default: nil, Name: "table", Group: false},
+			{Type: "dateTime:wrongLayout", Default: nil, Name: "_start", Group: true},
+			{Type: "dateTime", Default: nil, Name: "_stop", Group: true},
+			{Type: "dateTime:RFC3339Nano", Default: nil, Name: "_time", Group: false},
+			{Type: "double", Default: nil, Name: "_value", Group: false},
+			{Type: "string", Default: nil, Name: "_field", Group: true},
+			{Type: "string", Default: nil, Name: "_measurement", Group: true},
+			{Type: "string", Default: nil, Name: "a", Group: true},
+			{Type: "string", Default: nil, Name: "b", Group: true},
+		},
+			[][]interface{}{
+				{
+					"_result",
+					int64(0),
+					"2020-02-17T22:19:49.747562847Z",
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T10:34:08.135814545Z"),
+					1.4,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(0),
+					"2020-02-17T22:19:49.747562847Z",
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.850214724Z"),
+					6.6,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+			},
+		},
+	}
+
+	verifyTables(t, csvTable, tables)
+
+}
+
+func TestFailedConversion(t *testing.T) {
+	csvTable := `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,zero,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,seven,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,six,f,test,1,adsfasdf
+
+`
+	verifyParsingError(t, csvTable, `cannot convert default value "zero" to type "long": strconv.ParseInt: parsing "zero": invalid syntax`, true)
+
+	csvTable = `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,seven,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,six,f,test,1,adsfasdf
+
+`
+	verifyParsingError(t, csvTable, `cannot convert value "seven" to type "double" at line 5: strconv.ParseFloat: parsing "seven": invalid syntax`, false)
+}
+
+func TestDifferentNumberOfColumns(t *testing.T) {
+	// different #columns in group (8)
+	csvTable := `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,int,string,duration,base64Binary,dateTime:RFC3339
+#group,false,false,true,true,false,true,true,false
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,deviceId,sensor,elapsed,note,start
+,,0,2020-04-28T12:36:50.990018157Z,2020-04-28T12:51:50.990018157Z,2020-04-28T12:38:11.480545389Z,1467463,BME280,1m1s,ZGF0YWluYmFzZTY0,2020-04-27T00:00:00Z
+`
+	verifyParsingError(t, csvTable, "inconsistent table header (got 8 items want 10)", true)
+
+	// different #columns in default (8)
+	csvTable = `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,int,string,duration,base64Binary,dateTime:RFC3339
+#group,false,false,true,true,false,true,true,false,false,true
+#default,_result,,,,,,,
+,result,table,_start,_stop,_time,deviceId,sensor,elapsed,note,start
+,,0,2020-04-28T12:36:50.990018157Z,2020-04-28T12:51:50.990018157Z,2020-04-28T12:38:11.480545389Z,1467463,BME280,1m1s,ZGF0YWluYmFzZTY0,2020-04-27T00:00:00Z
+`
+	verifyParsingError(t, csvTable, "inconsistent table header (got 8 items want 10)", true)
+
+	// different #columns in dataType(10)
+	csvTable = `#default,_result,,,,,,,
+#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,long,string,duration,base64Binary,dateTime:RFC3339
+#group,false,false,true,true,false,true,true,false,false,true,true
+,result,table,_start,_stop,_time,deviceId,sensor,elapsed,note,start
+,,0,2020-04-28T12:36:50.990018157Z,2020-04-28T12:51:50.990018157Z,2020-04-28T12:38:11.480545389Z,1467463,BME280,1m1s,ZGF0YWluYmFzZTY0,2020-04-27T00:00:00Z
+`
+
+	verifyParsingError(t, csvTable, "inconsistent table header (got 10 items want 8)", true)
+
+	// different #columns in data row(8)
+	csvTable = `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,int,string,duration,base64Binary,dateTime:RFC3339
+#group,false,false,true,true,false,true,true,false,true,false
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,deviceId,sensor,elapsed,note,start
+,,0,2020-04-28T12:36:50.990018157Z,2020-04-28T12:51:50.990018157Z,2020-04-28T12:38:11.480545389Z,1467463,BME280,1m1s,ZGF0YWluYmFzZTY0,2020-04-27T00:00:00Z,2345234
+`
+	verifyParsingError(t, csvTable, "inconsistent number of columns at line 5 (got 11 items want 10)", false)
+}
+
+func TestCSVError(t *testing.T) {
+	csvErrTable := `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+,result,"table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+`
+	reader := strings.NewReader(csvErrTable)
+	res := annotatedcsv.NewReader(reader)
+
+	require.False(t, res.NextSection())
+	require.NotNil(t, res.Err())
+
+	csvErrTable = `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,",0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+`
+	reader = strings.NewReader(csvErrTable)
+	res = annotatedcsv.NewReader(reader)
+
+	require.True(t, res.NextSection())
+	require.False(t, res.NextRow())
+	require.NotNil(t, res.Err())
+}
+
+func verifyParsingError(t *testing.T, csvTable, error string, inHeader bool) {
+	reader := strings.NewReader(csvTable)
+	res := annotatedcsv.NewReader(reader)
+
+	if inHeader {
+		require.False(t, res.NextSection())
+	} else {
+		require.True(t, res.NextSection())
+	}
+	require.False(t, res.NextRow())
+	require.NotNil(t, res.Err())
+	assert.Equal(t, error, res.Err().Error())
+}
+
+func verifyTables(t *testing.T, csvTable string, expectedTables []expectedTable) {
+	reader := strings.NewReader(csvTable)
+	res := annotatedcsv.NewReader(reader)
+
+	for _, table := range expectedTables {
+		require.True(t, res.NextSection(), res.Err())
+		require.Nil(t, res.Err())
+		require.Equal(t, table.columns, res.Columns())
+		for _, row := range table.rows {
+			require.True(t, res.NextRow(), res.Err())
+			require.Nil(t, res.Err())
+			for i, v := range res.Row() {
+				if table.columns[i].Type == "base64Binary" {
+					require.Equal(t, row[i], v)
+				} else {
+					require.True(t, row[i] == v, fmt.Sprintf("%v vs %v", row[i], v))
+				}
+			}
+			for i, c := range table.columns {
+				v := res.ValueByName(c.Name)
+				if c.Type == "base64Binary" {
+					require.Equal(t, row[i], v)
+				} else {
+					require.True(t, row[i] == v)
+				}
+			}
+		}
+		require.False(t, res.NextRow(), res.Err())
+		require.Nil(t, res.Err())
+	}
+
+	require.False(t, res.NextSection(), res.Err())
+	require.Nil(t, res.Err())
+
+	require.Nil(t, res.Columns())
+	require.Nil(t, res.Row())
+	require.Nil(t, res.ValueByName("table"))
+}
+
+// ExpectedTable represents table for comparison with parsed tables
+type expectedTable struct {
+	columns []annotatedcsv.Column
+	rows    [][]interface{}
+}
+
+// MustParseTime returns  parsed dateTime in RFC3339 and panics if it fails
+func mustParseTime(s string) time.Time {
+	t, err := time.Parse(time.RFC3339, s)
+	if err != nil {
+		panic(err)
+	}
+	return t
+}
diff --git a/inluxclient/client_test.go b/inluxclient/client_test.go
index 40c7fde..0bf5850 100644
--- a/inluxclient/client_test.go
+++ b/inluxclient/client_test.go
@@ -1,3 +1,7 @@
+// Copyright 2021 InfluxData, Inc. All rights reserved.
+// Use of this source code is governed by MIT
+// license that can be found in the LICENSE file.
+
 package influxclient
 
 import (
diff --git a/inluxclient/query_test.go b/inluxclient/query_test.go
new file mode 100644
index 0000000..584d910
--- /dev/null
+++ b/inluxclient/query_test.go
@@ -0,0 +1,447 @@
+// Copyright 2021 InfluxData, Inc. All rights reserved.
+// Use of this source code is governed by MIT
+// license that can be found in the LICENSE file.
+
+package influxclient_test
+
+import (
+	"fmt"
+	"io/ioutil"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/influxdata/influxdb-client-go/annotatedcsv"
+	influxclient "github.com/influxdata/influxdb-client-go/inluxclient"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestMultiSections(t *testing.T) {
+	csvTableMultiStructure := `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+
+#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,long,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,1,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,4,i,test,1,adsfasdf
+,,1,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,-1,i,test,1,adsfasdf
+
+#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,boolean,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,2,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.62797864Z,false,f,test,0,adsfasdf
+,,2,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.969100374Z,true,f,test,0,adsfasdf
+
+#datatype,string,long,dateTime:RFC3339Nano,dateTime:RFC3339Nano,dateTime:RFC3339Nano,unsignedLong,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,3,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.62797864Z,0,i,test,0,adsfasdf
+,,3,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.969100374Z,2,i,test,0,adsfasdf
+
+`
+	tables := []expectedTable{
+		{ // Table 1
+			columns: []annotatedcsv.Column{
+				{Type: "string", Default: "_result", Name: "result", Group: false},
+				{Type: "long", Default: nil, Name: "table", Group: false},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_start", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_stop", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_time", Group: false},
+				{Type: "double", Default: nil, Name: "_value", Group: false},
+				{Type: "string", Default: nil, Name: "_field", Group: true},
+				{Type: "string", Default: nil, Name: "_measurement", Group: true},
+				{Type: "string", Default: nil, Name: "a", Group: true},
+				{Type: "string", Default: nil, Name: "b", Group: true},
+			},
+			rows: [][]interface{}{
+				{"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T10:34:08.135814545Z"),
+					1.4,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(0),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.850214724Z"),
+					6.6,
+					"f",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+			},
+		},
+		{ //Table 2
+			columns: []annotatedcsv.Column{
+				{Type: "string", Default: "_result", Name: "result", Group: false},
+				{Type: "long", Default: nil, Name: "table", Group: false},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_start", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_stop", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_time", Group: false},
+				{Type: "long", Default: nil, Name: "_value", Group: false},
+				{Type: "string", Default: nil, Name: "_field", Group: true},
+				{Type: "string", Default: nil, Name: "_measurement", Group: true},
+				{Type: "string", Default: nil, Name: "a", Group: true},
+				{Type: "string", Default: nil, Name: "b", Group: true},
+			},
+			rows: [][]interface{}{
+				{"_result",
+					int64(1),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T10:34:08.135814545Z"),
+					int64(4),
+					"i",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(1),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.850214724Z"),
+					int64(-1),
+					"i",
+					"test",
+					"1",
+					"adsfasdf",
+				},
+			},
+		},
+		{ // Table 3
+			columns: []annotatedcsv.Column{
+				{Type: "string", Default: "_result", Name: "result", Group: false},
+				{Type: "long", Default: nil, Name: "table", Group: false},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_start", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_stop", Group: true},
+				{Type: "dateTime:RFC3339", Default: nil, Name: "_time", Group: false},
+				{Type: "boolean", Default: nil, Name: "_value", Group: false},
+				{Type: "string", Default: nil, Name: "_field", Group: true},
+				{Type: "string", Default: nil, Name: "_measurement", Group: true},
+				{Type: "string", Default: nil, Name: "a", Group: true},
+				{Type: "string", Default: nil, Name: "b", Group: true},
+			},
+			rows: [][]interface{}{
+				{"_result",
+					int64(2),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.62797864Z"),
+					false,
+					"f",
+					"test",
+					"0",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(2),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.969100374Z"),
+					true,
+					"f",
+					"test",
+					"0",
+					"adsfasdf",
+				},
+			},
+		},
+		{ //Table 4
+			columns: []annotatedcsv.Column{
+				{Type: "string", Default: "_result", Name: "result", Group: false},
+				{Type: "long", Default: nil, Name: "table", Group: false},
+				{Type: "dateTime:RFC3339Nano", Default: nil, Name: "_start", Group: true},
+				{Type: "dateTime:RFC3339Nano", Default: nil, Name: "_stop", Group: true},
+				{Type: "dateTime:RFC3339Nano", Default: nil, Name: "_time", Group: false},
+				{Type: "unsignedLong", Default: nil, Name: "_value", Group: false},
+				{Type: "string", Default: nil, Name: "_field", Group: true},
+				{Type: "string", Default: nil, Name: "_measurement", Group: true},
+				{Type: "string", Default: nil, Name: "a", Group: true},
+				{Type: "string", Default: nil, Name: "b", Group: true},
+			},
+			rows: [][]interface{}{
+				{"_result",
+					int64(3),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.62797864Z"),
+					uint64(0),
+					"i",
+					"test",
+					"0",
+					"adsfasdf",
+				},
+				{
+					"_result",
+					int64(3),
+					mustParseTime("2020-02-17T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:19:49.747562847Z"),
+					mustParseTime("2020-02-18T22:08:44.969100374Z"),
+					uint64(2),
+					"i",
+					"test",
+					"0",
+					"adsfasdf",
+				},
+			},
+		},
+	}
+
+	//verify full content
+	verifyTables(t, csvTableMultiStructure, tables)
+
+	// test advancing
+	reader := strings.NewReader(csvTableMultiStructure)
+	res := influxclient.NewQueryResultReader(ioutil.NopCloser(reader))
+
+	//test skip first table header
+	require.True(t, res.NextRow())
+	require.Nil(t, res.Err())
+	require.Equal(t, tables[0].rows[0], res.Row())
+	require.Nil(t, res.Close())
+
+	reader = strings.NewReader(csvTableMultiStructure)
+	res = influxclient.NewQueryResultReader(ioutil.NopCloser(reader))
+
+	//test skip tables
+	require.True(t, res.NextSection())
+	require.Nil(t, res.Err())
+	require.True(t, res.NextSection())
+	require.Nil(t, res.Err())
+	require.True(t, res.NextRow())
+	require.Nil(t, res.Err())
+	require.True(t, res.NextSection())
+	require.Nil(t, res.Err())
+	require.True(t, res.NextRow())
+	require.Nil(t, res.Err())
+	require.Equal(t, tables[2].rows[0], res.Row())
+
+	require.Nil(t, res.Close())
+
+	csvTableMultiTables := `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+#group,false,false,true,true,false,false,true,true,true,true
+#default,_result,,,,,,,,,
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+,,1,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,4.3,i,test,1,xyxyxyxy
+,,1,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,-1.2,i,test,1,xyxyxyxy
+,,2,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.62797864Z,0.1,f,test,0,adsfasdf
+,,2,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.969100374Z,0.3,f,test,0,adsfasdf
+,,3,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.62797864Z,10,i,test,0,xyxyxyxy
+,,3,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.969100374Z,2,i,test,0,xyxyxyxy
+
+`
+	reader = strings.NewReader(csvTableMultiTables)
+	res = influxclient.NewQueryResultReader(ioutil.NopCloser(reader))
+
+	//test skip first table header
+	require.True(t, res.NextRow())
+	require.True(t, res.NextRow())
+	require.Nil(t, res.Err())
+	require.Equal(t, tables[0].rows[1], res.Row())
+	require.Nil(t, res.Close())
+
+	// skip sections
+	reader = strings.NewReader(csvTableMultiTables)
+	res = influxclient.NewQueryResultReader(ioutil.NopCloser(reader))
+	require.True(t, res.NextSection())
+	require.Nil(t, res.Err())
+	require.False(t, res.NextSection())
+	require.Nil(t, res.Err())
+	require.Nil(t, res.Close())
+}
+
+func TestErrorInRow(t *testing.T) {
+	csvTableError := `#datatype,string,long
+#group,true,true
+#default,,
+,error,reference
+,failed to create physical plan: invalid time bounds from procedure from: bounds contain zero time,897
+
+`
+	verifyParsingError(t, csvTableError, "flux query error (code 897): failed to create physical plan: invalid time bounds from procedure from: bounds contain zero time", true)
+
+	csvTableErrorNoReference := `#datatype,string,long
+#group,true,true
+#default,,
+,error,reference
+,failed to create physical plan: invalid time bounds from procedure from: bounds contain zero time,
+
+`
+	verifyParsingError(t, csvTableErrorNoReference, "no row found in error section", true)
+
+	csvTableErrorNoMessage := `#datatype,string,long
+#group,true,true
+#default,,
+,error,reference
+,,
+
+`
+	verifyParsingError(t, csvTableErrorNoMessage, "no row found in error section", true)
+
+	csvTableErrorNoRows := `#datatype,string,string
+#group,true,true
+#default,,
+,error,reference
+
+`
+	verifyParsingError(t, csvTableErrorNoRows, "no row found in error section", true)
+
+	csvTableErrorInvalidReferenceType := `#datatype,string,string
+#group,true,true
+#default,,
+,error,reference
+,failed to create physical plan: invalid time bounds from procedure from: bounds contain zero time,897
+
+`
+	verifyParsingError(t, csvTableErrorInvalidReferenceType, "unexpected column types (string, string) in error section", true)
+
+	csvTableErrorNoColumnReference := `#datatype,string
+#group,true
+#default,
+,error
+,failed to create physical plan: invalid time bounds from procedure from: bounds contain zero time
+
+`
+	tables := []expectedTable{
+		{ // Table 1
+			[]annotatedcsv.Column{
+				{Type: "string", Default: nil, Name: "error", Group: true},
+			},
+			[][]interface{}{
+				{
+					"failed to create physical plan: invalid time bounds from procedure from: bounds contain zero time",
+				},
+			},
+		},
+	}
+	verifyTables(t, csvTableErrorNoColumnReference, tables)
+}
+
+func TestCSVError(t *testing.T) {
+	csvErrInHeader := `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+,result,"table,_start,_stop,_time,_value,_field,_measurement,a,b
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+
+`
+	reader := strings.NewReader(csvErrInHeader)
+	res := influxclient.NewQueryResultReader(ioutil.NopCloser(reader))
+
+	require.False(t, res.NextSection())
+	require.NotNil(t, res.Err())
+	require.Nil(t, res.Close())
+
+	reader = strings.NewReader(csvErrInHeader)
+	res = influxclient.NewQueryResultReader(ioutil.NopCloser(reader))
+	//straight to error
+	require.False(t, res.NextRow())
+	require.NotNil(t, res.Err())
+	require.Nil(t, res.Close())
+
+	csvErrInRow := `#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string
+,result,table,_start,_stop,_time,_value,_field,_measurement,a,b
+,",0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T10:34:08.135814545Z,1.4,f,test,1,adsfasdf
+,,0,2020-02-17T22:19:49.747562847Z,2020-02-18T22:19:49.747562847Z,2020-02-18T22:08:44.850214724Z,6.6,f,test,1,adsfasdf
+
+`
+	reader = strings.NewReader(csvErrInRow)
+	res = influxclient.NewQueryResultReader(ioutil.NopCloser(reader))
+
+	require.True(t, res.NextSection())
+	require.False(t, res.NextRow())
+	require.NotNil(t, res.Err())
+	require.Nil(t, res.Close())
+}
+
+func verifyParsingError(t *testing.T, csvTable, error string, inHeader bool) {
+	reader := strings.NewReader(csvTable)
+	res := influxclient.NewQueryResultReader(ioutil.NopCloser(reader))
+
+	if inHeader {
+		require.False(t, res.NextSection())
+		//repeatedly
+		require.False(t, res.NextSection())
+	} else {
+		require.True(t, res.NextSection())
+	}
+	require.False(t, res.NextRow())
+	require.NotNil(t, res.Err())
+	assert.Equal(t, error, res.Err().Error())
+	require.Nil(t, res.Close())
+}
+
+func verifyTables(t *testing.T, csvTable string, expectedTables []expectedTable) {
+	reader := strings.NewReader(csvTable)
+	res := influxclient.NewQueryResultReader(ioutil.NopCloser(reader))
+
+	for _, table := range expectedTables {
+		require.True(t, res.NextSection(), res.Err())
+		require.Nil(t, res.Err())
+		require.Equal(t, table.columns, res.Columns())
+		for _, row := range table.rows {
+			require.True(t, res.NextRow(), res.Err())
+			require.Nil(t, res.Err())
+			for i, v := range res.Row() {
+				if table.columns[i].Type == "base64Binary" {
+					require.Equal(t, row[i], v)
+				} else {
+					require.True(t, row[i] == v, fmt.Sprintf("%v vs %v", row[i], v))
+				}
+			}
+			for i, c := range table.columns {
+				v := res.ValueByName(c.Name)
+				if c.Type == "base64Binary" {
+					require.Equal(t, row[i], v)
+				} else {
+					require.True(t, row[i] == v)
+				}
+			}
+		}
+		require.False(t, res.NextRow(), res.Err())
+		require.Nil(t, res.Err())
+	}
+
+	require.False(t, res.NextSection(), res.Err())
+	require.Nil(t, res.Err())
+
+	require.Nil(t, res.Columns())
+	require.Nil(t, res.Row())
+	require.Nil(t, res.ValueByName("table"))
+	require.Nil(t, res.Close())
+}
+
+// ExpectedTable represents table for comparison with parsed tables
+type expectedTable struct {
+	columns []annotatedcsv.Column
+	rows    [][]interface{}
+}
+
+// MustParseTime returns  parsed dateTime in RFC3339 and panics if it fails
+func mustParseTime(s string) time.Time {
+	t, err := time.Parse(time.RFC3339, s)
+	if err != nil {
+		panic(err)
+	}
+	return t
+}
diff --git a/internal/csv/reader_test.go b/internal/csv/reader_test.go
new file mode 100644
index 0000000..6c9bffe
--- /dev/null
+++ b/internal/csv/reader_test.go
@@ -0,0 +1,541 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package csv
+
+import (
+	"io"
+	"reflect"
+	"strings"
+	"testing"
+	"unicode/utf8"
+)
+
+type readTest struct {
+	Name   string
+	Input  string
+	Output [][]string
+	Lines  []int // If this is nil, it's assumed to be 1, 2, etc.
+	Error  error
+
+	// These fields are copied into the Reader
+	Comma              rune
+	Comment            rune
+	UseFieldsPerRecord bool // false (default) means FieldsPerRecord is -1
+	FieldsPerRecord    int
+	LazyQuotes         bool
+	TrimLeadingSpace   bool
+	ReuseRecord        bool
+}
+
+var readTests = []readTest{{
+	Name:   "Simple",
+	Input:  "a,b,c\n",
+	Output: [][]string{{"a", "b", "c"}},
+}, {
+	Name:   "CRLF",
+	Input:  "a,b\r\nc,d\r\n",
+	Output: [][]string{{"a", "b"}, {"c", "d"}},
+}, {
+	Name:   "BareCR",
+	Input:  "a,b\rc,d\r\n",
+	Output: [][]string{{"a", "b\rc", "d"}},
+}, {
+	Name: "RFC4180test",
+	Input: `#field1,field2,field3
+"aaa","bb
+b","ccc"
+"a,a","b""bb","ccc"
+zzz,yyy,xxx
+`,
+	Output: [][]string{
+		{"#field1", "field2", "field3"},
+		{"aaa", "bb\nb", "ccc"},
+		{"a,a", `b"bb`, "ccc"},
+		{"zzz", "yyy", "xxx"},
+	},
+	Lines:              []int{1, 2, 4, 5},
+	UseFieldsPerRecord: true,
+	FieldsPerRecord:    0,
+}, {
+	Name:   "NoEOLTest",
+	Input:  "a,b,c",
+	Output: [][]string{{"a", "b", "c"}},
+}, {
+	Name:   "Semicolon",
+	Input:  "a;b;c\n",
+	Output: [][]string{{"a", "b", "c"}},
+	Comma:  ';',
+}, {
+	Name: "MultiLine",
+	Input: `"two
+line","one line","three
+line
+field"`,
+	Output: [][]string{{"two\nline", "one line", "three\nline\nfield"}},
+}, {
+	Name:  "BlankLine",
+	Input: "a,b,c\n\nd,e,f\n\n",
+	Output: [][]string{
+		{"a", "b", "c"},
+		{"d", "e", "f"},
+	},
+	Lines: []int{1, 3},
+}, {
+	Name:  "BlankLineFieldCount",
+	Input: "a,b,c\n\nd,e,f\n\n",
+	Output: [][]string{
+		{"a", "b", "c"},
+		{"d", "e", "f"},
+	},
+	UseFieldsPerRecord: true,
+	FieldsPerRecord:    0,
+	Lines:              []int{1, 3},
+}, {
+	Name:             "TrimSpace",
+	Input:            " a,  b,   c\n",
+	Output:           [][]string{{"a", "b", "c"}},
+	TrimLeadingSpace: true,
+}, {
+	Name:   "LeadingSpace",
+	Input:  " a,  b,   c\n",
+	Output: [][]string{{" a", "  b", "   c"}},
+}, {
+	Name:    "Comment",
+	Input:   "#1,2,3\na,b,c\n#comment",
+	Output:  [][]string{{"a", "b", "c"}},
+	Comment: '#',
+	Lines:   []int{2},
+}, {
+	Name:   "NoComment",
+	Input:  "#1,2,3\na,b,c",
+	Output: [][]string{{"#1", "2", "3"}, {"a", "b", "c"}},
+}, {
+	Name:       "LazyQuotes",
+	Input:      `a "word","1"2",a","b`,
+	Output:     [][]string{{`a "word"`, `1"2`, `a"`, `b`}},
+	LazyQuotes: true,
+}, {
+	Name:       "BareQuotes",
+	Input:      `a "word","1"2",a"`,
+	Output:     [][]string{{`a "word"`, `1"2`, `a"`}},
+	LazyQuotes: true,
+}, {
+	Name:       "BareDoubleQuotes",
+	Input:      `a""b,c`,
+	Output:     [][]string{{`a""b`, `c`}},
+	LazyQuotes: true,
+}, {
+	Name:  "BadDoubleQuotes",
+	Input: `a""b,c`,
+	Error: &ParseError{StartLine: 1, Line: 1, Column: 1, Err: ErrBareQuote},
+}, {
+	Name:             "TrimQuote",
+	Input:            ` "a"," b",c`,
+	Output:           [][]string{{"a", " b", "c"}},
+	TrimLeadingSpace: true,
+}, {
+	Name:  "BadBareQuote",
+	Input: `a "word","b"`,
+	Error: &ParseError{StartLine: 1, Line: 1, Column: 2, Err: ErrBareQuote},
+}, {
+	Name:  "BadTrailingQuote",
+	Input: `"a word",b"`,
+	Error: &ParseError{StartLine: 1, Line: 1, Column: 10, Err: ErrBareQuote},
+}, {
+	Name:  "ExtraneousQuote",
+	Input: `"a "word","b"`,
+	Error: &ParseError{StartLine: 1, Line: 1, Column: 3, Err: ErrQuote},
+}, {
+	Name:               "BadFieldCount",
+	Input:              "a,b,c\nd,e",
+	Error:              &ParseError{StartLine: 2, Line: 2, Err: ErrFieldCount},
+	UseFieldsPerRecord: true,
+	FieldsPerRecord:    0,
+}, {
+	Name:               "BadFieldCount1",
+	Input:              `a,b,c`,
+	Error:              &ParseError{StartLine: 1, Line: 1, Err: ErrFieldCount},
+	UseFieldsPerRecord: true,
+	FieldsPerRecord:    2,
+}, {
+	Name:   "FieldCount",
+	Input:  "a,b,c\nd,e",
+	Output: [][]string{{"a", "b", "c"}, {"d", "e"}},
+}, {
+	Name:   "TrailingCommaEOF",
+	Input:  "a,b,c,",
+	Output: [][]string{{"a", "b", "c", ""}},
+}, {
+	Name:   "TrailingCommaEOL",
+	Input:  "a,b,c,\n",
+	Output: [][]string{{"a", "b", "c", ""}},
+}, {
+	Name:             "TrailingCommaSpaceEOF",
+	Input:            "a,b,c, ",
+	Output:           [][]string{{"a", "b", "c", ""}},
+	TrimLeadingSpace: true,
+}, {
+	Name:             "TrailingCommaSpaceEOL",
+	Input:            "a,b,c, \n",
+	Output:           [][]string{{"a", "b", "c", ""}},
+	TrimLeadingSpace: true,
+}, {
+	Name:             "TrailingCommaLine3",
+	Input:            "a,b,c\nd,e,f\ng,hi,",
+	Output:           [][]string{{"a", "b", "c"}, {"d", "e", "f"}, {"g", "hi", ""}},
+	TrimLeadingSpace: true,
+}, {
+	Name:   "NotTrailingComma3",
+	Input:  "a,b,c, \n",
+	Output: [][]string{{"a", "b", "c", " "}},
+}, {
+	Name: "CommaFieldTest",
+	Input: `x,y,z,w
+x,y,z,
+x,y,,
+x,,,
+,,,
+"x","y","z","w"
+"x","y","z",""
+"x","y","",""
+"x","","",""
+"","","",""
+`,
+	Output: [][]string{
+		{"x", "y", "z", "w"},
+		{"x", "y", "z", ""},
+		{"x", "y", "", ""},
+		{"x", "", "", ""},
+		{"", "", "", ""},
+		{"x", "y", "z", "w"},
+		{"x", "y", "z", ""},
+		{"x", "y", "", ""},
+		{"x", "", "", ""},
+		{"", "", "", ""},
+	},
+}, {
+	Name:  "TrailingCommaIneffective1",
+	Input: "a,b,\nc,d,e",
+	Output: [][]string{
+		{"a", "b", ""},
+		{"c", "d", "e"},
+	},
+	TrimLeadingSpace: true,
+}, {
+	Name:  "ReadAllReuseRecord",
+	Input: "a,b\nc,d",
+	Output: [][]string{
+		{"a", "b"},
+		{"c", "d"},
+	},
+	ReuseRecord: true,
+}, {
+	Name:  "StartLine1", // Issue 19019
+	Input: "a,\"b\nc\"d,e",
+	Error: &ParseError{StartLine: 1, Line: 2, Column: 1, Err: ErrQuote},
+}, {
+	Name:  "StartLine2",
+	Input: "a,b\n\"d\n\n,e",
+	Error: &ParseError{StartLine: 2, Line: 5, Column: 0, Err: ErrQuote},
+}, {
+	Name:  "CRLFInQuotedField", // Issue 21201
+	Input: "A,\"Hello\r\nHi\",B\r\n",
+	Output: [][]string{
+		{"A", "Hello\nHi", "B"},
+	},
+}, {
+	Name:   "BinaryBlobField", // Issue 19410
+	Input:  "x09\x41\xb4\x1c,aktau",
+	Output: [][]string{{"x09A\xb4\x1c", "aktau"}},
+}, {
+	Name:   "TrailingCR",
+	Input:  "field1,field2\r",
+	Output: [][]string{{"field1", "field2"}},
+}, {
+	Name:   "QuotedTrailingCR",
+	Input:  "\"field\"\r",
+	Output: [][]string{{"field"}},
+}, {
+	Name:  "QuotedTrailingCRCR",
+	Input: "\"field\"\r\r",
+	Error: &ParseError{StartLine: 1, Line: 1, Column: 6, Err: ErrQuote},
+}, {
+	Name:   "FieldCR",
+	Input:  "field\rfield\r",
+	Output: [][]string{{"field\rfield"}},
+}, {
+	Name:   "FieldCRCR",
+	Input:  "field\r\rfield\r\r",
+	Output: [][]string{{"field\r\rfield\r"}},
+}, {
+	Name:   "FieldCRCRLF",
+	Input:  "field\r\r\nfield\r\r\n",
+	Output: [][]string{{"field\r"}, {"field\r"}},
+}, {
+	Name:   "FieldCRCRLFCR",
+	Input:  "field\r\r\n\rfield\r\r\n\r",
+	Output: [][]string{{"field\r"}, {"\rfield\r"}},
+}, {
+	Name:   "FieldCRCRLFCRCR",
+	Input:  "field\r\r\n\r\rfield\r\r\n\r\r",
+	Output: [][]string{{"field\r"}, {"\r\rfield\r"}, {"\r"}},
+}, {
+	Name:  "MultiFieldCRCRLFCRCR",
+	Input: "field1,field2\r\r\n\r\rfield1,field2\r\r\n\r\r,",
+	Output: [][]string{
+		{"field1", "field2\r"},
+		{"\r\rfield1", "field2\r"},
+		{"\r\r", ""},
+	},
+}, {
+	Name:             "NonASCIICommaAndComment",
+	Input:            "a£b,c£ \td,e\n€ comment\n",
+	Output:           [][]string{{"a", "b,c", "d,e"}},
+	TrimLeadingSpace: true,
+	Comma:            '£',
+	Comment:          '€',
+}, {
+	Name:    "NonASCIICommaAndCommentWithQuotes",
+	Input:   "a€\"  b,\"€ c\nλ comment\n",
+	Output:  [][]string{{"a", "  b,", " c"}},
+	Comma:   '€',
+	Comment: 'λ',
+}, {
+	// λ and θ start with the same byte.
+	// This tests that the parser doesn't confuse such characters.
+	Name:    "NonASCIICommaConfusion",
+	Input:   "\"abθcd\"λefθgh",
+	Output:  [][]string{{"abθcd", "efθgh"}},
+	Comma:   'λ',
+	Comment: '€',
+}, {
+	Name:    "NonASCIICommentConfusion",
+	Input:   "λ\nλ\nθ\nλ\n",
+	Output:  [][]string{{"λ"}, {"λ"}, {"λ"}},
+	Comment: 'θ',
+	Lines:   []int{1, 2, 4},
+}, {
+	Name:   "QuotedFieldMultipleLF",
+	Input:  "\"\n\n\n\n\"",
+	Output: [][]string{{"\n\n\n\n"}},
+}, {
+	Name:  "MultipleCRLF",
+	Input: "\r\n\r\n\r\n\r\n",
+}, {
+	// The implementation may read each line in several chunks if it doesn't fit entirely
+	// in the read buffer, so we should test the code to handle that condition.
+	Name:    "HugeLines",
+	Input:   strings.Repeat("#ignore\n", 10000) + strings.Repeat("@", 5000) + "," + strings.Repeat("*", 5000),
+	Output:  [][]string{{strings.Repeat("@", 5000), strings.Repeat("*", 5000)}},
+	Comment: '#',
+	Lines:   []int{10001},
+}, {
+	Name:  "QuoteWithTrailingCRLF",
+	Input: "\"foo\"bar\"\r\n",
+	Error: &ParseError{StartLine: 1, Line: 1, Column: 4, Err: ErrQuote},
+}, {
+	Name:       "LazyQuoteWithTrailingCRLF",
+	Input:      "\"foo\"bar\"\r\n",
+	Output:     [][]string{{`foo"bar`}},
+	LazyQuotes: true,
+}, {
+	Name:   "DoubleQuoteWithTrailingCRLF",
+	Input:  "\"foo\"\"bar\"\r\n",
+	Output: [][]string{{`foo"bar`}},
+}, {
+	Name:   "EvenQuotes",
+	Input:  `""""""""`,
+	Output: [][]string{{`"""`}},
+}, {
+	Name:  "OddQuotes",
+	Input: `"""""""`,
+	Error: &ParseError{StartLine: 1, Line: 1, Column: 7, Err: ErrQuote},
+}, {
+	Name:       "LazyOddQuotes",
+	Input:      `"""""""`,
+	Output:     [][]string{{`"""`}},
+	LazyQuotes: true,
+}, {
+	Name:  "BadComma1",
+	Comma: '\n',
+	Error: errInvalidDelim,
+}, {
+	Name:  "BadComma2",
+	Comma: '\r',
+	Error: errInvalidDelim,
+}, {
+	Name:  "BadComma3",
+	Comma: '"',
+	Error: errInvalidDelim,
+}, {
+	Name:  "BadComma4",
+	Comma: utf8.RuneError,
+	Error: errInvalidDelim,
+}, {
+	Name:    "BadComment1",
+	Comment: '\n',
+	Error:   errInvalidDelim,
+}, {
+	Name:    "BadComment2",
+	Comment: '\r',
+	Error:   errInvalidDelim,
+}, {
+	Name:    "BadComment3",
+	Comment: utf8.RuneError,
+	Error:   errInvalidDelim,
+}, {
+	Name:    "BadCommaComment",
+	Comma:   'X',
+	Comment: 'X',
+	Error:   errInvalidDelim,
+}}
+
+func TestRead(t *testing.T) {
+	newReader := func(tt readTest) *Reader {
+		r := NewReader(strings.NewReader(tt.Input))
+
+		if tt.Comma != 0 {
+			r.Comma = tt.Comma
+		}
+		r.Comment = tt.Comment
+		if tt.UseFieldsPerRecord {
+			r.FieldsPerRecord = tt.FieldsPerRecord
+		} else {
+			r.FieldsPerRecord = -1
+		}
+		r.LazyQuotes = tt.LazyQuotes
+		r.TrimLeadingSpace = tt.TrimLeadingSpace
+		r.ReuseRecord = tt.ReuseRecord
+		return r
+	}
+
+	for _, tt := range readTests {
+		t.Run(tt.Name, func(t *testing.T) {
+			r := newReader(tt)
+			out, err := r.ReadAll()
+			if !reflect.DeepEqual(err, tt.Error) {
+				t.Errorf("ReadAll() error:\ngot  %v\nwant %v", err, tt.Error)
+			} else if !reflect.DeepEqual(out, tt.Output) {
+				t.Errorf("ReadAll() output:\ngot  %q\nwant %q", out, tt.Output)
+			}
+
+			// Check line numbers:
+			r = newReader(tt)
+			for recNum := 0; recNum < len(tt.Output); recNum++ {
+				rec, err := r.Read()
+				if err != nil {
+					break
+				}
+				if got, want := rec, tt.Output[recNum]; !reflect.DeepEqual(got, want) {
+					t.Errorf("Read vs ReadAll mismatch;\ngot %q\nwant %q", got, want)
+				}
+				wantLine := recNum + 1
+				if tt.Lines != nil {
+					wantLine = tt.Lines[recNum]
+				}
+				if gotLine := r.Line(); gotLine != wantLine {
+					t.Errorf("unexpected line number at record %d; got %d want %d", recNum, gotLine, wantLine)
+				}
+			}
+		})
+	}
+}
+
+// nTimes is an io.Reader which yields the string s n times.
+type nTimes struct {
+	s   string
+	n   int
+	off int
+}
+
+func (r *nTimes) Read(p []byte) (n int, err error) {
+	for {
+		if r.n <= 0 || r.s == "" {
+			return n, io.EOF
+		}
+		n0 := copy(p, r.s[r.off:])
+		p = p[n0:]
+		n += n0
+		r.off += n0
+		if r.off == len(r.s) {
+			r.off = 0
+			r.n--
+		}
+		if len(p) == 0 {
+			return
+		}
+	}
+}
+
+// benchmarkRead measures reading the provided CSV rows data.
+// initReader, if non-nil, modifies the Reader before it's used.
+func benchmarkRead(b *testing.B, initReader func(*Reader), rows string) {
+	b.ReportAllocs()
+	r := NewReader(&nTimes{s: rows, n: b.N})
+	if initReader != nil {
+		initReader(r)
+	}
+	for {
+		_, err := r.Read()
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			b.Fatal(err)
+		}
+	}
+}
+
+const benchmarkCSVData = `x,y,z,w
+x,y,z,
+x,y,,
+x,,,
+,,,
+"x","y","z","w"
+"x","y","z",""
+"x","y","",""
+"x","","",""
+"","","",""
+`
+
+func BenchmarkRead(b *testing.B) {
+	benchmarkRead(b, nil, benchmarkCSVData)
+}
+
+func BenchmarkReadWithFieldsPerRecord(b *testing.B) {
+	benchmarkRead(b, func(r *Reader) { r.FieldsPerRecord = 4 }, benchmarkCSVData)
+}
+
+func BenchmarkReadWithoutFieldsPerRecord(b *testing.B) {
+	benchmarkRead(b, func(r *Reader) { r.FieldsPerRecord = -1 }, benchmarkCSVData)
+}
+
+func BenchmarkReadLargeFields(b *testing.B) {
+	benchmarkRead(b, nil, strings.Repeat(`xxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+xxxxxxxxxxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvv
+,,zzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+`, 3))
+}
+
+func BenchmarkReadReuseRecord(b *testing.B) {
+	benchmarkRead(b, func(r *Reader) { r.ReuseRecord = true }, benchmarkCSVData)
+}
+
+func BenchmarkReadReuseRecordWithFieldsPerRecord(b *testing.B) {
+	benchmarkRead(b, func(r *Reader) { r.ReuseRecord = true; r.FieldsPerRecord = 4 }, benchmarkCSVData)
+}
+
+func BenchmarkReadReuseRecordWithoutFieldsPerRecord(b *testing.B) {
+	benchmarkRead(b, func(r *Reader) { r.ReuseRecord = true; r.FieldsPerRecord = -1 }, benchmarkCSVData)
+}
+
+func BenchmarkReadReuseRecordLargeFields(b *testing.B) {
+	benchmarkRead(b, func(r *Reader) { r.ReuseRecord = true }, strings.Repeat(`xxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+xxxxxxxxxxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvv
+,,zzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww,vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
+`, 3))
+}