Skip to content

Commit b7c545b

Browse files
authored
Fix for panic in rows.Next() when stored procedures emit NOTICE messages (#188)
1 parent cccffde commit b7c545b

3 files changed

Lines changed: 167 additions & 2 deletions

File tree

driver_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,6 +1660,128 @@ func TestInvalidEmailParseStatement(t *testing.T) {
16601660
assertEqual(t, all_roles, role)
16611661
}
16621662
}
1663+
1664+
// TestStoredProcedureWithNotice verifies that rows.Next() does not panic when a
1665+
// stored procedure raises one or more RAISE NOTICE messages before returning its
1666+
// result set.
1667+
//
1668+
// Root cause: Vertica sends a correct RowDescription (2 cols: a, b) for the CALL
1669+
// result, then emits NoticeResponse messages, then sends a spurious second
1670+
// RowDescription with fewer columns (a PLvSQL protocol artefact). The driver was
1671+
// blindly adopting the second RowDescription, so r.columnDefs ended up with 1
1672+
// column while the DataRow still contained 2 fields — causing an index-out-of-range
1673+
// panic in rows.Next().
1674+
//
1675+
// Fix: runSimpleStatement and collectResults now ignore any RowDescription that
1676+
// arrives after DataRows have already been buffered for the current result set.
1677+
func TestStoredProcedureWithNotice(t *testing.T) {
1678+
connDB := openConnection(t)
1679+
defer closeConnection(t, connDB)
1680+
1681+
// Create the procedure; drop it afterward.
1682+
_, err := connDB.ExecContext(ctx, `
1683+
CREATE OR REPLACE PROCEDURE test_notice_proc(INOUT a INT, INOUT b VARCHAR(64))
1684+
LANGUAGE PLvSQL AS $$
1685+
BEGIN
1686+
RAISE NOTICE 'Value of a: %', a;
1687+
RAISE NOTICE 'Value of b: %', b;
1688+
END;
1689+
$$`)
1690+
assertNoErr(t, err)
1691+
1692+
defer connDB.ExecContext(ctx, "DROP PROCEDURE IF EXISTS test_notice_proc(INT, VARCHAR)")
1693+
1694+
// CALL must not panic. We wrap in a recover so that a panic becomes a
1695+
// test failure with a meaningful message rather than crashing the whole
1696+
// test binary.
1697+
func() {
1698+
defer func() {
1699+
if r := recover(); r != nil {
1700+
t.Fatalf("rows.Next() panicked when stored procedure emitted NOTICE messages: %v", r)
1701+
}
1702+
}()
1703+
1704+
rows, err := connDB.QueryContext(ctx, "CALL test_notice_proc(10, 'hello')")
1705+
assertNoErr(t, err)
1706+
defer rows.Close()
1707+
1708+
// Iterate over all result sets (NOTICE messages may produce extra sets).
1709+
for {
1710+
cols, err := rows.Columns()
1711+
assertNoErr(t, err)
1712+
1713+
valuePtrs := make([]interface{}, len(cols))
1714+
values := make([]interface{}, len(cols))
1715+
for i := range values {
1716+
valuePtrs[i] = &values[i]
1717+
}
1718+
1719+
for rows.Next() {
1720+
assertNoErr(t, rows.Scan(valuePtrs...))
1721+
testLogger.Debug("TestStoredProcedureWithNotice row: %v", values)
1722+
}
1723+
1724+
if !rows.NextResultSet() {
1725+
break
1726+
}
1727+
}
1728+
}()
1729+
}
1730+
1731+
// TestStoredProcedureWithNoticeSimpleQuery is the same scenario as
1732+
// TestStoredProcedureWithNotice but forces the simple query protocol path
1733+
// (use_prepared_statements=0) to verify correct behaviour on both code paths.
1734+
func TestStoredProcedureWithNoticeSimpleQuery(t *testing.T) {
1735+
simpleConnStr := strings.Replace(myDBConnectString, "use_prepared_statements=1", "use_prepared_statements=0", 1)
1736+
connDB, err := sql.Open("vertica", simpleConnStr)
1737+
assertNoErr(t, err)
1738+
assertNoErr(t, connDB.PingContext(ctx))
1739+
defer closeConnection(t, connDB)
1740+
1741+
_, err = connDB.ExecContext(ctx, `
1742+
CREATE OR REPLACE PROCEDURE test_notice_proc_simple(INOUT a INT, INOUT b VARCHAR(64))
1743+
LANGUAGE PLvSQL AS $$
1744+
BEGIN
1745+
RAISE NOTICE 'Value of a: %', a;
1746+
RAISE NOTICE 'Value of b: %', b;
1747+
END;
1748+
$$`)
1749+
assertNoErr(t, err)
1750+
defer connDB.ExecContext(ctx, "DROP PROCEDURE IF EXISTS test_notice_proc_simple(INT, VARCHAR)")
1751+
1752+
func() {
1753+
defer func() {
1754+
if r := recover(); r != nil {
1755+
t.Fatalf("rows.Next() panicked on simple query path: %v", r)
1756+
}
1757+
}()
1758+
1759+
rows, err := connDB.QueryContext(ctx, "CALL test_notice_proc_simple(10, 'hello')")
1760+
assertNoErr(t, err)
1761+
defer rows.Close()
1762+
1763+
for {
1764+
cols, err := rows.Columns()
1765+
assertNoErr(t, err)
1766+
1767+
valuePtrs := make([]interface{}, len(cols))
1768+
values := make([]interface{}, len(cols))
1769+
for i := range values {
1770+
valuePtrs[i] = &values[i]
1771+
}
1772+
1773+
for rows.Next() {
1774+
assertNoErr(t, rows.Scan(valuePtrs...))
1775+
testLogger.Debug("TestStoredProcedureWithNoticeSimpleQuery row: %v", values)
1776+
}
1777+
1778+
if !rows.NextResultSet() {
1779+
break
1780+
}
1781+
}
1782+
}()
1783+
}
1784+
16631785
func init() {
16641786
// One or both lines below are necessary depending on your go version
16651787
testing.Init()

rows.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,3 +352,27 @@ func newEmptyRows() *rows {
352352
be := &msgs.BERowDescMsg{Columns: cdf}
353353
return newRows(context.Background(), be, "")
354354
}
355+
356+
// expandColumnDefs grows r.columnDefs to cover at least numCols columns.
357+
// It is a last-resort fallback for CALL statements where Vertica's Describe
358+
// phase returns an incomplete RowDescription and no execution-time
359+
// RowDescription is sent to correct it. Synthetic entries use OID 0 so that
360+
// rows.Next() returns the raw wire bytes as a string (the default case),
361+
// making the behaviour explicit rather than falsely claiming a specific type.
362+
// The caller invokes this whenever a DataRow wider than columnDefs is seen,
363+
// not only on the first row.
364+
//
365+
// Ordering guarantee: this function is called inside collectResults(), which
366+
// fully drains the wire and buffers all rows before returning the *rows object
367+
// to database/sql. Because database/sql only calls Columns() after receiving
368+
// that object, all expansions are complete before Columns() executes.
369+
// This invariant would break in a streaming/lazy-row model where *rows is
370+
// handed to the caller before all DataRows have been received.
371+
func (r *rows) expandColumnDefs(numCols uint16) {
372+
for uint16(len(r.columnDefs.Columns)) < numCols {
373+
r.columnDefs.Columns = append(r.columnDefs.Columns, &msgs.BERowDescColumnDef{
374+
FieldName: fmt.Sprintf("unknown_col%d", len(r.columnDefs.Columns)),
375+
DataTypeOID: 0, // unknown — falls to the default string case in rows.Next()
376+
})
377+
}
378+
}

stmt.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -595,13 +595,32 @@ func (s *stmt) collectResults(ctx context.Context) (*rows, error) {
595595

596596
switch msg := bMsg.(type) {
597597
case *msgs.BEDataRowMsg:
598+
// Vertica's Describe phase under-reports the column count for CALL
599+
// statements that emit RAISE NOTICE: a DataRow at execution time may
600+
// contain more fields than columnDefs describes. Expand columnDefs
601+
// whenever a wider DataRow is seen so that Columns() always returns
602+
// the correct width.
603+
//
604+
// This is safe because collectResults() is synchronous and fully
605+
// buffers all rows before returning *rows to the caller. database/sql
606+
// calls Columns() only after receiving that object, so all expansions
607+
// are complete by the time the column list is observed.
608+
if uint16(len(rows.columnDefs.Columns)) < msg.Columns().NumCols {
609+
rows.expandColumnDefs(msg.Columns().NumCols)
610+
}
598611
err = rows.addRow(msg)
599612
if err != nil {
600613
return rows, err
601614
}
602615
case *msgs.BERowDescMsg:
603-
s.lastRowDesc = msg
604-
rows = newRows(ctx, s.lastRowDesc, s.conn.serverTZOffset)
616+
// An execution-time RowDescription may arrive before any DataRows
617+
// and can carry a more complete schema than the Describe-phase one.
618+
// Only adopt it if it has at least as many columns, to prevent a
619+
// truncated description from silently replacing a wider one.
620+
if rows.resultData.Peek() == nil && len(msg.Columns) >= len(rows.columnDefs.Columns) {
621+
s.lastRowDesc = msg
622+
rows = newRows(ctx, s.lastRowDesc, s.conn.serverTZOffset)
623+
}
605624
case *msgs.BEErrorMsg:
606625
s.conn.sync()
607626
return newEmptyRows(), s.evaluateErrorMsg(msg)

0 commit comments

Comments
 (0)