Repository: pingcap/dumpling
Pull request #18 (2019-12-28 12:10:29)

bugfix: make RowIter.HasNext idempotent
The method `HasNext()` of the interface `RowIter` should have no side effect.

Changes:
- reimplement `rowIter`

codecov-io: # [Codecov](https://codecov.io/gh/pingcap/dumpling/pull/18?src=pr&el=h1) Report
> Merging [#18](https://codecov.io/gh/pingcap/dumpling/pull/18?src=pr&el=desc) into [master](https://codecov.io/gh/pingcap/dumpling/commit/75dc4054f422275424ddead7afb95ee4b759b708?src=pr&el=desc) will **decrease** coverage by `0.51%`.
> The diff coverage is `0%`.


```diff
@@            Coverage Diff             @@
##           master      #18      +/-   ##
==========================================
- Coverage   39.19%   38.67%   -0.52%     
==========================================
  Files          10       10              
  Lines         523      530       +7     
==========================================
  Hits          205      205              
- Misses        299      306       +7     
  Partials       19       19
```


Patch:
diff --git a/v4/export/ir_impl.go b/v4/export/ir_impl.go
index 0c627c7..5712831 100644
--- a/v4/export/ir_impl.go
+++ b/v4/export/ir_impl.go
@@ -5,17 +5,31 @@ import (
 )
 
 // rowIter implements the SQLRowIter interface.
+// Note: To create a rowIter, please use `newRowIter()` instead of struct literal.
 type rowIter struct {
-	rows *sql.Rows
-	args []interface{}
+	rows    *sql.Rows
+	hasNext bool
+	args    []interface{}
+}
+
+func newRowIter(rows *sql.Rows, argLen int) *rowIter {
+	r := &rowIter{
+		rows:    rows,
+		hasNext: false,
+		args:    make([]interface{}, argLen),
+	}
+	r.hasNext = r.rows.Next()
+	return r
 }
 
 func (iter *rowIter) Next(row RowReceiver) error {
-	return decodeFromRows(iter.rows, iter.args, row)
+	err := decodeFromRows(iter.rows, iter.args, row)
+	iter.hasNext = iter.rows.Next()
+	return err
 }
 
 func (iter *rowIter) HasNext() bool {
-	return iter.rows.Next()
+	return iter.hasNext
 }
 
 type sizedRowIter struct {
@@ -94,10 +108,7 @@ func (td *tableData) ColumnCount() uint {
 }
 
 func (td *tableData) Rows() SQLRowIter {
-	return &rowIter{
-		rows: td.rows,
-		args: make([]interface{}, len(td.colTypes)),
-	}
+	return newRowIter(td.rows, len(td.colTypes))
 }
 
 func (td *tableData) SpecialComments() StringIter {


Test patch:
diff --git a/v4/export/ir_impl_test.go b/v4/export/ir_impl_test.go
new file mode 100644
index 0000000..2910a5d
--- /dev/null
+++ b/v4/export/ir_impl_test.go
@@ -0,0 +1,40 @@
+package export
+
+import (
+	"github.com/DATA-DOG/go-sqlmock"
+	. "github.com/pingcap/check"
+)
+
+var _ = Suite(&testIRImplSuite{})
+
+type testIRImplSuite struct{}
+
+func (s *testIRImplSuite) TestRowIter(c *C) {
+	db, mock, err := sqlmock.New()
+	c.Assert(err, IsNil)
+	defer db.Close()
+
+	expectedRows := mock.NewRows([]string{"id"}).
+		AddRow("1").
+		AddRow("2").
+		AddRow("3")
+	mock.ExpectQuery("SELECT id from t").WillReturnRows(expectedRows)
+	rows, err := db.Query("SELECT id from t")
+	c.Assert(err, IsNil)
+
+	iter := newRowIter(rows, 1)
+	for i := 0; i < 100; i += 1 {
+		c.Assert(iter.HasNext(), IsTrue)
+	}
+	res := make(dumplingRow, 1)
+	c.Assert(iter.Next(res), IsNil)
+	c.Assert(res[0].String, Equals, "1")
+	c.Assert(iter.HasNext(), IsTrue)
+	c.Assert(iter.HasNext(), IsTrue)
+	c.Assert(iter.Next(res), IsNil)
+	c.Assert(res[0].String, Equals, "2")
+	c.Assert(iter.HasNext(), IsTrue)
+	c.Assert(iter.Next(res), IsNil)
+	c.Assert(res[0].String, Equals, "3")
+	c.Assert(iter.HasNext(), IsFalse)
+}