diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8728960e75..e2dcde4f8e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -16,7 +16,7 @@ engine and perform queries. Engine tests live in the `enginetest` package, and are written in a harnessed manner to allow integrators to run them on their own -database implementation. The `memory_engine_test.go` runs these tests +database implementation. The `memory\_engine\_test.go` runs these tests on the built-in in-memory database implementation in the `memory` package. @@ -33,7 +33,7 @@ several main roles: `Expression`, ... - Provides implementations of components used in the rest of the packages `Row`, `Context`, `ProcessList`, `Catalog`, ... -- Defines the `information_schema` database, which is a special +- Defines the `information\_schema` database, which is a special database and contains some information about the schemas of other tables. @@ -125,14 +125,14 @@ Contains a function to `Find` the most similar name from an array to a given one using the Levenshtein distance algorithm. Used for suggestions on errors. -## `_integration` +## `\_integration` To ensure compatibility with some clients, there is a small example connecting and querying a go-mysql-server server from those clients. Each folder corresponds to a different client. For more info about supported clients see -[SUPPORTED_CLIENTS.md](/SUPPORTED_CLIENTS.md). +[SUPPORTED\_CLIENTS.md](/SUPPORTED\_CLIENTS.md). These integrations tests can be run using this command: @@ -142,7 +142,7 @@ make TEST=${CLIENT FOLDER NAME} integration It will take care of setting up the test server and shutting it down. -## `_example` +## `\_example` A small example of how to use go-mysql-server to create a server and run it. diff --git a/README.md b/README.md index e1230617db..75a5dee70b 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ for a reference implementation. Or, hop into the Dolt Discord server With the exception of specific limitations (see below), **go-mysql-server** is a drop-in replacement for MySQL. Any client library, tool, query, SQL syntax, SQL function, etc. that works with -MySQL (including the [MariaDB Java client](SUPPORTED_CLIENTS.md#mariadb-java-client)) should also work with **go-mysql-server**. If you find a gap in +MySQL (including the [MariaDB Java client](SUPPORTED\_CLIENTS.md#mariadb-java-client)) should also work with **go-mysql-server**. If you find a gap in functionality, please file an issue. For full MySQL compatibility documentation, see the [Dolt @@ -66,15 +66,15 @@ equivalent for your environment, installed and available to your C++ toolchain. For convenience, `go-mysql-server` also includes a non-compatible regex implementation based on the Go standard library `regex.Regex`. To build against that, instead of the `go-icu-regex` implementation, you must compile with -`-tags=gms_pure_go`. Please note that some of go-mysql-server's tests do not -pass with `-tags=gms_pure_go` and in general `gms_pure_go` is not recommended +`-tags=gms\_pure\_go`. Please note that some of go-mysql-server's tests do not +pass with `-tags=gms\_pure\_go` and in general `gms\_pure\_go` is not recommended for users seeking MySQL compatibility. ## Using the in-memory test server The in-memory test server can replace a real MySQL server in -tests. Start the server using the code in the [_example -directory](_example/main.go), also reproduced below. +tests. Start the server using the code in the [\_example +directory](\_example/main.go), also reproduced below. ```go package main @@ -98,7 +98,7 @@ import ( // // > mysql --host=localhost --port=3306 --user=root mydb --execute="SELECT * FROM mytable;" // +----------+-------------------+-------------------------------+----------------------------+ -// | name | email | phone_numbers | created_at | +// | name | email | phone\_numbers | created\_at | // +----------+-------------------+-------------------------------+----------------------------+ // | Jane Deo | janedeo@gmail.com | ["556-565-566","777-777-777"] | 2022-11-01 12:00:00.000001 | // | Jane Doe | jane@doe.com | [] | 2022-11-01 12:00:00.000001 | @@ -123,7 +123,7 @@ func main() { ctx := sql.NewContext(context.Background(), sql.WithSession(session)) ctx.SetCurrentDatabase(dbName) - // This variable may be found in the "users_example.go" file. Please refer to that file for a walkthrough on how to + // This variable may be found in the "users\_example.go" file. Please refer to that file for a walkthrough on how to // set up the "mysql" database to allow user creation and user checking when establishing connections. This is set // to false for this example, but feel free to play around with it and see how it works. if enableUsers { @@ -155,16 +155,16 @@ func createTestDatabase() *memory.DbProvider { table := memory.NewTable(ctx, db, tableName, sql.NewPrimaryKeySchema(sql.Schema{ {Name: "name", Type: types.Text, Nullable: false, Source: tableName, PrimaryKey: true}, {Name: "email", Type: types.Text, Nullable: false, Source: tableName, PrimaryKey: true}, - {Name: "phone_numbers", Type: types.JSON, Nullable: false, Source: tableName}, - {Name: "created_at", Type: types.MustCreateDatetimeType(query.Type_DATETIME, 6), Nullable: false, Source: tableName}, + {Name: "phone\_numbers", Type: types.JSON, Nullable: false, Source: tableName}, + {Name: "created\_at", Type: types.MustCreateDatetimeType(query.Type\_DATETIME, 6), Nullable: false, Source: tableName}, }), db.GetForeignKeyCollection()) db.AddTable(tableName, table) creationTime := time.Unix(0, 1667304000000001000).UTC() - _ = table.Insert(ctx, sql.NewRow("Jane Deo", "janedeo@gmail.com", types.MustJSON(`["556-565-566", "777-777-777"]`), creationTime)) - _ = table.Insert(ctx, sql.NewRow("Jane Doe", "jane@doe.com", types.MustJSON(`[]`), creationTime)) - _ = table.Insert(ctx, sql.NewRow("John Doe", "john@doe.com", types.MustJSON(`["555-555-555"]`), creationTime)) - _ = table.Insert(ctx, sql.NewRow("John Doe", "johnalt@doe.com", types.MustJSON(`[]`), creationTime)) + \_ = table.Insert(ctx, sql.NewRow("Jane Deo", "janedeo@gmail.com", types.MustJSON(`["556-565-566", "777-777-777"]`), creationTime)) + \_ = table.Insert(ctx, sql.NewRow("Jane Doe", "jane@doe.com", types.MustJSON(`[]`), creationTime)) + \_ = table.Insert(ctx, sql.NewRow("John Doe", "john@doe.com", types.MustJSON(`["555-555-555"]`), creationTime)) + \_ = table.Insert(ctx, sql.NewRow("John Doe", "johnalt@doe.com", types.MustJSON(`[]`), creationTime)) return pro } @@ -181,7 +181,7 @@ the golang MySQL connector and the `mysql` shell. ```bash > mysql --host=localhost --port=3306 --user=root mydb --execute="SELECT * FROM mytable;" +----------+-------------------+-------------------------------+----------------------------+ -| name | email | phone_numbers | created_at | +| name | email | phone\_numbers | created\_at | +----------+-------------------+-------------------------------+----------------------------+ | Jane Deo | janedeo@gmail.com | ["556-565-566","777-777-777"] | 2022-11-01 12:00:00.000001 | | Jane Doe | jane@doe.com | [] | 2022-11-01 12:00:00.000001 | diff --git a/SUPPORTED_CLIENTS.md b/SUPPORTED_CLIENTS.md index 0ec9bfb619..334ea19644 100644 --- a/SUPPORTED_CLIENTS.md +++ b/SUPPORTED_CLIENTS.md @@ -76,11 +76,11 @@ finally: import pandas as pd import sqlalchemy -engine = sqlalchemy.create_engine('mysql+pymysql://root:@127.0.0.1:3306/mydb') +engine = sqlalchemy.create\_engine('mysql+pymysql://root:@127.0.0.1:3306/mydb') with engine.connect() as conn: - repo_df = pd.read_sql_table("mytable", con=conn) - for table_name in repo_df.to_dict(): - print(table_name) + repo\_df = pd.read\_sql\_table("mytable", con=conn) + for table\_name in repo\_df.to\_dict(): + print(table\_name) ``` ### ruby-mysql @@ -101,10 +101,10 @@ conn.close() ```php try { $conn = new PDO("mysql:host=127.0.0.1:3306;dbname=mydb", "root", ""); - $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $conn->setAttribute(PDO::ATTR\_ERRMODE, PDO::ERRMODE\_EXCEPTION); $stmt = $conn->query('SELECT * FROM mytable LIMIT 1'); - $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + $result = $stmt->fetchAll(PDO::FETCH\_ASSOC); // use result } catch (PDOException $e) { @@ -127,7 +127,7 @@ const connection = mysql.createConnection({ connection.connect(); const query = 'SELECT * FROM mytable LIMIT 1'; -connection.query(query, function (error, results, _) { +connection.query(query, function (error, results, \_) { if (error) throw error; // use results @@ -202,7 +202,7 @@ package main import ( "database/sql" - _ "github.com/go-sql-driver/mysql" + \_ "github.com/go-sql-driver/mysql" ) func main() { @@ -223,53 +223,53 @@ func main() { ### mysql-connector-c ```c -#include +#include #include -void finish_with_error(MYSQL *con) +void finish\_with\_error(MYSQL *con) { - fprintf(stderr, "%s\n", mysql_error(con)); - mysql_close(con); + fprintf(stderr, "%s\n", mysql\_error(con)); + mysql\_close(con); exit(1); } int main(int argc, char **argv) { MYSQL *con = NULL; - MYSQL_RES *result = NULL; - int num_fields = 0; - MYSQL_ROW row; + MYSQL\_RES *result = NULL; + int num\_fields = 0; + MYSQL\_ROW row; - printf("MySQL client version: %s\n", mysql_get_client_info()); + printf("MySQL client version: %s\n", mysql\_get\_client\_info()); - con = mysql_init(NULL); + con = mysql\_init(NULL); if (con == NULL) { - finish_with_error(con); + finish\_with\_error(con); } - if (mysql_real_connect(con, "127.0.0.1", "root", "", "mydb", 3306, NULL, 0) == NULL) { - finish_with_error(con); + if (mysql\_real\_connect(con, "127.0.0.1", "root", "", "mydb", 3306, NULL, 0) == NULL) { + finish\_with\_error(con); } - if (mysql_query(con, "SELECT name, email, phone_numbers FROM mytable")) { - finish_with_error(con); + if (mysql\_query(con, "SELECT name, email, phone\_numbers FROM mytable")) { + finish\_with\_error(con); } - result = mysql_store_result(con); + result = mysql\_store\_result(con); if (result == NULL) { - finish_with_error(con); + finish\_with\_error(con); } - num_fields = mysql_num_fields(result); - while ((row = mysql_fetch_row(result))) { - for(int i = 0; i < num_fields; i++) { + num\_fields = mysql\_num\_fields(result); + while ((row = mysql\_fetch\_row(result))) { + for(int i = 0; i < num\_fields; i++) { printf("%s ", row[i] ? row[i] : "NULL"); } printf("\n"); } - mysql_free_result(result); - mysql_close(con); + mysql\_free\_result(result); + mysql\_close(con); return 0; } diff --git a/enginetest/queries/information_schema_queries.go b/enginetest/queries/information_schema_queries.go index d92edbd860..48ffd1b3c8 100644 --- a/enginetest/queries/information_schema_queries.go +++ b/enginetest/queries/information_schema_queries.go @@ -395,6 +395,29 @@ var InfoSchemaQueries = []QueryTest{ {"mytable", 1, "idx_si", 2, "i", nil, 0, nil, nil, "", "BTREE", "", "", "YES", nil}, }, }, + { + // The WHERE clause was previously silently ignored, always returning + // every index on the table regardless of the predicate (found via a + // downstream Drupal core compatibility failure: Drupal's mysql + // driver implements Schema::indexExists() with exactly this query + // shape). MySQL: https://dev.mysql.com/doc/refman/8.0/en/show-index.html + Query: `SHOW INDEX FROM mytable WHERE key_name = 'mytable_s'`, + Expected: []sql.Row{ + {"mytable", 0, "mytable_s", 1, "s", nil, 0, nil, nil, "", "BTREE", "", "", "YES", nil}, + }, + }, + { + Query: `SHOW INDEX FROM mytable WHERE key_name = 'no_such_index'`, + Expected: []sql.Row{}, + }, + { + Query: `SHOW INDEX FROM mytable WHERE key_name LIKE 'mytable_%'`, + Expected: []sql.Row{ + {"mytable", 0, "mytable_s", 1, "s", nil, 0, nil, nil, "", "BTREE", "", "", "YES", nil}, + {"mytable", 1, "mytable_i_s", 1, "i", nil, 0, nil, nil, "", "BTREE", "", "", "YES", nil}, + {"mytable", 1, "mytable_i_s", 2, "s", nil, 0, nil, nil, "", "BTREE", "", "", "YES", nil}, + }, + }, { Query: `SHOW CREATE TABLE mytaBLE`, Expected: []sql.Row{ diff --git a/server/handler.go b/server/handler.go index 8abf4d7e8c..a240cfc6cd 100644 --- a/server/handler.go +++ b/server/handler.go @@ -965,7 +965,23 @@ func setConnStatusFlags(ctx *sql.Context, c *mysql.Conn) error { c.StatusFlags &= ^uint16(mysql.ServerStatusAutocommit) } - if t := ctx.GetTransaction(); t != nil { + // A non-nil transaction alone does not mean the client is inside an + // explicit transaction: the engine also opens an implicit, per-statement + // transaction for ordinary autocommit statements. Reporting + // ServerInTransaction for those implicit transactions leaks an internal + // implementation detail onto the wire and desyncs MySQL clients (e.g. + // PDO_MySQL, whose BEGIN/COMMIT bookkeeping mirrors this flag) from the + // server's actual, client-visible transaction state: a client can end up + // believing a transaction is still open immediately after an ordinary + // autocommit statement, and its next explicit BEGIN is then refused + // client-side with "There is already an active transaction" even though + // it never issued a matching COMMIT/ROLLBACK for anything. + // + // GetIgnoreAutoCommit() is true only between an explicit + // BEGIN/START TRANSACTION and its COMMIT/ROLLBACK (see + // sql/rowexec/transaction.go), so it is the correct signal for whether + // the current transaction is client-visible. + if t := ctx.GetTransaction(); t != nil && ctx.GetIgnoreAutoCommit() { c.StatusFlags |= uint16(mysql.ServerInTransaction) } else { c.StatusFlags &= ^uint16(mysql.ServerInTransaction) diff --git a/server/handler_test.go b/server/handler_test.go index 8c12e11660..64f5d9d086 100644 --- a/server/handler_test.go +++ b/server/handler_test.go @@ -2066,3 +2066,53 @@ func TestHandlerNewConnectionProcessListInteractions(t *testing.T) { assert.Equal(t, "test", procs[0].Database) } } + +// fakeTransaction is a minimal sql.Transaction fixture for tests that only +// need a non-nil transaction, not real commit/rollback semantics. +type fakeTransaction struct{} + +func (fakeTransaction) String() string { return "fakeTransaction" } +func (fakeTransaction) IsReadOnly() bool { return false } + +// TestSetConnStatusFlagsInTransaction verifies that the ServerInTransaction +// status flag reflects only client-visible (explicit) transactions, not the +// engine's internal, per-statement implicit transactions used for ordinary +// autocommit statements. A non-nil ctx.GetTransaction() alone is not +// sufficient: an implicit transaction with GetIgnoreAutoCommit() == false +// must not set the flag, or MySQL clients (whose own BEGIN/COMMIT +// bookkeeping mirrors this wire flag, e.g. PHP's PDO_MySQL) end up believing +// a transaction is open when the server considers none to be, and refuse a +// subsequent, legitimate client-issued BEGIN. +func TestSetConnStatusFlagsInTransaction(t *testing.T) { + newCtx := func() *sql.Context { + session := sql.NewBaseSession() + return sql.NewContext(context.Background(), sql.WithSession(session)) + } + + t.Run("no transaction", func(t *testing.T) { + ctx := newCtx() + conn := &mysql.Conn{} + require.NoError(t, setConnStatusFlags(ctx, conn)) + assert.Equal(t, uint16(0), conn.StatusFlags&uint16(mysql.ServerInTransaction)) + }) + + t.Run("implicit per-statement transaction (ordinary autocommit statement)", func(t *testing.T) { + ctx := newCtx() + ctx.SetTransaction(fakeTransaction{}) + // GetIgnoreAutoCommit() defaults to false: no explicit BEGIN was issued. + conn := &mysql.Conn{} + require.NoError(t, setConnStatusFlags(ctx, conn)) + assert.Equal(t, uint16(0), conn.StatusFlags&uint16(mysql.ServerInTransaction), + "an implicit, per-statement transaction must not set ServerInTransaction") + }) + + t.Run("explicit client transaction (after BEGIN)", func(t *testing.T) { + ctx := newCtx() + ctx.SetTransaction(fakeTransaction{}) + ctx.SetIgnoreAutoCommit(true) + conn := &mysql.Conn{} + require.NoError(t, setConnStatusFlags(ctx, conn)) + assert.NotEqual(t, uint16(0), conn.StatusFlags&uint16(mysql.ServerInTransaction), + "an explicit client transaction must set ServerInTransaction") + }) +} diff --git a/sql/errors.go b/sql/errors.go index ef2eab36d1..4aa22493c4 100644 --- a/sql/errors.go +++ b/sql/errors.go @@ -1040,21 +1040,50 @@ func CastSQLError(err error) *mysql.SQLError { case ErrInvalidOperandColumns.Is(err): code = mysql.EROperandColumns case ErrInsertIntoNonNullableProvidedNull.Is(err): + // NOT NULL constraint violation. MySQL reports SQLSTATE 23000 + // (integrity constraint violation) for this, not the driver default + // of HY000; MySQL clients that branch on SQLSTATE class to detect + // constraint violations (e.g. PHP's PDO, whose mysql driver maps + // 23000 to a dedicated exception type) cannot otherwise distinguish + // this from a generic server error. code = mysql.ERBadNullError + sqlState = mysql.SSConstraintViolation + case ErrInsertIntoNonNullableDefaultNullColumn.Is(err): + // A distinct NOT NULL violation: the column was omitted from the + // INSERT entirely (vs. explicitly provided as NULL) and has no + // default. Real MySQL 8.0 reports this as error 1364 + // (ER_NO_DEFAULT_FOR_FIELD) under SQLSTATE HY000 - unlike the other + // constraint-violation cases above, MySQL does NOT use 23000 here, + // so sqlState is intentionally left at its HY000 default. Before + // this fix, this case fell through to the generic `default:` branch + // below and was reported as error 1105 (ER_UNKNOWN_ERROR) instead + // of 1364. Some clients special-case the exact numeric code 1364 for + // this scenario in addition to SQLSTATE (e.g. Drupal's mysql driver, + // core/modules/mysql/src/Driver/Database/mysql/ExceptionHandler.php), + // since MySQL itself doesn't give this case a distinguishing + // SQLSTATE the way it does for duplicate-key/FK violations. + code = mysql.ERNoDefaultForField case ErrNonAggregatedColumnWithoutGroupBy.Is(err): code = mysql.ERMixOfGroupFuncAndFields case ErrPrimaryKeyViolation.Is(err): + // See the ErrInsertIntoNonNullableProvidedNull comment above: + // duplicate-key violations are also SQLSTATE 23000 in MySQL. code = mysql.ERDupEntry + sqlState = mysql.SSConstraintViolation case ErrUniqueKeyViolation.Is(err): code = mysql.ERDupEntry + sqlState = mysql.SSConstraintViolation case ErrPartitionNotFound.Is(err): code = 1526 // TODO: Needs to be added to vitess case ErrForeignKeyChildViolation.Is(err): code = mysql.ErNoReferencedRow2 // test with mysql returns 1452 vs 1216 + sqlState = mysql.SSConstraintViolation case ErrForeignKeyParentViolation.Is(err): code = mysql.ERRowIsReferenced2 // test with mysql returns 1451 vs 1215 + sqlState = mysql.SSConstraintViolation case ErrDuplicateEntry.Is(err): code = mysql.ERDupEntry + sqlState = mysql.SSConstraintViolation case ErrInvalidJSONText.Is(err): code = 3141 // TODO: Needs to be added to vitess case ErrMultiplePrimaryKeysDefined.Is(err): diff --git a/sql/errors_test.go b/sql/errors_test.go index 4bb0b9d357..65261bf5cc 100644 --- a/sql/errors_test.go +++ b/sql/errors_test.go @@ -35,6 +35,53 @@ func TestSQLErrorCast(t *testing.T) { } } +// TestSQLErrorCastConstraintViolationSQLState verifies that integrity- +// constraint-violation errors (duplicate key, NOT NULL violation, foreign +// key violation) are cast to MySQL's SQLSTATE 23000, matching real MySQL's +// behavior. Before this fix these fell through to the driver default of +// SQLSTATE HY000 ("General error"), which prevents MySQL clients that +// branch on the SQLSTATE class to detect constraint violations (e.g. PHP's +// PDO, whose mysql driver maps 23000 to a dedicated +// IntegrityConstraintViolationException) from distinguishing a constraint +// violation from any other server error. +func TestSQLErrorCastConstraintViolationSQLState(t *testing.T) { + tests := []struct { + name string + err error + }{ + {"primary key violation", ErrPrimaryKeyViolation.New("dup")}, + {"unique key violation", ErrUniqueKeyViolation.New("dup")}, + {"duplicate entry", ErrDuplicateEntry.New("dup")}, + {"not null violation", ErrInsertIntoNonNullableProvidedNull.New("col")}, + {"foreign key child violation", ErrForeignKeyChildViolation.New("fk", "child", "parent", "idx")}, + {"foreign key parent violation", ErrForeignKeyParentViolation.New("fk", "child", "parent", "idx")}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := CastSQLError(test.err) + require.NotNil(t, err) + assert.Equal(t, mysql.SSConstraintViolation, err.SQLState(), + "expected SQLSTATE 23000 (integrity constraint violation), matching MySQL") + }) + } +} + +// TestSQLErrorCastNoDefaultForField verifies that omitting a NOT NULL +// column with no default from an INSERT is cast to MySQL's real error code +// 1364 (ER_NO_DEFAULT_FOR_FIELD), not the generic 1105 (ER_UNKNOWN_ERROR) +// it fell through to before this fix. Unlike duplicate-key/FK violations, +// real MySQL 8.0 reports this under SQLSTATE HY000, not 23000 - some +// clients (e.g. Drupal's mysql driver) special-case the numeric code 1364 +// directly for this reason, so getting the code right matters even though +// the SQLSTATE here intentionally stays HY000. +func TestSQLErrorCastNoDefaultForField(t *testing.T) { + err := CastSQLError(ErrInsertIntoNonNullableDefaultNullColumn.New("age")) + require.NotNil(t, err) + assert.Equal(t, mysql.ERNoDefaultForField, err.Number()) + assert.Equal(t, "HY000", err.SQLState()) +} + func TestWrappedInsertError(t *testing.T) { tests := []struct { err error diff --git a/sql/planbuilder/show.go b/sql/planbuilder/show.go index dac84e8c88..e570b7fe1b 100644 --- a/sql/planbuilder/show.go +++ b/sql/planbuilder/show.go @@ -558,7 +558,34 @@ func (b *Builder) buildShowIndex(inScope *scope, s *ast.Show) (outScope *scope) err := sql.ErrTableNotFound.New(s.Table.Name.String()) b.handleErr(err) } - outScope.node = showIdx + + // SHOW INDEX supports a WHERE clause (MySQL: + // https://dev.mysql.com/doc/refman/8.0/en/show-index.html), but that + // clause was never applied here. Unlike most other SHOW statements + // (which carry their filter in the generic s.Filter field, a + // *ShowFilter), the grammar parses SHOW INDEX's WHERE clause into its + // own dedicated s.ShowIndexFilterOpt field (see sql.y's "SHOW + // indexes_or_keys ... where_expression_opt" rule) - a plain + // ast.Expr, not a *ShowFilter. That field was never read here, so it + // was silently discarded and every SHOW INDEX query always returned + // every index on the table regardless of the predicate. Apply it the + // same way the other column-filtering SHOW statements above do (see + // buildShowProcedureStatus): register the output columns of showIdx so + // the filter expression can resolve references like `key_name`, then + // wrap the node in a Having filter. + var node sql.Node = showIdx + if s.ShowIndexFilterOpt != nil { + for _, c := range showIdx.Schema(b.ctx) { + outScope.newColumn(scopeColumn{table: "", col: c.Name, typ: c.Type, nullable: c.Nullable}) + } + + filter := b.buildScalar(outScope, s.ShowIndexFilterOpt) + if filter != nil { + node = plan.NewHaving(filter, showIdx) + } + } + + outScope.node = node return } diff --git a/sql/rowexec/transaction_iters.go b/sql/rowexec/transaction_iters.go index 99b0041436..7b163f2366 100644 --- a/sql/rowexec/transaction_iters.go +++ b/sql/rowexec/transaction_iters.go @@ -145,6 +145,21 @@ func (t *TransactionCommittingIter) Close(ctx *sql.Context) error { // Clearing out the current transaction will tell us to start a new one the next time this session queries ctx.SetTransaction(nil) + // An implicit commit (e.g. a DDL statement issued mid-transaction) ends + // whatever explicit, client-initiated transaction was in progress, exactly + // like MySQL's own implicit-commit semantics. If that explicit transaction + // had set ctx.SetIgnoreAutoCommit(true) (see rowexec/transaction.go + // buildStartTransaction), that flag must be cleared here too - otherwise it + // stays incorrectly true, causing the *next* ordinary autocommit statement + // to be misreported as inside a client-visible transaction (see + // server/handler.go setConnStatusFlags), which desyncs MySQL clients whose + // BEGIN/COMMIT bookkeeping mirrors that wire status flag (e.g. PHP's + // PDO_MySQL) and makes their next legitimate BEGIN fail client-side with + // "There is already an active transaction". + if t.implicitCommit { + ctx.SetIgnoreAutoCommit(false) + } + return nil }