From 945a3da13bf3a0bf96eaa2cf2974381a43d7839b Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 17 Jul 2023 12:26:59 -0400 Subject: [PATCH 1/2] Always use QUERY_SQL request in the dqlite shell In recent versions of dqlite, the QUERY_SQL request supports SQL queries that modify the database and do not return rows. Update internal/shell/shell.go to always send this request for SQL strings entered by the user, instead of switching between QUERY_SQL and EXEC_SQL based on whether the string start with `SELECT`. Also fix the Rows iterator so that it can deal with empty row sets that have zero columns. Previously it would enter an infinite loop in this situation. Signed-off-by: Cole Miller --- internal/protocol/message.go | 6 ++++++ internal/shell/shell.go | 8 ++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/protocol/message.go b/internal/protocol/message.go index 95133995..3a9581d9 100644 --- a/internal/protocol/message.go +++ b/internal/protocol/message.go @@ -478,6 +478,12 @@ func (r *Rows) columnTypes(save bool) ([]uint8, error) { r.types = make([]uint8, len(r.Columns)) } + // If there are zero columns, no rows can be encoded or decoded, + // so we signal EOF immediately. + if len(r.types) == 0 { + return r.types, io.EOF + } + // Each column needs a 4 byte slot to store the column type. The row // header must be padded to reach word boundary. headerBits := len(r.types) * 4 diff --git a/internal/shell/shell.go b/internal/shell/shell.go index 5d7a573e..a93c69fd 100644 --- a/internal/shell/shell.go +++ b/internal/shell/shell.go @@ -87,11 +87,7 @@ func (s *Shell) Process(ctx context.Context, line string) (string, error) { if strings.HasPrefix(strings.ToLower(strings.TrimLeft(line, " ")), ".reconfigure") { return s.processReconfigure(ctx, line) } - if strings.HasPrefix(strings.ToUpper(strings.TrimLeft(line, " ")), "SELECT") { - return s.processSelect(ctx, line) - } else { - return "", s.processExec(ctx, line) - } + return s.processQuery(ctx, line) } func (s *Shell) processHelp() string { @@ -317,7 +313,7 @@ func (s *Shell) processWeight(ctx context.Context, line string) (string, error) return "", nil } -func (s *Shell) processSelect(ctx context.Context, line string) (string, error) { +func (s *Shell) processQuery(ctx context.Context, line string) (string, error) { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return "", fmt.Errorf("begin transaction: %w", err) From f7320ed4ca453e08e6d8abdced2d0152789da9f7 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 17 Jul 2023 12:59:06 -0400 Subject: [PATCH 2/2] Add a regression test for the zero-column Query case Signed-off-by: Cole Miller --- driver/driver_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/driver/driver_test.go b/driver/driver_test.go index bbb3ae7a..3d030c50 100644 --- a/driver/driver_test.go +++ b/driver/driver_test.go @@ -567,6 +567,22 @@ func Test_ColumnTypesEnd(t *testing.T) { assert.NoError(t, conn.Close()) } +func Test_ZeroColumns(t *testing.T) { + drv, cleanup := newDriver(t) + defer cleanup() + + conn, err := drv.Open("test.db") + require.NoError(t, err) + queryer := conn.(driver.Queryer) + + rows, err := queryer.Query("CREATE TABLE foo (bar INTEGER)", []driver.Value{}) + require.NoError(t, err) + values := []driver.Value{} + require.Equal(t, io.EOF, rows.Next(values)) + + require.NoError(t, conn.Close()) +} + func newDriver(t *testing.T) (*dqlitedriver.Driver, func()) { t.Helper()