From 0e0eedaf5f867740c3d60feee4f95a5b60db3525 Mon Sep 17 00:00:00 2001 From: Arvid Fahlstrom Myrman <885076+arvidfm@users.noreply.github.com> Date: Mon, 10 Jun 2024 20:58:37 +0100 Subject: [PATCH 01/23] add type conversion for complex types --- rows.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/rows.go b/rows.go index b507b1b..c7802f0 100644 --- a/rows.go +++ b/rows.go @@ -4,8 +4,10 @@ import ( "database/sql/driver" "errors" "fmt" - gms "github.com/dolthub/go-mysql-server/sql" "io" + + gms "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" ) var _ driver.Rows = (*doltRows)(nil) @@ -58,6 +60,24 @@ func (rows *doltRows) Next(dest []driver.Value) error { if err != nil { return fmt.Errorf("error processing column %d: %w", i, err) } + } else if geomValue, ok := nextRow[i].(types.GeometryValue); ok { + dest[i] = geomValue.Serialize() + } else if enumType, ok := rows.sch[i].Type.(gms.EnumType); ok { + if v, _, err := enumType.Convert(nextRow[i]); err != nil { + return fmt.Errorf("could not convert to expected enum type for column %d: %w", i, err) + } else if enumStr, ok := enumType.At(int(v.(uint16))); !ok { + return fmt.Errorf("not a valid enum index for column %d: %v", i, v) + } else { + dest[i] = enumStr + } + } else if setType, ok := rows.sch[i].Type.(gms.SetType); ok { + if v, _, err := setType.Convert(nextRow[i]); err != nil { + return fmt.Errorf("could not convert to expected set type for column %d: %w", i, err) + } else if setStr, err := setType.BitsToString(v.(uint64)); err != nil { + return fmt.Errorf("could not convert value to set string for column %d: %w", i, err) + } else { + dest[i] = setStr + } } else { dest[i] = nextRow[i] } From e7fd0599f18ca9f5f00ba9f024743d5982643e57 Mon Sep 17 00:00:00 2001 From: Arvid Fahlstrom Myrman <885076+arvidfm@users.noreply.github.com> Date: Mon, 10 Jun 2024 20:58:47 +0100 Subject: [PATCH 02/23] add tests for type conversion --- smoke_test.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/smoke_test.go b/smoke_test.go index 63a8f74..17ae4bb 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -136,6 +136,43 @@ func TestQueryContextInitialization(t *testing.T) { require.NoError(t, conn.Close()) } +// TestTypes asserts that various MySQL types are returned as the expected Go type by the driver. +func TestTypes(t *testing.T) { + conn, cleanupFunc := initializeTestDatabaseConnection(t, false) + defer cleanupFunc() + + ctx := context.Background() + _, err := conn.ExecContext(ctx, ` +create table testtable ( + enum_col ENUM('a', 'b', 'c'), + set_col SET('a', 'b', 'c'), + json_col JSON, + blob_col BLOB, + text_col TEXT, + geom_col POINT, + date_col DATETIME +); + +insert into testtable values ('b', 'a,c', '{"key": 42}', 'data', 'text', Point(5, -5), NOW()); +`) + require.NoError(t, err) + + row := conn.QueryRowContext(ctx, "select * from testtable") + vals := make([]any, 7) + ptrs := make([]any, 7) + for i := range vals { + ptrs[i] = &vals[i] + } + require.NoError(t, row.Scan(ptrs...)) + require.Equal(t, "b", vals[0]) + require.Equal(t, "a,c", vals[1]) + require.Equal(t, `{"key": 42}`, vals[2]) + require.Equal(t, []byte(`data`), vals[3]) + require.Equal(t, "text", vals[4]) + require.IsType(t, []byte(nil), vals[5]) + require.IsType(t, time.Time{}, vals[6]) +} + // initializeTestDatabaseConnection create a test database called testdb and initialize a database/sql connection // using the Dolt driver. The connection, |conn|, is returned, and |cleanupFunc| is a function that the test function // should defer in order to properly dispose of test resources. From 42baac522a5414ea61bcdbf76e7e60cc4310f5a1 Mon Sep 17 00:00:00 2001 From: Arvid Fahlstrom Myrman <885076+arvidfm@users.noreply.github.com> Date: Mon, 10 Jun 2024 20:59:24 +0100 Subject: [PATCH 03/23] close row iterator --- result.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/result.go b/result.go index 54b9374..301330b 100644 --- a/result.go +++ b/result.go @@ -1,9 +1,8 @@ package embedded import ( - "io" - "database/sql/driver" + "io" gms "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/types" @@ -39,6 +38,10 @@ func newResult(gmsCtx *gms.Context, sch gms.Schema, rowItr gms.RowIter) *doltRes } } + if err := rowItr.Close(gmsCtx); err != nil { + return &doltResult{err: err} + } + return &doltResult{ affected: affected, last: last, From 332ea7a6ade94f2084a64e4651f16c050227b44b Mon Sep 17 00:00:00 2001 From: Arvid Fahlstrom Myrman <885076+arvidfm@users.noreply.github.com> Date: Mon, 10 Jun 2024 20:59:37 +0100 Subject: [PATCH 04/23] remove unnecessary commit in tests --- smoke_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/smoke_test.go b/smoke_test.go index 63a8f74..1efffcb 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -171,8 +171,5 @@ func initializeTestDatabaseConnection(t *testing.T, clientFoundRows bool) (conn _, err = res.RowsAffected() require.NoError(t, err) - _, err = conn.ExecContext(ctx, "commit;") - require.NoError(t, err) - return conn, cleanUpFunc } From 35a04c47358d42a01eb491b77f62ad2841380667 Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Mon, 10 Jun 2024 13:49:25 -0700 Subject: [PATCH 05/23] Added test workflow --- .github/workflows/test.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..23b13d2 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: Test +on: [pull_request] + +concurrency: + group: test-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + test: + strategy: + fail-fast: false + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + id: go + - name: Test + run: go test ./... + - name: Test Race + run: go test -race ./... From eb4eaf8b1f0cc31573672c95600edbc7a57f3d2f Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Mon, 10 Jun 2024 14:12:55 -0700 Subject: [PATCH 06/23] Bug fix for windows --- smoke_test.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/smoke_test.go b/smoke_test.go index 86b4a39..0e40e04 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -5,6 +5,7 @@ import ( "database/sql" "net/url" "os" + "strings" "testing" "time" @@ -195,7 +196,7 @@ func initializeTestDatabaseConnection(t *testing.T, clientFoundRows bool) (conn if clientFoundRows { query["clientfoundrows"] = []string{"true"} } - dsn := url.URL{Scheme: "file", Path: dir, RawQuery: query.Encode()} + dsn := url.URL{Scheme: "file", Path: encodeDir(dir), RawQuery: query.Encode()} db, err := sql.Open(DoltDriverName, dsn.String()) require.NoError(t, err) require.NoError(t, db.PingContext(ctx)) @@ -210,3 +211,13 @@ func initializeTestDatabaseConnection(t *testing.T, clientFoundRows bool) (conn return conn, cleanUpFunc } + +func encodeDir(dir string) string { + // encodeDir translate a given path to a URL compatible path, mostly for windows compatibility + if os.PathSeparator == '\\' { + dir = strings.ReplaceAll(dir, `\`, `/`) + // strip off drive letter + dir = dir[2:] + } + return dir +} From c1fa11299eb869db58abc19b55a0d6789aa7717f Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Tue, 11 Jun 2024 12:45:43 -0700 Subject: [PATCH 07/23] Bug fix for non-C: drive letters --- smoke_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/smoke_test.go b/smoke_test.go index 0e40e04..d6807b8 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -216,8 +216,6 @@ func encodeDir(dir string) string { // encodeDir translate a given path to a URL compatible path, mostly for windows compatibility if os.PathSeparator == '\\' { dir = strings.ReplaceAll(dir, `\`, `/`) - // strip off drive letter - dir = dir[2:] } return dir } From ab4981be462e6712b6f7fcfe74159ac7d57141c1 Mon Sep 17 00:00:00 2001 From: James Cor Date: Wed, 26 Jun 2024 12:07:38 -0700 Subject: [PATCH 08/23] changes --- conn.go | 32 ++++++++++++++++++-------------- smoke_test.go | 22 +++++++++++++++++++++- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/conn.go b/conn.go index e102134..dedefcf 100644 --- a/conn.go +++ b/conn.go @@ -5,10 +5,12 @@ import ( "database/sql" "database/sql/driver" "fmt" - "github.com/dolthub/dolt/go/cmd/dolt/commands/engine" - gms "github.com/dolthub/go-mysql-server/sql" "io" - "time" + "time" + + "github.com/dolthub/dolt/go/cmd/dolt/commands/engine" + + gms "github.com/dolthub/go-mysql-server/sql" ) var _ driver.Conn = (*DoltConn)(nil) @@ -25,20 +27,21 @@ func (d *DoltConn) Prepare(query string) (driver.Stmt, error) { multiStatements := d.DataSource.ParamIsTrue(MultiStatementsParam) if multiStatements { - qs := NewQuerySplitter(query) - current, err := qs.Next() - if err != io.EOF && err != nil { + scanner := gms.NewMysqlParser() + parsed, prequery, remainder, err := scanner.Parse(d.gmsCtx, query, true) + if err != nil { return nil, translateError(err) } - for len(current) > 0 { - if !qs.HasMore() { + for { + if len(remainder) == 0 { + query = prequery break } - d.se.GetUnderlyingEngine() err = func() error { - _, rowIter, err := d.se.Query(d.gmsCtx, current) + var rowIter gms.RowIter + _, rowIter, err = d.se.GetUnderlyingEngine().QueryWithBindings(d.gmsCtx, prequery, parsed, nil) if err != nil { return translateError(err) } @@ -59,13 +62,14 @@ func (d *DoltConn) Prepare(query string) (driver.Stmt, error) { return nil, err } - current, err = qs.Next() + parsed, prequery, remainder, err = scanner.Parse(d.gmsCtx, remainder, true) if err != nil { - return nil, err + return nil, translateError(err) } } - - query = current + if prequery != "" { + query = prequery + } } if len(query) > 0 { diff --git a/smoke_test.go b/smoke_test.go index d6807b8..c31fd1d 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -3,7 +3,8 @@ package embedded import ( "context" "database/sql" - "net/url" + "fmt" +"net/url" "os" "strings" "testing" @@ -57,6 +58,25 @@ func TestMultiStatements(t *testing.T) { require.NoError(t, conn.Close()) } +func TestMultiStatementsBlocks(t *testing.T) { + conn, cleanupFunc := initializeTestDatabaseConnection(t, false) + defer cleanupFunc() + + ctx := context.Background() + rows, err := conn.QueryContext(ctx, "create procedure p() begin select 1; end; call p(); call p(); call p();") + require.NoError(t, err) + for rows.Next() { + var i int + err = rows.Scan(&i) + if err != nil { + panic(err) + } + fmt.Println(i) + } + require.NoError(t, rows.Err()) + require.NoError(t, rows.Close()) +} + // TestClientFoundRows asserts that the number of affected rows reported for a query // correctly reflects whether the CLIENT_FOUND_ROWS capability is set or not. func TestClientFoundRows(t *testing.T) { From 294710fc0ea6174f725bfa5bc2bc5c00aabf2960 Mon Sep 17 00:00:00 2001 From: James Cor Date: Wed, 26 Jun 2024 13:50:03 -0700 Subject: [PATCH 09/23] more tests and tidying up --- conn.go | 4 ++-- smoke_test.go | 34 +++++++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/conn.go b/conn.go index dedefcf..591ceca 100644 --- a/conn.go +++ b/conn.go @@ -6,9 +6,9 @@ import ( "database/sql/driver" "fmt" "io" - "time" + "time" - "github.com/dolthub/dolt/go/cmd/dolt/commands/engine" + "github.com/dolthub/dolt/go/cmd/dolt/commands/engine" gms "github.com/dolthub/go-mysql-server/sql" ) diff --git a/smoke_test.go b/smoke_test.go index c31fd1d..6d138d8 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -3,8 +3,7 @@ package embedded import ( "context" "database/sql" - "fmt" -"net/url" + "net/url" "os" "strings" "testing" @@ -58,7 +57,7 @@ func TestMultiStatements(t *testing.T) { require.NoError(t, conn.Close()) } -func TestMultiStatementsBlocks(t *testing.T) { +func TestMultiStatementsStoredProc(t *testing.T) { conn, cleanupFunc := initializeTestDatabaseConnection(t, false) defer cleanupFunc() @@ -68,10 +67,31 @@ func TestMultiStatementsBlocks(t *testing.T) { for rows.Next() { var i int err = rows.Scan(&i) - if err != nil { - panic(err) - } - fmt.Println(i) + require.NoError(t, err) + require.Equal(t, 1, i) + } + require.NoError(t, rows.Err()) + require.NoError(t, rows.Close()) +} + +func TestMultiStatementsTrigger(t *testing.T) { + conn, cleanupFunc := initializeTestDatabaseConnection(t, false) + defer cleanupFunc() + + ctx := context.Background() + res, err := conn.ExecContext(ctx, "create table t (i int primary key, j int);") + require.NoError(t, err) + _, err = res.RowsAffected() + require.NoError(t, err) + + rows, err := conn.QueryContext(ctx, "create trigger trig before insert on t for each row begin set new.j = new.j * 100; end; insert into t values (1, 2); select * from t;") + require.NoError(t, err) + for rows.Next() { + var i, j int + err = rows.Scan(&i, &j) + require.NoError(t, err) + require.Equal(t, 1, i) + require.Equal(t, 200, j) } require.NoError(t, rows.Err()) require.NoError(t, rows.Close()) From f2c312d0df5a711b2cf07c92a2dace16152836fa Mon Sep 17 00:00:00 2001 From: James Cor Date: Mon, 1 Jul 2024 16:52:24 -0700 Subject: [PATCH 10/23] partial work --- conn.go | 1 - example/main.go | 310 ++++++++++++++---------------------------------- go.mod | 27 +++-- go.sum | 44 ++++--- result.go | 29 ++++- 5 files changed, 154 insertions(+), 257 deletions(-) diff --git a/conn.go b/conn.go index 591ceca..1d623e9 100644 --- a/conn.go +++ b/conn.go @@ -35,7 +35,6 @@ func (d *DoltConn) Prepare(query string) (driver.Stmt, error) { for { if len(remainder) == 0 { - query = prequery break } diff --git a/example/main.go b/example/main.go index 3e5fffe..c15e0ec 100644 --- a/example/main.go +++ b/example/main.go @@ -1,246 +1,114 @@ package main import ( - "bytes" "context" "database/sql" - "encoding/base64" "fmt" - "os" - "reflect" - "strconv" - "strings" - "time" + "net/url" + + "github.com/go-sql-driver/mysql" _ "github.com/dolthub/driver" ) -func errExit(wrapFormat string, err error) { - if err != nil { - if len(wrapFormat) > 0 { - err = fmt.Errorf(wrapFormat, err) - } - - fmt.Fprintf(os.Stderr, err.Error()) - os.Exit(1) - } -} - func main() { - if len(os.Args) != 2 { - fmt.Println("usage: example file:///path/to/doltdb?commitname=&commitemail=&database=&multistatements=") - return - } - ctx := context.Background() - - dataSource := os.Args[1] - fmt.Println("Connecting to", dataSource) - db, err := sql.Open("dolt", dataSource) - errExit("failed to open database using the dolt driver: %w", err) - - err = printQuery(ctx, db, "CREATE DATABASE IF NOT EXISTS testdb;USE testdb;") - errExit("", err) - - err = printQuery(ctx, db, "USE testdb;") - errExit("", err) - - printQuery(ctx, db, `CREATE TABLE IF NOT EXISTS t2( - pk int primary key auto_increment, - c1 varchar(32) -)`) - errExit("", err) - - printQuery(ctx, db, "SHOW TABLES;") - - stmt, err := db.PrepareContext(ctx, "Insert into t2 (c1) values (?);") - errExit("", err) - defer stmt.Close() - - result, err := stmt.ExecContext(ctx, "test") - errExit("", err) - - fmt.Println(result.LastInsertId()) - - err = printQuery(ctx, db, `CREATE TABLE IF NOT EXISTS t1 ( - pk int PRIMARY KEY, - c1 varchar(512), - c2 float, - c3 bool, - c4 datetime -);`) - - err = printQuery(ctx, db, "SELECT * FROM t1;") - errExit("", err) - - err = printQuery(ctx, db, `REPLACE INTO t1 VALUES -(1, 'this is a test', 0, 0, '1998-01-23 12:45:56'), -(2, 'it is only a test', 1.0, 1, '2010-12-31 01:15:00'), -(3, NULL, 3.335, 0, NULL), -(4, 'something something', 3.5, 1, '2015-04-03 14:00:45');`) - errExit("", err) - - err = printQuery(ctx, db, "SELECT * FROM t1;") - errExit("", err) - - err = printQuery(ctx, db, "DELETE FROM t1;") - errExit("", err) - - rows := [][]interface{}{ - {1, "this is a test", 0, 0, time.Date(1998, 1, 23, 12, 45, 56, 0, time.UTC)}, - {2, "it is only a test", 1.0, 1, time.Date(2010, 12, 31, 1, 15, 0, 0, time.UTC)}, - {3, nil, 3.335, 0, nil}, - {4, "something something", 3.5, 1, time.Date(2015, 4, 3, 14, 0, 45, 0, time.UTC)}, + if ctx == nil {} + + dir := "./" + params := url.Values{ + "database": []string{"tmp"}, + "commitname": []string{"root"}, + "commitemail": []string{"root@dolthub.com"}, + "multistatements": []string{"true"}, } - - tx, err := db.Begin() - errExit("", err) - - err = prepareAndExec(ctx, tx, "INSERT INTO t1 VALUES (?, ?, ?, ?, ?)", rows) - errExit("", err) - - err = printQuery(ctx, tx, `INSERT INTO t1 VALUES (5, "blah", 4.0, 0, now()); -INSERT INTO t1 VALUES (6, 'aoeu', 7.0, 1, now()), (7,"aoeu aoeu", 8.1, 0, now()); -SELECT * FROM t1;`) - - fmt.Println("Query Before Rollback") - err = printQuery(ctx, tx, "SELECT * FROM t1;") - - //err = tx.Rollback() - //errExit("", err) - //fmt.Println("Query After Rollback") - - err = tx.Commit() - errExit("", err) - fmt.Println("Query After Commit") - - err = printQuery(ctx, db, "SELECT * FROM t1;") - errExit("", err) -} - -type Queryable interface { - QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) -} - -func printQuery(ctx context.Context, queryable Queryable, query string) error { - fmt.Println("query:", query) - rows, err := queryable.QueryContext(ctx, query) + dsn := url.URL{Scheme: "file", Path: dir, RawQuery: params.Encode()} + db, err := sql.Open("dolt", dsn.String()) if err != nil { - return fmt.Errorf("query '%s' failed: %w", query, err) + panic(err) } - defer rows.Close() - - fmt.Println("results:") - err = printRows(rows) - if err != nil { - return err + db.Ping() + + cfg := mysql.Config{ + User: "root", + Passwd: "root", + Net: "tcp", + Addr: "127.0.0.1:3306", + DBName: "test_db", + AllowNativePasswords: true, } - - fmt.Println() - return nil -} - -func printRows(rows *sql.Rows) error { - cols, err := rows.Columns() + if cfg.Addr == "" {} + db, err = sql.Open("mysql", cfg.FormatDSN()) if err != nil { - return fmt.Errorf("Failed to get columns: %w", err) + panic(err) } - - fmt.Println(strings.Join(cols, "|")) - - for rows.Next() { - values := make([]interface{}, len(cols)) - var generic = reflect.TypeOf(values).Elem() - for i := 0; i < len(cols); i++ { - values[i] = reflect.New(generic).Interface() - } - - err = rows.Scan(values...) - if err != nil { - return fmt.Errorf("scan failed: %w", err) - } - - result := bytes.NewBuffer(nil) - for i := 0; i < len(cols); i++ { - if i != 0 { - result.WriteString("|") - } - - var rawValue = *(values[i].(*interface{})) - switch val := rawValue.(type) { - case string: - result.WriteString(val) - case int: - result.WriteString(strconv.FormatInt(int64(val), 10)) - case int8: - result.WriteString(strconv.FormatInt(int64(val), 10)) - case int16: - result.WriteString(strconv.FormatInt(int64(val), 10)) - case int32: - result.WriteString(strconv.FormatInt(int64(val), 10)) - case int64: - result.WriteString(strconv.FormatInt(val, 10)) - case uint: - result.WriteString(strconv.FormatUint(uint64(val), 10)) - case uint8: - result.WriteString(strconv.FormatUint(uint64(val), 10)) - case uint16: - result.WriteString(strconv.FormatUint(uint64(val), 10)) - case uint32: - result.WriteString(strconv.FormatUint(uint64(val), 10)) - case uint64: - result.WriteString(strconv.FormatUint(val, 10)) - case float32: - result.WriteString(strconv.FormatFloat(float64(val), 'f', 2, 64)) - case float64: - result.WriteString(strconv.FormatFloat(val, 'f', 2, 64)) - case bool: - if val { - result.WriteString("true") - } else { - result.WriteString("false") - } - case []byte: - enc := base64.NewEncoder(base64.URLEncoding, result) - _, err := enc.Write(val) - errExit("failed to base64 encode blob: %w", err) - case time.Time: - timeStr := val.Format(time.RFC3339) - result.WriteString(timeStr) - } - } - - fmt.Println(result.String()) - } - - return nil -} - -type Preparable interface { - PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) -} - -func prepareAndExec(ctx context.Context, prepable Preparable, query string, vals [][]interface{}) error { - stmt, err := prepable.PrepareContext(ctx, query) + db.Ping() + + defer db.Close() + + //var res sql.Result + //res, err = db.ExecContext(ctx, "select 1; select 2; select 3;") + //if err != nil { + // panic(err) + //} + //numRows, err := res.RowsAffected() + //if err != nil { + // panic(err) + //} + //fmt.Sprintf("Rows Affected: %d", numRows) + + var rows *sql.Rows + rows, err = db.QueryContext(ctx, "select 1;") if err != nil { - return err + panic(err) } - - for i := range vals { - result, err := stmt.ExecContext(ctx, vals[i]...) - if err != nil { - return fmt.Errorf("failed to execute prepared statement '%s' with parameters: %v - %w", query, vals[i], err) - } - - affected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("failed to get num rows affected: %w", err) + var s int + for { + fmt.Printf("Result Set #%d\n", s) + for rows.Next() { + var i int + err = rows.Scan(&i) + if err != nil { + panic(err) + } + println(i) } + s++ - if affected != 1 { - return fmt.Errorf("expected '%s' to affect 1 row but it affected %d; params: %v - %w", query, affected, vals[i], err) + if !rows.NextResultSet() { + break } } - return nil -} + //defer db.ExecContext(ctx, "drop procedure p") + return + + //cfg := mysql.Config{ + // User: "root", + // Passwd: "", + // Net: "tcp", + // Addr: "127.0.0.1:3306", + // DBName: "tmp", + // AllowNativePasswords: true, + //} + //// Get a database handle. + //db, err = sql.Open("mysql", cfg.FormatDSN()) + //if err != nil { + // panic(err) + //} + //defer db.Close() + // + //var rows *sql.Rows + //rows, err = db.QueryContext(ctx, "delimiter //") + //if err != nil { + // panic(err) + //} + //for rows.Next() { + // var i int + // err = rows.Scan(&i) + // if err != nil { + // panic(err) + // } + // fmt.Println(i) + //} +} \ No newline at end of file diff --git a/go.mod b/go.mod index c2e5532..a2c9224 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ toolchain go1.22.3 require ( github.com/dolthub/dolt/go v0.40.5-0.20240604165632-02f450318cb3 - github.com/dolthub/go-mysql-server v0.18.2-0.20240604161217-d1dca79a32b8 - github.com/dolthub/vitess v0.0.0-20240603172811-467efd832e48 + github.com/dolthub/go-mysql-server v0.18.2-0.20240701183357-77fa27941c98 + github.com/dolthub/vitess v0.0.0-20240626174323-4083c07f5e9c github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d github.com/stretchr/testify v1.8.4 gorm.io/driver/mysql v1.5.6 @@ -71,6 +71,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mohae/uvarint v0.0.0-20160208145430-c3f9e62bf2b0 // indirect github.com/oracle/oci-go-sdk/v65 v65.55.0 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -94,17 +95,17 @@ require ( go.opentelemetry.io/otel/trace v1.23.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.21.0 // indirect + golang.org/x/crypto v0.23.0 // indirect golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect - golang.org/x/mod v0.15.0 // indirect - golang.org/x/net v0.23.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.17.0 // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/term v0.20.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.18.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/api v0.164.0 // indirect google.golang.org/appengine v1.6.8 // indirect @@ -120,4 +121,10 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) +replace github.com/dolthub/dolt/go => ../dolt/go + +replace github.com/dolthub/go-mysql-server => ../go-mysql-server + +replace github.com/dolthub/vitess => ../vitess + replace github.com/google/flatbuffers => github.com/dolthub/flatbuffers v1.13.0-dh.1 diff --git a/go.sum b/go.sum index b4fcfd3..eaf6476 100644 --- a/go.sum +++ b/go.sum @@ -246,8 +246,6 @@ github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQ github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/dolthub/dolt/go v0.40.5-0.20240604165632-02f450318cb3 h1:dprIlHxkwCRjzBVUop1FgwUDe3aiF118XVf6n866B/E= -github.com/dolthub/dolt/go v0.40.5-0.20240604165632-02f450318cb3/go.mod h1:bDWUUxoq/7AxDszTKFneulNc6Uh1PLqK897LjqKfoWY= github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20240212175631-02e9f99a3a9b h1:VehmKUF425NgXpRQVYMPzJx6rWZaJ2cbTwTTwXlrbiM= github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20240212175631-02e9f99a3a9b/go.mod h1:gHeHIDGU7em40EhFTliq62pExFcc1hxDTIZ9g5UqXYM= github.com/dolthub/flatbuffers v1.13.0-dh.1 h1:OWJdaPep22N52O/0xsUevxJ6Qfw1M2txCjZPOdjXybE= @@ -258,8 +256,6 @@ github.com/dolthub/fslock v0.0.3 h1:iLMpUIvJKMKm92+N1fmHVdxJP5NdyDK5bK7z7Ba2s2U= github.com/dolthub/fslock v0.0.3/go.mod h1:QWql+P17oAAMLnL4HGB5tiovtDuAjdDTPbuqx7bYfa0= github.com/dolthub/go-icu-regex v0.0.0-20230524105445-af7e7991c97e h1:kPsT4a47cw1+y/N5SSCkma7FhAPw7KeGmD6c9PBZW9Y= github.com/dolthub/go-icu-regex v0.0.0-20230524105445-af7e7991c97e/go.mod h1:KPUcpx070QOfJK1gNe0zx4pA5sicIK1GMikIGLKC168= -github.com/dolthub/go-mysql-server v0.18.2-0.20240604161217-d1dca79a32b8 h1:F9tnktZhnglXHYiG/tjnVMiLQSBZ8iJiflqgHg+ldKo= -github.com/dolthub/go-mysql-server v0.18.2-0.20240604161217-d1dca79a32b8/go.mod h1:GT7JcQavIf7bAO17/odujkgHM/N0t4b1HfAPBJ2jzXo= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63 h1:OAsXLAPL4du6tfbBgK0xXHZkOlos63RdKYS3Sgw/dfI= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63/go.mod h1:lV7lUeuDhH5thVGDCKXbatwKy2KW80L4rMT46n+Y2/Q= github.com/dolthub/ishell v0.0.0-20221214210346-d7db0b066488 h1:0HHu0GWJH0N6a6keStrHhUAK5/o9LVfkh44pvsV4514= @@ -270,8 +266,6 @@ github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw= github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0= -github.com/dolthub/vitess v0.0.0-20240603172811-467efd832e48 h1:KfVnDVNytmTHeYZaQfUWZF/uE/fbLdLvXVdebQTPaMk= -github.com/dolthub/vitess v0.0.0-20240603172811-467efd832e48/go.mod h1:uBvlRluuL+SbEWTCZ68o0xvsdYZER3CEG/35INdzfJM= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= @@ -610,6 +604,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/mohae/uvarint v0.0.0-20160208145430-c3f9e62bf2b0 h1:fXRYk7YXVIBMGAHT+GmAcbiXrudXMPtqdLfbkVfUhkI= +github.com/mohae/uvarint v0.0.0-20160208145430-c3f9e62bf2b0/go.mod h1:+6ZKJfAk1B0oKLOwdzYuRVJn3upG1c7uOm5Ih7Rrkvc= github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/ncw/swift v1.0.52/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= @@ -795,8 +791,8 @@ golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -822,8 +818,8 @@ golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+o golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.10.0 h1:gXjUUtwtx5yOE0VKWq1CH4IJAClq4UGgUA3i+rpON9M= -golang.org/x/image v0.10.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -850,8 +846,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -905,8 +901,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -942,8 +938,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1024,16 +1020,16 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1047,8 +1043,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1121,8 +1117,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= -golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/result.go b/result.go index 301330b..754b879 100644 --- a/result.go +++ b/result.go @@ -8,7 +8,8 @@ import ( "github.com/dolthub/go-mysql-server/sql/types" ) -var _ driver.Result = (*doltResult)(nil) +//var _ driver.Result = (*doltResult)(nil) +var _ driver.RowsNextResultSet = (*doltResult)(nil) type doltResult struct { affected int64 @@ -16,6 +17,32 @@ type doltResult struct { err error } +func (result *doltResult) Columns() []string { + //TODO implement me + panic("implement me") +} + +func (result *doltResult) Close() error { + //TODO implement me + panic("implement me") +} + +func (result *doltResult) Next(dest []driver.Value) error { + //TODO implement me + panic("implement me") +} + +func (result *doltResult) HasNextResultSet() bool { + //TODO implement me + panic("implement me") +} + +func (result *doltResult) NextResultSet() error { + //TODO implement me + panic("implement me") +} + + func newResult(gmsCtx *gms.Context, sch gms.Schema, rowItr gms.RowIter) *doltResult { var resultErr error var affected int64 From 48aa5481db547d9a53b08f46f7214f161d31356e Mon Sep 17 00:00:00 2001 From: Jason Fulghum Date: Wed, 3 Jul 2024 17:01:02 -0700 Subject: [PATCH 11/23] Adding support for iterating over multiple result sets, via the driver.RowsNextResultSet interface --- conn.go | 103 ++++++++++++++++++++++++--------------------------- driver.go | 13 +++---- go.mod | 10 +---- go.sum | 10 ++++- result.go | 29 +-------------- rows.go | 35 ++++++++++++++++- statement.go | 54 ++++++++++++++++++++++++++- 7 files changed, 152 insertions(+), 102 deletions(-) diff --git a/conn.go b/conn.go index 1d623e9..2489874 100644 --- a/conn.go +++ b/conn.go @@ -5,11 +5,9 @@ import ( "database/sql" "database/sql/driver" "fmt" - "io" "time" "github.com/dolthub/dolt/go/cmd/dolt/commands/engine" - gms "github.com/dolthub/go-mysql-server/sql" ) @@ -22,65 +20,35 @@ type DoltConn struct { DataSource *DoltDataSource } -// Prepare returns a prepared statement, bound to this connection. +// Prepare calls the SQL engine to prepare |query| on this connection and returns a *doltStmt. If any +// errors were encountered preparing |query|, the error is returned instead. If multistatement mode +// has been enabled, then a *doltMultiStmt will be returned, capable of executing multiple statements. +// +// Note that the prepared query created by this method is never actually executed – the query is later +// executed as part of doltStmt, without using the prepared statement. The point of preparing it here is +// to detect any analysis errors. This matches the behavior of the MySQL driver implementation, but it +// may make sense to revisit this in the future. func (d *DoltConn) Prepare(query string) (driver.Stmt, error) { - multiStatements := d.DataSource.ParamIsTrue(MultiStatementsParam) - - if multiStatements { - scanner := gms.NewMysqlParser() - parsed, prequery, remainder, err := scanner.Parse(d.gmsCtx, query, true) - if err != nil { - return nil, translateError(err) - } + // Reuse the same ctx instance, but update the QueryTime to the current time. + // Statements are executed serially on a connection, so it's safe to reuse + // the same ctx instance and update the time. + d.gmsCtx.SetQueryTime(time.Now()) - for { - if len(remainder) == 0 { - break - } - - err = func() error { - var rowIter gms.RowIter - _, rowIter, err = d.se.GetUnderlyingEngine().QueryWithBindings(d.gmsCtx, prequery, parsed, nil) - if err != nil { - return translateError(err) - } - defer rowIter.Close(d.gmsCtx) - - for { - _, err := rowIter.Next(d.gmsCtx) - if err == io.EOF { - break - } else if err != nil { - return translateError(err) - } - } - - return nil - }() - if err != nil { - return nil, err - } - - parsed, prequery, remainder, err = scanner.Parse(d.gmsCtx, remainder, true) - if err != nil { - return nil, translateError(err) - } - } - if prequery != "" { - query = prequery - } + if d.DataSource.ParamIsTrue(MultiStatementsParam) { + return d.prepareMultiStatement(query) + } else { + return d.prepareSingleStatement(query) } +} - if len(query) > 0 { - _, err := d.se.GetUnderlyingEngine().PrepareQuery(d.gmsCtx, query) - if err != nil { - return nil, translateError(err) - } +// prepareSingleStatement creates a prepared statement from |query|, returning any analysis errors, +// and if successful returns a doltStmt containing the query. +func (d *DoltConn) prepareSingleStatement(query string) (*doltStmt, error) { + _, err := d.se.GetUnderlyingEngine().PrepareQuery(d.gmsCtx, query) + if err != nil { + return nil, translateError(err) } - // Reuse the same ctx instance, but update the QueryTime to the current time. Since statements are - // executed serially on a connection, it's safe to reuse the same ctx instance and update the time. - d.gmsCtx.SetQueryTime(time.Now()) return &doltStmt{ query: query, se: d.se, @@ -88,6 +56,31 @@ func (d *DoltConn) Prepare(query string) (driver.Stmt, error) { }, nil } +// prepareMultiStatement creates a prepared statement from each individual statement in |query|, +// returning any analysis errors. Otherwise, if successful, returns a doltMultiStmt containing +// a doltStmt for each individual statement in |query|. +func (d *DoltConn) prepareMultiStatement(query string) (*doltMultiStmt, error) { + var doltMultiStmt doltMultiStmt + scanner := gms.NewMysqlParser() + + remainder := query + var err error + for remainder != "" { + _, query, remainder, err = scanner.Parse(d.gmsCtx, remainder, true) + if err != nil { + return nil, translateError(err) + } + + doltStmt, err := d.prepareSingleStatement(query) + if err != nil { + return nil, translateError(err) + } + doltMultiStmt.stmts = append(doltMultiStmt.stmts, doltStmt) + } + + return &doltMultiStmt, nil +} + // Close releases the resources held by the DoltConn instance func (d *DoltConn) Close() error { err := d.se.Close() diff --git a/driver.go b/driver.go index 84813b9..74be90f 100644 --- a/driver.go +++ b/driver.go @@ -42,7 +42,6 @@ type doltDriver struct { // // The path needs to point to a directory whose subdirectories are dolt databases. If a "Create Database" command is // run a new subdirectory will be created in this path. -// The supported parameters are func (d *doltDriver) Open(dataSource string) (driver.Conn, error) { ctx := context.Background() var fs filesys.Filesys = filesys.LocalFS @@ -89,7 +88,7 @@ func (d *doltDriver) Open(dataSource string) (driver.Conn, error) { ServerUser: "root", Autocommit: true, } - + se, err := engine.NewSqlEngine(ctx, mrEnv, seCfg) if err != nil { return nil, err @@ -122,16 +121,16 @@ func (d *doltDriver) Open(dataSource string) (driver.Conn, error) { // with initialized environments for each of those subfolder data repositories. subfolders whose name starts with '.' are // skipped. func LoadMultiEnvFromDir( - ctx context.Context, - cfg config.ReadWriteConfig, - fs filesys.Filesys, - path, version string, + ctx context.Context, + cfg config.ReadWriteConfig, + fs filesys.Filesys, + path, version string, ) (*env.MultiRepoEnv, error) { multiDbDirFs, err := fs.WithWorkingDir(path) if err != nil { return nil, errhand.VerboseErrorFromError(err) } - + return env.MultiEnvForDirectory(ctx, cfg, multiDbDirFs, version, nil) } diff --git a/go.mod b/go.mod index a2c9224..6ee6989 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.22.2 toolchain go1.22.3 require ( - github.com/dolthub/dolt/go v0.40.5-0.20240604165632-02f450318cb3 - github.com/dolthub/go-mysql-server v0.18.2-0.20240701183357-77fa27941c98 + github.com/dolthub/dolt/go v0.40.5-0.20240702155756-bcf4dd5f5cc1 + github.com/dolthub/go-mysql-server v0.18.2-0.20240702022058-d7eb602c04ee github.com/dolthub/vitess v0.0.0-20240626174323-4083c07f5e9c github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d github.com/stretchr/testify v1.8.4 @@ -121,10 +121,4 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace github.com/dolthub/dolt/go => ../dolt/go - -replace github.com/dolthub/go-mysql-server => ../go-mysql-server - -replace github.com/dolthub/vitess => ../vitess - replace github.com/google/flatbuffers => github.com/dolthub/flatbuffers v1.13.0-dh.1 diff --git a/go.sum b/go.sum index eaf6476..64d7911 100644 --- a/go.sum +++ b/go.sum @@ -246,6 +246,8 @@ github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQ github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/dolthub/dolt/go v0.40.5-0.20240702155756-bcf4dd5f5cc1 h1:zja4D6qChO7OZqh00buv9FTVu5pYzLEq1jptxpATcQE= +github.com/dolthub/dolt/go v0.40.5-0.20240702155756-bcf4dd5f5cc1/go.mod h1:QaKI/d6K38jAtq2gn11lQz+rkHECSwlbEzHyWSts+g0= github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20240212175631-02e9f99a3a9b h1:VehmKUF425NgXpRQVYMPzJx6rWZaJ2cbTwTTwXlrbiM= github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20240212175631-02e9f99a3a9b/go.mod h1:gHeHIDGU7em40EhFTliq62pExFcc1hxDTIZ9g5UqXYM= github.com/dolthub/flatbuffers v1.13.0-dh.1 h1:OWJdaPep22N52O/0xsUevxJ6Qfw1M2txCjZPOdjXybE= @@ -256,16 +258,20 @@ github.com/dolthub/fslock v0.0.3 h1:iLMpUIvJKMKm92+N1fmHVdxJP5NdyDK5bK7z7Ba2s2U= github.com/dolthub/fslock v0.0.3/go.mod h1:QWql+P17oAAMLnL4HGB5tiovtDuAjdDTPbuqx7bYfa0= github.com/dolthub/go-icu-regex v0.0.0-20230524105445-af7e7991c97e h1:kPsT4a47cw1+y/N5SSCkma7FhAPw7KeGmD6c9PBZW9Y= github.com/dolthub/go-icu-regex v0.0.0-20230524105445-af7e7991c97e/go.mod h1:KPUcpx070QOfJK1gNe0zx4pA5sicIK1GMikIGLKC168= +github.com/dolthub/go-mysql-server v0.18.2-0.20240702022058-d7eb602c04ee h1:VYwVsWT3byEtq6W8ebAVO7cNCPUKeUNr590s/U6F3wo= +github.com/dolthub/go-mysql-server v0.18.2-0.20240702022058-d7eb602c04ee/go.mod h1:JahRYjx/Py6T/bWrnTu25CaGn94Df+McAuWGEG0shwU= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63 h1:OAsXLAPL4du6tfbBgK0xXHZkOlos63RdKYS3Sgw/dfI= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63/go.mod h1:lV7lUeuDhH5thVGDCKXbatwKy2KW80L4rMT46n+Y2/Q= -github.com/dolthub/ishell v0.0.0-20221214210346-d7db0b066488 h1:0HHu0GWJH0N6a6keStrHhUAK5/o9LVfkh44pvsV4514= -github.com/dolthub/ishell v0.0.0-20221214210346-d7db0b066488/go.mod h1:ehexgi1mPxRTk0Mok/pADALuHbvATulTh6gzr7NzZto= +github.com/dolthub/ishell v0.0.0-20240701202509-2b217167d718 h1:lT7hE5k+0nkBdj/1UOSFwjWpNxf+LCApbRHgnCA17XE= +github.com/dolthub/ishell v0.0.0-20240701202509-2b217167d718/go.mod h1:ehexgi1mPxRTk0Mok/pADALuHbvATulTh6gzr7NzZto= github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71 h1:bMGS25NWAGTEtT5tOBsCuCrlYnLRKpbJVJkDbrTRhwQ= github.com/dolthub/jsonpath v0.0.2-0.20240227200619-19675ab05c71/go.mod h1:2/2zjLQ/JOOSbbSboojeg+cAwcRV0fDLzIiWch/lhqI= github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw= github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0= +github.com/dolthub/vitess v0.0.0-20240626174323-4083c07f5e9c h1:Y3M0hPCUvT+5RTNbJLKywGc9aHIRCIlg+0NOhC91GYE= +github.com/dolthub/vitess v0.0.0-20240626174323-4083c07f5e9c/go.mod h1:uBvlRluuL+SbEWTCZ68o0xvsdYZER3CEG/35INdzfJM= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= diff --git a/result.go b/result.go index 754b879..301330b 100644 --- a/result.go +++ b/result.go @@ -8,8 +8,7 @@ import ( "github.com/dolthub/go-mysql-server/sql/types" ) -//var _ driver.Result = (*doltResult)(nil) -var _ driver.RowsNextResultSet = (*doltResult)(nil) +var _ driver.Result = (*doltResult)(nil) type doltResult struct { affected int64 @@ -17,32 +16,6 @@ type doltResult struct { err error } -func (result *doltResult) Columns() []string { - //TODO implement me - panic("implement me") -} - -func (result *doltResult) Close() error { - //TODO implement me - panic("implement me") -} - -func (result *doltResult) Next(dest []driver.Value) error { - //TODO implement me - panic("implement me") -} - -func (result *doltResult) HasNextResultSet() bool { - //TODO implement me - panic("implement me") -} - -func (result *doltResult) NextResultSet() error { - //TODO implement me - panic("implement me") -} - - func newResult(gmsCtx *gms.Context, sch gms.Schema, rowItr gms.RowIter) *doltResult { var resultErr error var affected int64 diff --git a/rows.go b/rows.go index c7802f0..24fb1aa 100644 --- a/rows.go +++ b/rows.go @@ -10,7 +10,38 @@ import ( "github.com/dolthub/go-mysql-server/sql/types" ) -var _ driver.Rows = (*doltRows)(nil) +// doltMultiRows implements driver.RowsNextResultSet by aggregating a set of individual +// doltRows instances. +type doltMultiRows struct { + rowSets []*doltRows + currentRowSet int +} + +var _ driver.RowsNextResultSet = (*doltMultiRows)(nil) + +func (d *doltMultiRows) Columns() []string { + return d.rowSets[d.currentRowSet].Columns() +} + +func (d *doltMultiRows) Close() error { + return d.rowSets[d.currentRowSet].Close() +} + +func (d *doltMultiRows) Next(dest []driver.Value) error { + return d.rowSets[d.currentRowSet].Next(dest) +} + +func (d *doltMultiRows) HasNextResultSet() bool { + return d.currentRowSet < len(d.rowSets)-1 +} + +func (d *doltMultiRows) NextResultSet() error { + if d.currentRowSet+1 >= len(d.rowSets) { + return io.EOF + } + d.currentRowSet += 1 + return nil +} type doltRows struct { sch gms.Schema @@ -20,6 +51,8 @@ type doltRows struct { columns []string } +var _ driver.Rows = (*doltRows)(nil) + // Columns returns the names of the columns. The number of columns of the result is inferred from the length of the // slice. If a particular column name isn't known, an empty string should be returned for that entry. func (rows *doltRows) Columns() []string { diff --git a/statement.go b/statement.go index f4e480a..e0dcae7 100644 --- a/statement.go +++ b/statement.go @@ -12,14 +12,66 @@ import ( "strconv" ) -var _ driver.Stmt = (*doltStmt)(nil) +// doltMultiStmt represents a collection of statements to be executed against a +// Dolt database. +type doltMultiStmt struct { + stmts []*doltStmt +} + +var _ driver.Stmt = (*doltMultiStmt)(nil) + +func (d doltMultiStmt) Close() error { + var retErr error + for _, stmt := range d.stmts { + if err := stmt.Close(); err != nil { + retErr = err + } + } + + return retErr +} + +func (d doltMultiStmt) NumInput() int { + return -1 +} + +func (d doltMultiStmt) Exec(args []driver.Value) (result driver.Result, err error) { + // TODO: Do we need a doltMultiResult? Doesn't seem like the driver package + // supports multiple results from an exec statement. + for _, stmt := range d.stmts { + result, err = stmt.Exec(args) + if err != nil { + // If any error occurs, return the error and stop executing statements + return nil, err + } + } + // return the last result + return result, nil +} + +func (d doltMultiStmt) Query(args []driver.Value) (driver.Rows, error) { + var multiResultSet doltMultiRows + for _, stmt := range d.stmts { + rows, err := stmt.Query(args) + if err != nil { + // If any error occurs, return the error and stop executing statements + return nil, err + } + multiResultSet.rowSets = append(multiResultSet.rowSets, rows.(*doltRows)) + } + return &multiResultSet, nil +} + +// doltStmt represents a single statement to be executed against a Dolt database. type doltStmt struct { se *engine.SqlEngine gmsCtx *gms.Context query string } +var _ driver.Stmt = (*doltStmt)(nil) + // Close closes the statement. func (stmt *doltStmt) Close() error { return nil From 833e8d60f1fab3aa5298be255f00aa3dac88337d Mon Sep 17 00:00:00 2001 From: Jason Fulghum Date: Mon, 8 Jul 2024 16:44:35 -0700 Subject: [PATCH 12/23] Making the driver implementation match the MySQL driver implementation --- conn.go | 5 ---- rows.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++-- smoke_test.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++-- statement.go | 51 +++++++++++++++++++++++++++------------- 4 files changed, 160 insertions(+), 25 deletions(-) diff --git a/conn.go b/conn.go index 2489874..ab51853 100644 --- a/conn.go +++ b/conn.go @@ -44,11 +44,6 @@ func (d *DoltConn) Prepare(query string) (driver.Stmt, error) { // prepareSingleStatement creates a prepared statement from |query|, returning any analysis errors, // and if successful returns a doltStmt containing the query. func (d *DoltConn) prepareSingleStatement(query string) (*doltStmt, error) { - _, err := d.se.GetUnderlyingEngine().PrepareQuery(d.gmsCtx, query) - if err != nil { - return nil, translateError(err) - } - return &doltStmt{ query: query, se: d.se, diff --git a/rows.go b/rows.go index 24fb1aa..6b594d1 100644 --- a/rows.go +++ b/rows.go @@ -23,8 +23,18 @@ func (d *doltMultiRows) Columns() []string { return d.rowSets[d.currentRowSet].Columns() } +// Close implements the driver.Rows interface. When Close is called on a doltMultiRows instance, +// it will close all individual doltRows instances that it contains. If any errors are encountered +// while closing the individual row sets, the first error will be returned, after attempting to close +// all row sets. func (d *doltMultiRows) Close() error { - return d.rowSets[d.currentRowSet].Close() + var retErr error + for _, rowSet := range d.rowSets { + if err := rowSet.Close(); err != nil { + retErr = err + } + } + return retErr } func (d *doltMultiRows) Next(dest []driver.Value) error { @@ -39,8 +49,12 @@ func (d *doltMultiRows) NextResultSet() error { if d.currentRowSet+1 >= len(d.rowSets) { return io.EOF } + + // Move to the next row set. If we encountered an error running the statement earlier and saved + // an error in the row set, return that error now that the result set with the error has been requested. + // This is to match the MySQL driver's behavior. d.currentRowSet += 1 - return nil + return d.rowSets[d.currentRowSet].err } type doltRows struct { @@ -49,6 +63,9 @@ type doltRows struct { gmsCtx *gms.Context columns []string + + // err holds the error encountered while trying to retrieve this result set + err error } var _ driver.Rows = (*doltRows)(nil) @@ -68,6 +85,10 @@ func (rows *doltRows) Columns() []string { // Close closes the rows iterator. func (rows *doltRows) Close() error { + if rows.rowIter == nil { + return nil + } + return translateError(rows.rowIter.Close(rows.gmsCtx)) } @@ -118,3 +139,42 @@ func (rows *doltRows) Next(dest []driver.Value) error { return nil } + +// peekableRowIter wrap another gms.RowIter and allows the caller to peek at results, without disturbing the order +// that results are returned from the Next() method. +type peekableRowIter struct { + iter gms.RowIter + peeks []gms.Row +} + +var _ gms.RowIter = (*peekableRowIter)(nil) + +// Peek returns the next row from this row iterator, without causing that row to be skipped from future calls +// to Next(). There is no limit on how many rows can be peeked. +func (p *peekableRowIter) Peek(ctx *gms.Context) (gms.Row, error) { + next, err := p.iter.Next(ctx) + if err != nil { + return nil, err + } + p.peeks = append(p.peeks, next) + + return next, nil +} + +// Next implements gms.RowIter +func (p *peekableRowIter) Next(ctx *gms.Context) (gms.Row, error) { + // TODO: If peek returned an error, we need to return that error here + // Although... calling Next() again *should* still return the error? + if len(p.peeks) > 0 { + peek := p.peeks[0] + p.peeks = p.peeks[1:] + return peek, nil + } + + return p.iter.Next(ctx) +} + +// Close implements gms.RowIter +func (p *peekableRowIter) Close(ctx *gms.Context) error { + return p.iter.Close(ctx) +} diff --git a/smoke_test.go b/smoke_test.go index 6d138d8..92e5bb4 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -35,6 +35,12 @@ func TestMultiStatements(t *testing.T) { var id int var name string + // Move to the third result set; don't bother checking the results from the two insert statements. + // TODO: The MySQL driver does not require calling NextResultSet to move past insert statements – it detects that the + // result sets are empty, and skips any empty result sets when working with multi-statements. + require.True(t, rows.NextResultSet()) + require.True(t, rows.NextResultSet()) + require.True(t, rows.Next()) require.NoError(t, rows.Scan(&id, &name)) require.Equal(t, 1, id) @@ -51,8 +57,34 @@ func TestMultiStatements(t *testing.T) { require.NoError(t, rows.Err()) require.NoError(t, rows.Close()) - _, err = conn.QueryContext(ctx, "select * from testtable; select * from doesnotexist; select * from testtable") - require.Error(t, err) + rows, err = conn.QueryContext(ctx, "select * from testtable; select * from doesnotexist; select * from testtable") + require.NoError(t, err) + + // The first result set contains all the rows from testtable + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&id, &name)) + require.Equal(t, 1, id) + require.Equal(t, "aaron", name) + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&id, &name)) + require.Equal(t, 2, id) + require.Equal(t, "brian", name) + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&id, &name)) + require.Equal(t, 3, id) + require.Equal(t, "tim", name) + require.False(t, rows.Next()) + require.NoError(t, rows.Err()) + + // The second result set has an error + require.False(t, rows.NextResultSet()) + require.NotNil(t, rows.Err()) + require.Equal(t, "Error 1146: table not found: doesnotexist", rows.Err().Error()) + + // The third result set should have more rows... but we can't access them after the + // error in the second result set. This is the same behavior as the MySQL driver + require.False(t, rows.NextResultSet()) + require.NotNil(t, rows.Err()) require.NoError(t, conn.Close()) } @@ -64,6 +96,9 @@ func TestMultiStatementsStoredProc(t *testing.T) { ctx := context.Background() rows, err := conn.QueryContext(ctx, "create procedure p() begin select 1; end; call p(); call p(); call p();") require.NoError(t, err) + + // Advance to the second result set and check its rows + require.True(t, rows.NextResultSet()) for rows.Next() { var i int err = rows.Scan(&i) @@ -71,6 +106,27 @@ func TestMultiStatementsStoredProc(t *testing.T) { require.Equal(t, 1, i) } require.NoError(t, rows.Err()) + + // Advance to the third result set and check its rows + require.True(t, rows.NextResultSet()) + for rows.Next() { + var i int + err = rows.Scan(&i) + require.NoError(t, err) + require.Equal(t, 1, i) + } + require.NoError(t, rows.Err()) + + // Advance to the fourth result set and check its rows + require.True(t, rows.NextResultSet()) + for rows.Next() { + var i int + err = rows.Scan(&i) + require.NoError(t, err) + require.Equal(t, 1, i) + } + require.NoError(t, rows.Err()) + require.NoError(t, rows.Close()) } @@ -86,6 +142,11 @@ func TestMultiStatementsTrigger(t *testing.T) { rows, err := conn.QueryContext(ctx, "create trigger trig before insert on t for each row begin set new.j = new.j * 100; end; insert into t values (1, 2); select * from t;") require.NoError(t, err) + + // Advance to the third result set to test its results + require.True(t, rows.NextResultSet()) + require.True(t, rows.NextResultSet()) + for rows.Next() { var i, j int err = rows.Scan(&i, &j) diff --git a/statement.go b/statement.go index e0dcae7..ff898cd 100644 --- a/statement.go +++ b/statement.go @@ -2,14 +2,12 @@ package embedded import ( "database/sql/driver" - - "github.com/dolthub/vitess/go/sqltypes" + "strconv" "github.com/dolthub/dolt/go/cmd/dolt/commands/engine" gms "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/vitess/go/sqltypes" querypb "github.com/dolthub/vitess/go/vt/proto/query" - - "strconv" ) // doltMultiStmt represents a collection of statements to be executed against a @@ -35,18 +33,22 @@ func (d doltMultiStmt) NumInput() int { return -1 } -func (d doltMultiStmt) Exec(args []driver.Value) (result driver.Result, err error) { - // TODO: Do we need a doltMultiResult? Doesn't seem like the driver package - // supports multiple results from an exec statement. +func (d doltMultiStmt) Exec(args []driver.Value) (result driver.Result, retErr error) { for _, stmt := range d.stmts { + var err error result, err = stmt.Exec(args) - if err != nil { - // If any error occurs, return the error and stop executing statements - return nil, err + if err != nil && retErr == nil { + // If any error occurs, record the first error, but continue executing all statements + retErr = err } } - // return the last result + // Return the first error encountered, if there was one + if retErr != nil { + return nil, retErr + } + + // Otherwise, return the last result, to match the MySQL driver's behavior return result, nil } @@ -55,12 +57,20 @@ func (d doltMultiStmt) Query(args []driver.Value) (driver.Rows, error) { for _, stmt := range d.stmts { rows, err := stmt.Query(args) if err != nil { - // If any error occurs, return the error and stop executing statements - return nil, err + // To match the MySQL driver's behavior, we attempt to execute all statements in a multi-statement + // query, even if some statements fail. If the first statement errors out, then we return that error, + // otherwise we save the error from any statements, so that they can be returned from NextResultSet() + // with the caller requests that result set. + rows = &doltRows{err: err} } multiResultSet.rowSets = append(multiResultSet.rowSets, rows.(*doltRows)) } - return &multiResultSet, nil + + if multiResultSet.rowSets[0].err != nil { + return nil, multiResultSet.rowSets[0].err + } else { + return &multiResultSet, nil + } } // doltStmt represents a single statement to be executed against a Dolt database. @@ -135,14 +145,23 @@ func (stmt *doltStmt) Query(args []driver.Value) (driver.Rows, error) { } else { sch, rowIter, err = stmt.se.Query(stmt.gmsCtx, stmt.query) } - if err != nil { return nil, translateError(err) } + // Wrap the result iterator in a peekableRowIter and call Peek() to read the first row from the result iterator. + // This is necessary for insert operations, since the insert happens inside the result iterator logic. Without + // calling this now, insert statements and some DML statements (e.g. CREATE PROCEDURE) would not be executed yet, + // and future statements in a multi-statement query that depend on those results would fail. + // Note that we don't worry about the result or the error here – we just want to exercise the iterator code to + // ensure the statement is executed. If an error does occur, we want that error to be returned in the Next() + // codepath, not here. + peekIter := peekableRowIter{iter: rowIter} + _, _ = peekIter.Peek(stmt.gmsCtx) + return &doltRows{ sch: sch, - rowIter: rowIter, + rowIter: &peekIter, gmsCtx: stmt.gmsCtx, }, nil } From 245e7ff6b15144fb7d567571ed39039e8ec5d09d Mon Sep 17 00:00:00 2001 From: Jason Fulghum Date: Tue, 9 Jul 2024 10:46:51 -0700 Subject: [PATCH 13/23] Skipping empty statements in multistatement mode --- conn.go | 6 +++++- smoke_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/conn.go b/conn.go index ab51853..4ca365e 100644 --- a/conn.go +++ b/conn.go @@ -9,6 +9,7 @@ import ( "github.com/dolthub/dolt/go/cmd/dolt/commands/engine" gms "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/vitess/go/vt/sqlparser" ) var _ driver.Conn = (*DoltConn)(nil) @@ -62,7 +63,10 @@ func (d *DoltConn) prepareMultiStatement(query string) (*doltMultiStmt, error) { var err error for remainder != "" { _, query, remainder, err = scanner.Parse(d.gmsCtx, remainder, true) - if err != nil { + if err == sqlparser.ErrEmpty { + // Skip over any empty statements + continue + } else if err != nil { return nil, translateError(err) } diff --git a/smoke_test.go b/smoke_test.go index 92e5bb4..bbf1354 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -89,6 +89,44 @@ func TestMultiStatements(t *testing.T) { require.NoError(t, conn.Close()) } +// TestMultiStatementsWithEmptyStatements tests that any empty statements in a multistatement query are skipped over. +// This includes statements that are entirely empty, as well as statements that contain only comments. +func TestMultiStatementsWithEmptyStatements(t *testing.T) { + conn, cleanupFunc := initializeTestDatabaseConnection(t, false) + defer cleanupFunc() + + var v int + ctx := context.Background() + + // Test that empty statements don't return errors and don't return result sets + rows, err := conn.QueryContext(ctx, "select 42 from dual; # This is an empty statement") + require.NoError(t, err) + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&v)) + require.Equal(t, 42, v) + require.NoError(t, rows.Err()) + require.False(t, rows.Next()) + require.False(t, rows.NextResultSet()) + require.NoError(t, rows.Close()) + + // Test another form of empty statement + rows, err = conn.QueryContext(ctx, "select 42 from dual; ; ; ; select 24 from dual; ;") + require.NoError(t, err) + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&v)) + require.Equal(t, 42, v) + require.NoError(t, rows.Err()) + require.False(t, rows.Next()) + require.True(t, rows.NextResultSet()) + require.NoError(t, err) + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&v)) + require.Equal(t, 24, v) + require.NoError(t, rows.Err()) + require.False(t, rows.Next()) + require.NoError(t, rows.Close()) +} + func TestMultiStatementsStoredProc(t *testing.T) { conn, cleanupFunc := initializeTestDatabaseConnection(t, false) defer cleanupFunc() From e46f207acd6eec7dbd5f5a5a5e106a126db8c4ea Mon Sep 17 00:00:00 2001 From: Jason Fulghum Date: Tue, 9 Jul 2024 11:13:43 -0700 Subject: [PATCH 14/23] Reverting changes to example file --- example/main.go | 311 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 222 insertions(+), 89 deletions(-) diff --git a/example/main.go b/example/main.go index c15e0ec..65662b7 100644 --- a/example/main.go +++ b/example/main.go @@ -1,114 +1,247 @@ package main import ( + "bytes" "context" "database/sql" + "encoding/base64" "fmt" - "net/url" - - "github.com/go-sql-driver/mysql" + "os" + "reflect" + "strconv" + "strings" + "time" _ "github.com/dolthub/driver" ) +func errExit(wrapFormat string, err error) { + if err != nil { + if len(wrapFormat) > 0 { + err = fmt.Errorf(wrapFormat, err) + } + + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } +} + func main() { + if len(os.Args) != 2 { + fmt.Println("usage: example file:///path/to/doltdb?commitname=&commitemail=&database=&multistatements=") + return + } + ctx := context.Background() - if ctx == nil {} - - dir := "./" - params := url.Values{ - "database": []string{"tmp"}, - "commitname": []string{"root"}, - "commitemail": []string{"root@dolthub.com"}, - "multistatements": []string{"true"}, + + dataSource := os.Args[1] + fmt.Println("Connecting to", dataSource) + db, err := sql.Open("dolt", dataSource) + errExit("failed to open database using the dolt driver: %w", err) + + err = printQuery(ctx, db, "CREATE DATABASE IF NOT EXISTS testdb; USE testdb;") + errExit("", err) + + err = printQuery(ctx, db, "USE testdb;") + errExit("", err) + + err = printQuery(ctx, db, `CREATE TABLE IF NOT EXISTS t2( + pk int primary key auto_increment, + c1 varchar(32) + )`) + errExit("", err) + + printQuery(ctx, db, "SHOW TABLES;") + + stmt, err := db.PrepareContext(ctx, "Insert into t2 (c1) values (?);") + errExit("", err) + defer stmt.Close() + + result, err := stmt.ExecContext(ctx, "test") + errExit("", err) + + fmt.Println(result.LastInsertId()) + + err = printQuery(ctx, db, `CREATE TABLE IF NOT EXISTS t1 ( + pk int PRIMARY KEY, + c1 varchar(512), + c2 float, + c3 bool, + c4 datetime + );`) + errExit("", err) + + err = printQuery(ctx, db, "SELECT * FROM t1;") + errExit("", err) + + err = printQuery(ctx, db, `REPLACE INTO t1 VALUES + (1, 'this is a test', 0, 0, '1998-01-23 12:45:56'), + (2, 'it is only a test', 1.0, 1, '2010-12-31 01:15:00'), + (3, NULL, 3.335, 0, NULL), + (4, 'something something', 3.5, 1, '2015-04-03 14:00:45');`) + errExit("", err) + + err = printQuery(ctx, db, "SELECT * FROM t1;") + errExit("", err) + + err = printQuery(ctx, db, "DELETE FROM t1;") + errExit("", err) + + rows := [][]interface{}{ + {1, "this is a test", 0, 0, time.Date(1998, 1, 23, 12, 45, 56, 0, time.UTC)}, + {2, "it is only a test", 1.0, 1, time.Date(2010, 12, 31, 1, 15, 0, 0, time.UTC)}, + {3, nil, 3.335, 0, nil}, + {4, "something something", 3.5, 1, time.Date(2015, 4, 3, 14, 0, 45, 0, time.UTC)}, } - dsn := url.URL{Scheme: "file", Path: dir, RawQuery: params.Encode()} - db, err := sql.Open("dolt", dsn.String()) + + tx, err := db.Begin() + errExit("", err) + + err = prepareAndExec(ctx, tx, "INSERT INTO t1 VALUES (?, ?, ?, ?, ?)", rows) + errExit("", err) + + err = printQuery(ctx, tx, `INSERT INTO t1 VALUES (5, "blah", 4.0, 0, now()); +INSERT INTO t1 VALUES (6, 'aoeu', 7.0, 1, now()), (7,"aoeu aoeu", 8.1, 0, now()); +SELECT * FROM t1;`) + + fmt.Println("Query Before Rollback") + err = printQuery(ctx, tx, "SELECT * FROM t1;") + + //err = tx.Rollback() + //errExit("", err) + //fmt.Println("Query After Rollback") + + err = tx.Commit() + errExit("", err) + fmt.Println("Query After Commit") + + err = printQuery(ctx, db, "SELECT * FROM t1;") + errExit("", err) +} + +type Queryable interface { + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) +} + +func printQuery(ctx context.Context, queryable Queryable, query string) error { + fmt.Println("query:", query) + rows, err := queryable.QueryContext(ctx, query) if err != nil { - panic(err) + return fmt.Errorf("query '%s' failed: %w", query, err) } - db.Ping() - - cfg := mysql.Config{ - User: "root", - Passwd: "root", - Net: "tcp", - Addr: "127.0.0.1:3306", - DBName: "test_db", - AllowNativePasswords: true, - } - if cfg.Addr == "" {} - db, err = sql.Open("mysql", cfg.FormatDSN()) + defer rows.Close() + + fmt.Println("results:") + err = printRows(rows) if err != nil { - panic(err) + return err } - db.Ping() - - defer db.Close() - - //var res sql.Result - //res, err = db.ExecContext(ctx, "select 1; select 2; select 3;") - //if err != nil { - // panic(err) - //} - //numRows, err := res.RowsAffected() - //if err != nil { - // panic(err) - //} - //fmt.Sprintf("Rows Affected: %d", numRows) - - var rows *sql.Rows - rows, err = db.QueryContext(ctx, "select 1;") + + fmt.Println() + return nil +} + +func printRows(rows *sql.Rows) error { + cols, err := rows.Columns() if err != nil { - panic(err) + return fmt.Errorf("Failed to get columns: %w", err) } - var s int - for { - fmt.Printf("Result Set #%d\n", s) - for rows.Next() { - var i int - err = rows.Scan(&i) - if err != nil { - panic(err) + + fmt.Println(strings.Join(cols, "|")) + + for rows.Next() { + values := make([]interface{}, len(cols)) + var generic = reflect.TypeOf(values).Elem() + for i := 0; i < len(cols); i++ { + values[i] = reflect.New(generic).Interface() + } + + err = rows.Scan(values...) + if err != nil { + return fmt.Errorf("scan failed: %w", err) + } + + result := bytes.NewBuffer(nil) + for i := 0; i < len(cols); i++ { + if i != 0 { + result.WriteString("|") } - println(i) + + var rawValue = *(values[i].(*interface{})) + switch val := rawValue.(type) { + case string: + result.WriteString(val) + case int: + result.WriteString(strconv.FormatInt(int64(val), 10)) + case int8: + result.WriteString(strconv.FormatInt(int64(val), 10)) + case int16: + result.WriteString(strconv.FormatInt(int64(val), 10)) + case int32: + result.WriteString(strconv.FormatInt(int64(val), 10)) + case int64: + result.WriteString(strconv.FormatInt(val, 10)) + case uint: + result.WriteString(strconv.FormatUint(uint64(val), 10)) + case uint8: + result.WriteString(strconv.FormatUint(uint64(val), 10)) + case uint16: + result.WriteString(strconv.FormatUint(uint64(val), 10)) + case uint32: + result.WriteString(strconv.FormatUint(uint64(val), 10)) + case uint64: + result.WriteString(strconv.FormatUint(val, 10)) + case float32: + result.WriteString(strconv.FormatFloat(float64(val), 'f', 2, 64)) + case float64: + result.WriteString(strconv.FormatFloat(val, 'f', 2, 64)) + case bool: + if val { + result.WriteString("true") + } else { + result.WriteString("false") + } + case []byte: + enc := base64.NewEncoder(base64.URLEncoding, result) + _, err := enc.Write(val) + errExit("failed to base64 encode blob: %w", err) + case time.Time: + timeStr := val.Format(time.RFC3339) + result.WriteString(timeStr) + } + } + + fmt.Println(result.String()) + } + + return nil +} + +type Preparable interface { + PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) +} + +func prepareAndExec(ctx context.Context, prepable Preparable, query string, vals [][]interface{}) error { + stmt, err := prepable.PrepareContext(ctx, query) + if err != nil { + return err + } + + for i := range vals { + result, err := stmt.ExecContext(ctx, vals[i]...) + if err != nil { + return fmt.Errorf("failed to execute prepared statement '%s' with parameters: %v - %w", query, vals[i], err) + } + + affected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get num rows affected: %w", err) } - s++ - if !rows.NextResultSet() { - break + if affected != 1 { + return fmt.Errorf("expected '%s' to affect 1 row but it affected %d; params: %v - %w", query, affected, vals[i], err) } } - //defer db.ExecContext(ctx, "drop procedure p") - return - - //cfg := mysql.Config{ - // User: "root", - // Passwd: "", - // Net: "tcp", - // Addr: "127.0.0.1:3306", - // DBName: "tmp", - // AllowNativePasswords: true, - //} - //// Get a database handle. - //db, err = sql.Open("mysql", cfg.FormatDSN()) - //if err != nil { - // panic(err) - //} - //defer db.Close() - // - //var rows *sql.Rows - //rows, err = db.QueryContext(ctx, "delimiter //") - //if err != nil { - // panic(err) - //} - //for rows.Next() { - // var i int - // err = rows.Scan(&i) - // if err != nil { - // panic(err) - // } - // fmt.Println(i) - //} -} \ No newline at end of file + return nil +} From cadb8b30ed0f1267ca4c82052e1349d82b2357a6 Mon Sep 17 00:00:00 2001 From: Jason Fulghum Date: Tue, 9 Jul 2024 13:02:01 -0700 Subject: [PATCH 15/23] Bug fix for off-by-one bug in multistatement parsing: pulled in new version of Vitess dependency and added a new test. --- go.mod | 2 +- go.sum | 2 ++ smoke_test.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 6ee6989..fe34147 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.3 require ( github.com/dolthub/dolt/go v0.40.5-0.20240702155756-bcf4dd5f5cc1 github.com/dolthub/go-mysql-server v0.18.2-0.20240702022058-d7eb602c04ee - github.com/dolthub/vitess v0.0.0-20240626174323-4083c07f5e9c + github.com/dolthub/vitess v0.0.0-20240709194214-7926ea9d425d github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d github.com/stretchr/testify v1.8.4 gorm.io/driver/mysql v1.5.6 diff --git a/go.sum b/go.sum index 64d7911..aed7f82 100644 --- a/go.sum +++ b/go.sum @@ -272,6 +272,8 @@ github.com/dolthub/swiss v0.2.1 h1:gs2osYs5SJkAaH5/ggVJqXQxRXtWshF6uE0lgR/Y3Gw= github.com/dolthub/swiss v0.2.1/go.mod h1:8AhKZZ1HK7g18j7v7k6c5cYIGEZJcPn0ARsai8cUrh0= github.com/dolthub/vitess v0.0.0-20240626174323-4083c07f5e9c h1:Y3M0hPCUvT+5RTNbJLKywGc9aHIRCIlg+0NOhC91GYE= github.com/dolthub/vitess v0.0.0-20240626174323-4083c07f5e9c/go.mod h1:uBvlRluuL+SbEWTCZ68o0xvsdYZER3CEG/35INdzfJM= +github.com/dolthub/vitess v0.0.0-20240709194214-7926ea9d425d h1:qifIBMiYOCw/OLczNMBDg5ZMPEcEjrj5kSDeoyMXNBY= +github.com/dolthub/vitess v0.0.0-20240709194214-7926ea9d425d/go.mod h1:uBvlRluuL+SbEWTCZ68o0xvsdYZER3CEG/35INdzfJM= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= diff --git a/smoke_test.go b/smoke_test.go index bbf1354..4e858ff 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -89,6 +89,35 @@ func TestMultiStatements(t *testing.T) { require.NoError(t, conn.Close()) } +// TestMultiStatementsWithNoSpaces tests that multistatements are parsed correctly, even when +// there is no space between the statement delimiter and the next statement. +func TestMultiStatementsWithNoSpaces(t *testing.T) { + conn, cleanupFunc := initializeTestDatabaseConnection(t, false) + defer cleanupFunc() + + var v int + ctx := context.Background() + rows, err := conn.QueryContext(ctx, "select 42 from dual;select 43 from dual;") + + // Check the first result set + require.NoError(t, err) + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&v)) + require.Equal(t, 42, v) + require.NoError(t, rows.Err()) + require.False(t, rows.Next()) + + // Check the second result set + require.True(t, rows.NextResultSet()) + require.NoError(t, err) + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&v)) + require.Equal(t, 43, v) + require.NoError(t, rows.Err()) + require.False(t, rows.Next()) + require.NoError(t, rows.Close()) +} + // TestMultiStatementsWithEmptyStatements tests that any empty statements in a multistatement query are skipped over. // This includes statements that are entirely empty, as well as statements that contain only comments. func TestMultiStatementsWithEmptyStatements(t *testing.T) { From cbeff3c6ded4110496391dc29350814dbb65ee7a Mon Sep 17 00:00:00 2001 From: Jason Fulghum Date: Tue, 9 Jul 2024 14:12:23 -0700 Subject: [PATCH 16/23] Updating tests to allow running against a MySQL database, to compare the Dolt driver's behavior to the MySQL driver, and fixing tests to run correctly against the MySQL driver. --- smoke_test.go | 118 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 97 insertions(+), 21 deletions(-) diff --git a/smoke_test.go b/smoke_test.go index 4e858ff..ac465e1 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -9,10 +9,39 @@ import ( "testing" "time" + _ "github.com/go-sql-driver/mysql" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// runTestsAgainstMySQL can be set to true to run tests against a MySQL database using the MySQL driver. +// This is useful to test behavior compatibility between the Dolt driver and the MySQL driver. When +// turning this option on, you will need to modify code in the initializeTestDatabaseConnection function +// and specify a valid DSN to an existing MySQL database. +var runTestsAgainstMySQL = false + +// TestPreparedStatements tests that values can be plugged into "?" placeholders in queries. +func TestPreparedStatements(t *testing.T) { + conn, cleanupFunc := initializeTestDatabaseConnection(t, false) + defer cleanupFunc() + + ctx := context.Background() + rows, err := conn.QueryContext(ctx, "create table prepTest (id int, name varchar(256));") + require.NoError(t, err) + for rows.Next() { + } + require.NoError(t, rows.Err()) + require.NoError(t, rows.Close()) + + rows, err = conn.QueryContext(ctx, "insert into prepTest VALUES (?, ?);", 10, "foo") + require.NoError(t, err) + for rows.Next() { + } + require.NoError(t, rows.Err()) + require.NoError(t, rows.Close()) + +} + func TestMultiStatements(t *testing.T) { conn, cleanupFunc := initializeTestDatabaseConnection(t, false) defer cleanupFunc() @@ -36,10 +65,12 @@ func TestMultiStatements(t *testing.T) { var name string // Move to the third result set; don't bother checking the results from the two insert statements. - // TODO: The MySQL driver does not require calling NextResultSet to move past insert statements – it detects that the - // result sets are empty, and skips any empty result sets when working with multi-statements. - require.True(t, rows.NextResultSet()) - require.True(t, rows.NextResultSet()) + // NOTE: The MySQL driver does not require calling NextResultSet to move past insert statements – it detects that + // the result sets are empty, and skips any empty result sets when working with multi-statements. + if !runTestsAgainstMySQL { + require.True(t, rows.NextResultSet()) + require.True(t, rows.NextResultSet()) + } require.True(t, rows.Next()) require.NoError(t, rows.Scan(&id, &name)) @@ -79,7 +110,12 @@ func TestMultiStatements(t *testing.T) { // The second result set has an error require.False(t, rows.NextResultSet()) require.NotNil(t, rows.Err()) - require.Equal(t, "Error 1146: table not found: doesnotexist", rows.Err().Error()) + // MySQL returns a slightly different error message than Dolt + if !runTestsAgainstMySQL { + require.Equal(t, "Error 1146: table not found: doesnotexist", rows.Err().Error()) + } else { + require.Equal(t, "Error 1146 (42S02): Table 'testdb.doesnotexist' doesn't exist", rows.Err().Error()) + } // The third result set should have more rows... but we can't access them after the // error in the second result set. This is the same behavior as the MySQL driver @@ -146,13 +182,19 @@ func TestMultiStatementsWithEmptyStatements(t *testing.T) { require.Equal(t, 42, v) require.NoError(t, rows.Err()) require.False(t, rows.Next()) - require.True(t, rows.NextResultSet()) - require.NoError(t, err) - require.True(t, rows.Next()) - require.NoError(t, rows.Scan(&v)) - require.Equal(t, 24, v) - require.NoError(t, rows.Err()) - require.False(t, rows.Next()) + + // NOTE: The MySQL driver does not allow moving past empty statements to the next result set + if !runTestsAgainstMySQL { + require.True(t, rows.NextResultSet()) + require.NoError(t, err) + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&v)) + require.Equal(t, 24, v) + require.NoError(t, rows.Err()) + require.False(t, rows.Next()) + } + + require.False(t, rows.NextResultSet()) require.NoError(t, rows.Close()) } @@ -165,7 +207,11 @@ func TestMultiStatementsStoredProc(t *testing.T) { require.NoError(t, err) // Advance to the second result set and check its rows - require.True(t, rows.NextResultSet()) + // NOTE: The MySQL driver automatically positions the first result set at the second statement, + // since the first statement has an empty result set. + if !runTestsAgainstMySQL { + require.True(t, rows.NextResultSet()) + } for rows.Next() { var i int err = rows.Scan(&i) @@ -211,8 +257,12 @@ func TestMultiStatementsTrigger(t *testing.T) { require.NoError(t, err) // Advance to the third result set to test its results - require.True(t, rows.NextResultSet()) - require.True(t, rows.NextResultSet()) + // NOTE: The MySQL driver automatically positions the first result set at the third statement, because + // it skips empty result sets. + if !runTestsAgainstMySQL { + require.True(t, rows.NextResultSet()) + require.True(t, rows.NextResultSet()) + } for rows.Next() { var i, j int @@ -333,11 +383,11 @@ insert into testtable values ('b', 'a,c', '{"key": 42}', 'data', 'text', Point(5 ptrs[i] = &vals[i] } require.NoError(t, row.Scan(ptrs...)) - require.Equal(t, "b", vals[0]) - require.Equal(t, "a,c", vals[1]) - require.Equal(t, `{"key": 42}`, vals[2]) - require.Equal(t, []byte(`data`), vals[3]) - require.Equal(t, "text", vals[4]) + require.EqualValues(t, "b", vals[0]) + require.EqualValues(t, "a,c", vals[1]) + require.EqualValues(t, `{"key": 42}`, vals[2]) + require.EqualValues(t, []byte(`data`), vals[3]) + require.EqualValues(t, "text", vals[4]) require.IsType(t, []byte(nil), vals[5]) require.IsType(t, time.Time{}, vals[6]) } @@ -369,10 +419,36 @@ func initializeTestDatabaseConnection(t *testing.T, clientFoundRows bool) (conn require.NoError(t, err) require.NoError(t, db.PingContext(ctx)) + // Setting runTestsAgainstMySQL to true allows you to point these tests at a MySQL database and use the + // MySQL driver. This is useful to compare the behavior of the Dolt driver to the MySQL driver. Ideally, + // we want the Dolt driver to have the same semantics/behavior as the MySQL driver, so that customers + // familiar with using the MySQL driver, or code already using the MySQL driver, can easily switch over + // to the Dolt driver. + // Note that you have to manually configure this and plug in a valid MySQL DSN to run the tests this way. + if runTestsAgainstMySQL { + dsn := "root@tcp(localhost:3306)/?charset=utf8mb4&parseTime=True&loc=Local&multiStatements=true" + if clientFoundRows { + dsn += "&clientFoundRows=true" + } + db, err = sql.Open("mysql", dsn) + require.NoError(t, err) + require.NoError(t, db.PingContext(ctx)) + } + conn, err = db.Conn(ctx) require.NoError(t, err) - res, err := conn.ExecContext(ctx, "create database testdb") + res, err := conn.ExecContext(ctx, "drop database if exists testdb") + require.NoError(t, err) + _, err = res.RowsAffected() + require.NoError(t, err) + + res, err = conn.ExecContext(ctx, "create database testdb") + require.NoError(t, err) + _, err = res.RowsAffected() + require.NoError(t, err) + + res, err = conn.ExecContext(ctx, "use testdb") require.NoError(t, err) _, err = res.RowsAffected() require.NoError(t, err) From 7fda0602aaf53a0e02aa19c40aedbcace66966a2 Mon Sep 17 00:00:00 2001 From: Jason Fulghum Date: Tue, 9 Jul 2024 15:55:49 -0700 Subject: [PATCH 17/23] Adding tests for Query and Exec behavior --- smoke_test.go | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++ statement.go | 28 ++++++-------- 2 files changed, 114 insertions(+), 16 deletions(-) diff --git a/smoke_test.go b/smoke_test.go index ac465e1..7bbf746 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -125,6 +125,108 @@ func TestMultiStatements(t *testing.T) { require.NoError(t, conn.Close()) } +// TestMultiStatementsExecContext tests that using ExecContext to run a multi-statement query works as expected and +// matches the behavior of the MySQL driver. +func TestMultiStatementsExecContext(t *testing.T) { + conn, cleanupFunc := initializeTestDatabaseConnection(t, false) + defer cleanupFunc() + + ctx := context.Background() + _, err := conn.ExecContext(ctx, "CREATE TABLE example_table (id int, name varchar(256));") + require.NoError(t, err) + + // ExecContext returns the results from the LAST statement executed. This differs from the behavior for QueryContext. + result, err := conn.ExecContext(ctx, "INSERT into example_table VALUES (999, 'boo'); "+ + "INSERT into example_table VALUES (998, 'foo'); INSERT into example_table VALUES (997, 'goo'), (996, 'loo');") + require.NoError(t, err) + rowsAffected, err := result.RowsAffected() + require.NoError(t, err) + require.EqualValues(t, 2, rowsAffected) + + // ExecContext returns an error if ANY of the statements can't be executed. This also differs from the behavior of QueryContext. + _, err = conn.ExecContext(ctx, "INSERT into example_table VALUES (100, 'woo'); "+ + "INSERT into example_table VALUES (1, 2, 'too many'); SET @allStatementsExecuted=1;") + require.NotNil(t, err) + if !runTestsAgainstMySQL { + require.Equal(t, "Error 1105: number of values does not match number of columns provided", err.Error()) + } else { + require.Equal(t, "Error 1136 (21S01): Column count doesn't match value count at row 1", err.Error()) + } + + // Once an error occurs, additional statements are NOT executed. This code tests that the last SET statement + // above was NOT executed. + rows, err := conn.QueryContext(ctx, "SELECT @allStatementsExecuted;") + var v any + require.NoError(t, err) + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&v)) + require.Nil(t, v) + require.NoError(t, rows.Close()) +} + +// TestMultiStatementsQueryContext tests that using QueryContext to run a multi-statement query works as expected and +// matches the behavior of the MySQL driver. +func TestMultiStatementsQueryContext(t *testing.T) { + conn, cleanupFunc := initializeTestDatabaseConnection(t, false) + defer cleanupFunc() + + // QueryContext returns the results from the FIRST statement executed. This differs from the behavior for ExecContext. + ctx := context.Background() + rows, err := conn.QueryContext(ctx, "SELECT 1 FROM dual; SELECT 2 FROM dual; ") + require.NoError(t, err) + require.NoError(t, rows.Err()) + + var v any + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&v)) + require.EqualValues(t, 1, v) + require.False(t, rows.Next()) + + require.True(t, rows.NextResultSet()) + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&v)) + require.EqualValues(t, 2, v) + require.False(t, rows.Next()) + + require.False(t, rows.NextResultSet()) + require.NoError(t, rows.Close()) + + // QueryContext returns an error only if the FIRST statement can't be executed. + rows, err = conn.QueryContext(ctx, "SELECT * FROM no_table; SELECT 42 FROM dual;") + require.Nil(t, rows) + require.NotNil(t, err) + if !runTestsAgainstMySQL { + require.Equal(t, "Error 1146: table not found: no_table", err.Error()) + } else { + require.Equal(t, "Error 1146 (42S02): Table 'testdb.no_table' doesn't exist", err.Error()) + } + + // To access the error for statements after the first statement, you must use rows.Err() + rows, err = conn.QueryContext(ctx, "SELECT 42 FROM dual; SELECT * FROM no_table; SET @allStatementsExecuted=1;") + require.NoError(t, err) + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&v)) + require.EqualValues(t, 42, v) + require.False(t, rows.Next()) + require.False(t, rows.NextResultSet()) + require.NotNil(t, rows.Err()) + if !runTestsAgainstMySQL { + require.Equal(t, "Error 1146: table not found: no_table", rows.Err().Error()) + } else { + require.Equal(t, "Error 1146 (42S02): Table 'testdb.no_table' doesn't exist", rows.Err().Error()) + } + require.NoError(t, rows.Close()) + + // Once an error occurs, additional statements are NOT executed. This code tests that the last SET statement + // above was NOT executed. + rows, err = conn.QueryContext(ctx, "SELECT @allStatementsExecuted;") + require.NoError(t, err) + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&v)) + require.Nil(t, v) + require.NoError(t, rows.Close()) +} + // TestMultiStatementsWithNoSpaces tests that multistatements are parsed correctly, even when // there is no space between the statement delimiter and the next statement. func TestMultiStatementsWithNoSpaces(t *testing.T) { diff --git a/statement.go b/statement.go index ff898cd..ee5d947 100644 --- a/statement.go +++ b/statement.go @@ -33,21 +33,15 @@ func (d doltMultiStmt) NumInput() int { return -1 } -func (d doltMultiStmt) Exec(args []driver.Value) (result driver.Result, retErr error) { +func (d doltMultiStmt) Exec(args []driver.Value) (result driver.Result, err error) { for _, stmt := range d.stmts { - var err error result, err = stmt.Exec(args) - if err != nil && retErr == nil { - // If any error occurs, record the first error, but continue executing all statements - retErr = err + if err != nil { + // If any error occurs, return the error and don't execute any more statements + return nil, err } } - // Return the first error encountered, if there was one - if retErr != nil { - return nil, retErr - } - // Otherwise, return the last result, to match the MySQL driver's behavior return result, nil } @@ -57,15 +51,17 @@ func (d doltMultiStmt) Query(args []driver.Value) (driver.Rows, error) { for _, stmt := range d.stmts { rows, err := stmt.Query(args) if err != nil { - // To match the MySQL driver's behavior, we attempt to execute all statements in a multi-statement - // query, even if some statements fail. If the first statement errors out, then we return that error, - // otherwise we save the error from any statements, so that they can be returned from NextResultSet() - // with the caller requests that result set. - rows = &doltRows{err: err} + // If an error occurs, we don't execute any more statements in the multistatement query. Instead, we + // capture the error in a doltRows instance, so that rows.NextResultSet() will return the error when + // the caller requests that result set. This is to match the MySQL driver's behavior. + multiResultSet.rowSets = append(multiResultSet.rowSets, &doltRows{err: err}) + break + } else { + multiResultSet.rowSets = append(multiResultSet.rowSets, rows.(*doltRows)) } - multiResultSet.rowSets = append(multiResultSet.rowSets, rows.(*doltRows)) } + // If an error occurred on the first statement, go ahead and return the error, without any result set. if multiResultSet.rowSets[0].err != nil { return nil, multiResultSet.rowSets[0].err } else { From e1ee3f6c57af79d8d87b60b7406fd795900fdc9d Mon Sep 17 00:00:00 2001 From: Jason Fulghum Date: Tue, 9 Jul 2024 16:47:22 -0700 Subject: [PATCH 18/23] Updating README with new section on multistatement support --- README.md | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e3d2c6..6a69f28 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ cd dbs dolt clone ``` -Finally you can create the dbs directory as shown above and then create the database in code using a SQL `CREATE TABLE` statement +Finally, you can create the dbs directory as shown above and then create the database in code using a SQL `CREATE TABLE` statement ### Connecting to the Database @@ -61,3 +61,31 @@ clientfoundrows - If set to true, returns the number of matching rows instead of #### Example DSN `file:///path/to/dbs?commitname=Your%20Name&commitemail=your@email.com&database=databasename` + +### Multi-Statement Support + +If you pass the `multistatements=true` parameter in the DSN, you can execute multiple statements in one query. The returned +rows allow you to iterate over the returned result sets by using the `NextResultSet` method, just like you can with the +MySQL driver. + +```go +rows, err := db.Query("SELECT * from someTable; SELECT * from anotherTable;") +// If an error is returned, it means it came from the first statement +if err != nil { + panic(err) +} + +for rows.Next() { + // process the first result set +} + +if rows.NextResultSet() { + for rows.Next() { + // process the second result set + } +} else { + // If NextResultSet returns false when there were more statements, it means there was an error, + // which you can access through rows.Err() + panic(rows.Err()) +} +``` From 75515bab4105ac4605beebd8e5077f54d195c7fd Mon Sep 17 00:00:00 2001 From: Jason Fulghum Date: Tue, 9 Jul 2024 17:15:41 -0700 Subject: [PATCH 19/23] Tidying up --- conn.go | 15 +++------------ rows.go | 4 +--- smoke_test.go | 1 - 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/conn.go b/conn.go index 4ca365e..7ae4b03 100644 --- a/conn.go +++ b/conn.go @@ -21,14 +21,8 @@ type DoltConn struct { DataSource *DoltDataSource } -// Prepare calls the SQL engine to prepare |query| on this connection and returns a *doltStmt. If any -// errors were encountered preparing |query|, the error is returned instead. If multistatement mode +// Prepare packages up |query| as a *doltStmt so it can be executed. If multistatements mode // has been enabled, then a *doltMultiStmt will be returned, capable of executing multiple statements. -// -// Note that the prepared query created by this method is never actually executed – the query is later -// executed as part of doltStmt, without using the prepared statement. The point of preparing it here is -// to detect any analysis errors. This matches the behavior of the MySQL driver implementation, but it -// may make sense to revisit this in the future. func (d *DoltConn) Prepare(query string) (driver.Stmt, error) { // Reuse the same ctx instance, but update the QueryTime to the current time. // Statements are executed serially on a connection, so it's safe to reuse @@ -42,8 +36,7 @@ func (d *DoltConn) Prepare(query string) (driver.Stmt, error) { } } -// prepareSingleStatement creates a prepared statement from |query|, returning any analysis errors, -// and if successful returns a doltStmt containing the query. +// prepareSingleStatement creates a doltStmt from |query|. func (d *DoltConn) prepareSingleStatement(query string) (*doltStmt, error) { return &doltStmt{ query: query, @@ -52,9 +45,7 @@ func (d *DoltConn) prepareSingleStatement(query string) (*doltStmt, error) { }, nil } -// prepareMultiStatement creates a prepared statement from each individual statement in |query|, -// returning any analysis errors. Otherwise, if successful, returns a doltMultiStmt containing -// a doltStmt for each individual statement in |query|. +// prepareMultiStatement creates a doltStmt from each individual statement in |query|. func (d *DoltConn) prepareMultiStatement(query string) (*doltMultiStmt, error) { var doltMultiStmt doltMultiStmt scanner := gms.NewMysqlParser() diff --git a/rows.go b/rows.go index 6b594d1..bb68a2b 100644 --- a/rows.go +++ b/rows.go @@ -64,7 +64,7 @@ type doltRows struct { columns []string - // err holds the error encountered while trying to retrieve this result set + // err holds any error encountered while trying to retrieve this result set err error } @@ -163,8 +163,6 @@ func (p *peekableRowIter) Peek(ctx *gms.Context) (gms.Row, error) { // Next implements gms.RowIter func (p *peekableRowIter) Next(ctx *gms.Context) (gms.Row, error) { - // TODO: If peek returned an error, we need to return that error here - // Although... calling Next() again *should* still return the error? if len(p.peeks) > 0 { peek := p.peeks[0] p.peeks = p.peeks[1:] diff --git a/smoke_test.go b/smoke_test.go index 7bbf746..37ea586 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -39,7 +39,6 @@ func TestPreparedStatements(t *testing.T) { } require.NoError(t, rows.Err()) require.NoError(t, rows.Close()) - } func TestMultiStatements(t *testing.T) { From 21e65d8b734c4f092f903248b458eb6f8755ac26 Mon Sep 17 00:00:00 2001 From: Jason Fulghum Date: Wed, 10 Jul 2024 13:26:22 -0700 Subject: [PATCH 20/23] Making the Dolt driver match the MySQL driver's behavior around skipping non-query result sets. --- rows.go | 40 +++++++++++++++++++++------- smoke_test.go | 73 +++++++++++++++++++++++++++++++++++---------------- statement.go | 50 ++++++++++++++++++++++++++++------- 3 files changed, 122 insertions(+), 41 deletions(-) diff --git a/rows.go b/rows.go index bb68a2b..2ca6a12 100644 --- a/rows.go +++ b/rows.go @@ -20,6 +20,10 @@ type doltMultiRows struct { var _ driver.RowsNextResultSet = (*doltMultiRows)(nil) func (d *doltMultiRows) Columns() []string { + if d.currentRowSet >= len(d.rowSets) { + return nil + } + return d.rowSets[d.currentRowSet].Columns() } @@ -38,23 +42,35 @@ func (d *doltMultiRows) Close() error { } func (d *doltMultiRows) Next(dest []driver.Value) error { + if d.currentRowSet >= len(d.rowSets) { + return io.EOF + } + return d.rowSets[d.currentRowSet].Next(dest) } func (d *doltMultiRows) HasNextResultSet() bool { - return d.currentRowSet < len(d.rowSets)-1 + idx := d.currentRowSet + 1 + for ; idx < len(d.rowSets); idx++ { + if d.rowSets[idx].isQueryResultSet || d.rowSets[idx].err != nil { + return true + } + } + return false } func (d *doltMultiRows) NextResultSet() error { - if d.currentRowSet+1 >= len(d.rowSets) { - return io.EOF + idx := d.currentRowSet + 1 + for ; idx < len(d.rowSets); idx++ { + if d.rowSets[idx].isQueryResultSet || d.rowSets[idx].err != nil { + // Update the current row set index when we find the next result set for a query. If we encountered an + // error running the statement earlier and saved an error in the row set, return that error now that the + // result set with the error has been requested. This matches the MySQL driver's behavior. + d.currentRowSet = idx + return d.rowSets[d.currentRowSet].err + } } - - // Move to the next row set. If we encountered an error running the statement earlier and saved - // an error in the row set, return that error now that the result set with the error has been requested. - // This is to match the MySQL driver's behavior. - d.currentRowSet += 1 - return d.rowSets[d.currentRowSet].err + return io.EOF } type doltRows struct { @@ -66,6 +82,12 @@ type doltRows struct { // err holds any error encountered while trying to retrieve this result set err error + + // isQueryResultSet indicates if this result set was generated by a statement that doesn't produce a result set. For + // example, an INSERT or DML statement doesn't return a result set, but we still keep track of a doltRows + // instance for their results in case an error was returned. This field is also used to skip over doltRows + // that are not result sets when calling NextResultSet() on a doltMultiRows instance. + isQueryResultSet bool } var _ driver.Rows = (*doltRows)(nil) diff --git a/smoke_test.go b/smoke_test.go index 37ea586..76d938d 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -63,14 +63,8 @@ func TestMultiStatements(t *testing.T) { var id int var name string - // Move to the third result set; don't bother checking the results from the two insert statements. - // NOTE: The MySQL driver does not require calling NextResultSet to move past insert statements – it detects that - // the result sets are empty, and skips any empty result sets when working with multi-statements. - if !runTestsAgainstMySQL { - require.True(t, rows.NextResultSet()) - require.True(t, rows.NextResultSet()) - } - + // NOTE: Because the first two statements are not queries and don't have real result sets, the current result set + // is automatically positioned at the third statement. require.True(t, rows.Next()) require.NoError(t, rows.Scan(&id, &name)) require.Equal(t, 1, id) @@ -224,6 +218,51 @@ func TestMultiStatementsQueryContext(t *testing.T) { require.NoError(t, rows.Scan(&v)) require.Nil(t, v) require.NoError(t, rows.Close()) + + // Non-query statements don't return a real result set, so they are skipped over automatically + rows, err = conn.QueryContext(ctx, "SET @notUsed=1; SELECT 42 FROM dual; ") + require.NoError(t, err) + require.NoError(t, rows.Err()) + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&v)) + require.EqualValues(t, 42, v) + require.NoError(t, rows.Close()) + + // Queries that generate an empty result set are NOT skipped over automatically + rows, err = conn.QueryContext(ctx, "CREATE TABLE t (pk int primary key); SELECT * FROM t; SELECT 42 FROM dual;") + require.NoError(t, err) + require.NoError(t, rows.Err()) + require.False(t, rows.Next()) + require.True(t, rows.NextResultSet()) + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(&v)) + require.EqualValues(t, 42, v) + require.NoError(t, rows.Close()) + + // If an error occurs between two valid queries, NextResulSet() returns false and exposes the + // error from rows.Err(). + rows, err = conn.QueryContext(ctx, "SELECT * FROM t; SELECT * from t2; SELECT 42 FROM dual;") + require.NoError(t, err) + require.NoError(t, rows.Err()) + require.False(t, rows.Next()) + require.False(t, rows.NextResultSet()) + require.NotNil(t, rows.Err()) + if !runTestsAgainstMySQL { + require.Equal(t, "Error 1146: table not found: t2", rows.Err().Error()) + } else { + require.Equal(t, "Error 1146 (42S02): Table 'testdb.t2' doesn't exist", rows.Err().Error()) + } + require.NoError(t, rows.Close()) + + // If an error occurs before the first real query results set, the error is returned, with no rows + rows, err = conn.QueryContext(ctx, "set @foo='bar'; SELECT * from t2; SELECT 42 FROM dual;") + require.NotNil(t, err) + require.Nil(t, rows) + if !runTestsAgainstMySQL { + require.Equal(t, "Error 1146: table not found: t2", err.Error()) + } else { + require.Equal(t, "Error 1146 (42S02): Table 'testdb.t2' doesn't exist", err.Error()) + } } // TestMultiStatementsWithNoSpaces tests that multistatements are parsed correctly, even when @@ -307,12 +346,8 @@ func TestMultiStatementsStoredProc(t *testing.T) { rows, err := conn.QueryContext(ctx, "create procedure p() begin select 1; end; call p(); call p(); call p();") require.NoError(t, err) - // Advance to the second result set and check its rows - // NOTE: The MySQL driver automatically positions the first result set at the second statement, - // since the first statement has an empty result set. - if !runTestsAgainstMySQL { - require.True(t, rows.NextResultSet()) - } + // NOTE: Because the first statement is not a query and doesn't have a real result set, the current result set + // is automatically positioned at the second statement. for rows.Next() { var i int err = rows.Scan(&i) @@ -357,14 +392,8 @@ func TestMultiStatementsTrigger(t *testing.T) { rows, err := conn.QueryContext(ctx, "create trigger trig before insert on t for each row begin set new.j = new.j * 100; end; insert into t values (1, 2); select * from t;") require.NoError(t, err) - // Advance to the third result set to test its results - // NOTE: The MySQL driver automatically positions the first result set at the third statement, because - // it skips empty result sets. - if !runTestsAgainstMySQL { - require.True(t, rows.NextResultSet()) - require.True(t, rows.NextResultSet()) - } - + // NOTE: Because the first statement is not a query and doesn't have a real result set, the current result set + // is automatically positioned at the second statement. for rows.Next() { var i, j int err = rows.Scan(&i, &j) diff --git a/statement.go b/statement.go index ee5d947..60ee1ab 100644 --- a/statement.go +++ b/statement.go @@ -6,6 +6,7 @@ import ( "github.com/dolthub/dolt/go/cmd/dolt/commands/engine" gms "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" "github.com/dolthub/vitess/go/sqltypes" querypb "github.com/dolthub/vitess/go/vt/proto/query" ) @@ -61,9 +62,19 @@ func (d doltMultiStmt) Query(args []driver.Value) (driver.Rows, error) { } } - // If an error occurred on the first statement, go ahead and return the error, without any result set. - if multiResultSet.rowSets[0].err != nil { - return nil, multiResultSet.rowSets[0].err + // Position the current result set index at the first statement that is a query, with a real result set. In + // other words, skip over any statements that don't actually return results sets (e.g. INSERT or DDL statements). + for ; multiResultSet.currentRowSet < len(multiResultSet.rowSets); multiResultSet.currentRowSet++ { + if multiResultSet.rowSets[multiResultSet.currentRowSet].isQueryResultSet || + multiResultSet.rowSets[multiResultSet.currentRowSet].err != nil { + break + } + } + + // If an error occurred before any query result set, go ahead and return the error, without any result set. + if multiResultSet.currentRowSet < len(multiResultSet.rowSets) && + multiResultSet.rowSets[multiResultSet.currentRowSet].err != nil { + return nil, multiResultSet.rowSets[multiResultSet.currentRowSet].err } else { return &multiResultSet, nil } @@ -149,15 +160,34 @@ func (stmt *doltStmt) Query(args []driver.Value) (driver.Rows, error) { // This is necessary for insert operations, since the insert happens inside the result iterator logic. Without // calling this now, insert statements and some DML statements (e.g. CREATE PROCEDURE) would not be executed yet, // and future statements in a multi-statement query that depend on those results would fail. - // Note that we don't worry about the result or the error here – we just want to exercise the iterator code to - // ensure the statement is executed. If an error does occur, we want that error to be returned in the Next() - // codepath, not here. + // If an error does occur, we want that error to be returned in the Next() codepath, not here. peekIter := peekableRowIter{iter: rowIter} - _, _ = peekIter.Peek(stmt.gmsCtx) + row, _ := peekIter.Peek(stmt.gmsCtx) return &doltRows{ - sch: sch, - rowIter: &peekIter, - gmsCtx: stmt.gmsCtx, + sch: sch, + rowIter: &peekIter, + gmsCtx: stmt.gmsCtx, + isQueryResultSet: isQueryResultSet(row), }, nil } + +// isQueryResultSet returns true if the specified |row| is a valid result set for a query. If row only contains +// one column and is an OkResult, or if row has zero columns, then the statement that generated this row was not +// a query. +func isQueryResultSet(row gms.Row) bool { + // If row is nil, return true since this could still be a valid, empty result set. + if row == nil { + return true + } + + if len(row) == 1 { + if _, ok := row[0].(types.OkResult); ok { + return false + } + } else if len(row) == 0 { + return false + } + + return true +} From 3269897f675602a9c6ea7c6c368c46e8ef8206ab Mon Sep 17 00:00:00 2001 From: Jason Fulghum Date: Thu, 11 Jul 2024 09:31:30 -0700 Subject: [PATCH 21/23] PR Feedback: Adding additional test assertions --- smoke_test.go | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/smoke_test.go b/smoke_test.go index 76d938d..26c5057 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -136,6 +136,10 @@ func TestMultiStatementsExecContext(t *testing.T) { require.NoError(t, err) require.EqualValues(t, 2, rowsAffected) + // Assert that all statements were correctly executed + requireResults(t, conn, "SELECT * FROM example_table ORDER BY id;", + [][]any{{996, "loo"}, {997, "goo"}, {998, "foo"}, {999, "boo"}}) + // ExecContext returns an error if ANY of the statements can't be executed. This also differs from the behavior of QueryContext. _, err = conn.ExecContext(ctx, "INSERT into example_table VALUES (100, 'woo'); "+ "INSERT into example_table VALUES (1, 2, 'too many'); SET @allStatementsExecuted=1;") @@ -146,15 +150,13 @@ func TestMultiStatementsExecContext(t *testing.T) { require.Equal(t, "Error 1136 (21S01): Column count doesn't match value count at row 1", err.Error()) } + // Assert that the first insert statement was executed before the error occurred + requireResults(t, conn, "SELECT * FROM example_table ORDER BY id;", + [][]any{{100, "woo"}, {996, "loo"}, {997, "goo"}, {998, "foo"}, {999, "boo"}}) + // Once an error occurs, additional statements are NOT executed. This code tests that the last SET statement // above was NOT executed. - rows, err := conn.QueryContext(ctx, "SELECT @allStatementsExecuted;") - var v any - require.NoError(t, err) - require.True(t, rows.Next()) - require.NoError(t, rows.Scan(&v)) - require.Nil(t, v) - require.NoError(t, rows.Close()) + requireResults(t, conn, "SELECT @allStatementsExecuted;", [][]any{{nil}}) } // TestMultiStatementsQueryContext tests that using QueryContext to run a multi-statement query works as expected and @@ -586,6 +588,30 @@ func initializeTestDatabaseConnection(t *testing.T, clientFoundRows bool) (conn return conn, cleanUpFunc } +// requireResults uses |conn| to run the specified |query| and asserts that the results +// match |expected|. If any differences are encountered, the current test fails. +func requireResults(t *testing.T, conn *sql.Conn, query string, expected [][]any) { + ctx := context.Background() + vals := make([]any, len(expected[0])) + + rows, err := conn.QueryContext(ctx, query) + require.NoError(t, err) + + for _, expectedRow := range expected { + for i := range vals { + vals[i] = &vals[i] + } + require.True(t, rows.Next()) + require.NoError(t, rows.Scan(vals...)) + for i, expectedVal := range expectedRow { + require.EqualValues(t, expectedVal, vals[i]) + } + } + + require.False(t, rows.Next()) + require.NoError(t, rows.Close()) +} + func encodeDir(dir string) string { // encodeDir translate a given path to a URL compatible path, mostly for windows compatibility if os.PathSeparator == '\\' { From 5b91028855daab1057c1f19251bb3dc379010210 Mon Sep 17 00:00:00 2001 From: Jason Fulghum Date: Thu, 11 Jul 2024 09:54:11 -0700 Subject: [PATCH 22/23] PR Feedback: Cleaning up configuration for running tests against MySQL --- smoke_test.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/smoke_test.go b/smoke_test.go index 26c5057..75c16a5 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -15,10 +15,16 @@ import ( ) // runTestsAgainstMySQL can be set to true to run tests against a MySQL database using the MySQL driver. -// This is useful to test behavior compatibility between the Dolt driver and the MySQL driver. When -// turning this option on, you will need to modify code in the initializeTestDatabaseConnection function -// and specify a valid DSN to an existing MySQL database. -var runTestsAgainstMySQL = false +// This is useful to test behavior compatibility between the Dolt driver and the MySQL driver. We +// want the Dolt driver to have the same semantics/behavior as the MySQL driver, so that customers +// familiar with using the MySQL driver, or code already using the MySQL driver, can easily switch +// to the Dolt driver. When this option is enabled, the MySQL database connection can be configured +// using mysqlDsn below. +var runTestsAgainstMySQL = true + +// mysqlDsn specifies the connection string for a MySQL database. Used only when the +// runTestsAgainstMySQL variable above is enabled. +var mysqlDsn = "root@tcp(localhost:3306)/?charset=utf8mb4&parseTime=True&loc=Local&multiStatements=true" // TestPreparedStatements tests that values can be plugged into "?" placeholders in queries. func TestPreparedStatements(t *testing.T) { @@ -551,14 +557,8 @@ func initializeTestDatabaseConnection(t *testing.T, clientFoundRows bool) (conn require.NoError(t, err) require.NoError(t, db.PingContext(ctx)) - // Setting runTestsAgainstMySQL to true allows you to point these tests at a MySQL database and use the - // MySQL driver. This is useful to compare the behavior of the Dolt driver to the MySQL driver. Ideally, - // we want the Dolt driver to have the same semantics/behavior as the MySQL driver, so that customers - // familiar with using the MySQL driver, or code already using the MySQL driver, can easily switch over - // to the Dolt driver. - // Note that you have to manually configure this and plug in a valid MySQL DSN to run the tests this way. if runTestsAgainstMySQL { - dsn := "root@tcp(localhost:3306)/?charset=utf8mb4&parseTime=True&loc=Local&multiStatements=true" + dsn := mysqlDsn if clientFoundRows { dsn += "&clientFoundRows=true" } From 891a991a867263abc06f359df9f25b69218b5946 Mon Sep 17 00:00:00 2001 From: Jason Fulghum Date: Thu, 11 Jul 2024 09:59:06 -0700 Subject: [PATCH 23/23] Turn off MySQL tests for CI --- smoke_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smoke_test.go b/smoke_test.go index 75c16a5..0819fc8 100644 --- a/smoke_test.go +++ b/smoke_test.go @@ -20,7 +20,7 @@ import ( // familiar with using the MySQL driver, or code already using the MySQL driver, can easily switch // to the Dolt driver. When this option is enabled, the MySQL database connection can be configured // using mysqlDsn below. -var runTestsAgainstMySQL = true +var runTestsAgainstMySQL = false // mysqlDsn specifies the connection string for a MySQL database. Used only when the // runTestsAgainstMySQL variable above is enabled.