🐬

MySQL 8.0.22 のプリペアドステートメントの非互換

2020/11/01に公開

MySQL 8.0.22 のリリースノートによるとプリペアドステートメントまわりに非互換があるらしい。

https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-22.html

For a prepared statement of the form SELECT expr1, expr2, ... FROM table ORDER BY ?, passing an integer value N for the parameter no longer causes ordering of the results by the Nth expression in the select list; the results are no longer ordered, as is expected with ORDER BY constant.

order by の引数をプレースホルダで与えても無視されるらしい。

試してみる。

8.0.21:

% docker run -p 13306:3306 --name mysql8021 -d -e MYSQL_ALLOW_EMPTY_PASSWORD=1 mysql:8.0.21
% docker exec -it mysql8021 mysql
mysql> create database test;
mysql> use test;
mysql> create table t (id int auto_increment primary key, n int);
mysql> insert into t values (1,5),(2,4),(3,3),(4,2),(5,1);
mysql> select id, n from t order by 2;
+----+------+
| id | n    |
+----+------+
|  5 |    1 |
|  4 |    2 |
|  3 |    3 |
|  2 |    4 |
|  1 |    5 |
+----+------+
mysql> prepare st from "select id, n from t order by ?";
mysql> set @n=2;
mysql> execute st using @n;
+----+------+
| id | n    |
+----+------+
|  5 |    1 |
|  4 |    2 |
|  3 |    3 |
|  2 |    4 |
|  1 |    5 |
+----+------+

通常のクエリとプリペアドステートメントで同じ結果になる。

8.0.22:

% docker run -p 23306:3306 --name mysql8022 -d -e MYSQL_ALLOW_EMPTY_PASSWORD=1 mysql:8.0.22
% docker exec -it mysql8022 mysql
mysql> create database test;
mysql> use test;
mysql> create table t (id int auto_increment primary key, n int);
mysql> insert into t values (1,5),(2,4),(3,3),(4,2),(5,1);
mysql> select id, n from t order by 2;
+----+------+
| id | n    |
+----+------+
|  5 |    1 |
|  4 |    2 |
|  3 |    3 |
|  2 |    4 |
|  1 |    5 |
+----+------+
mysql> prepare st from "select id, n from t order by ?";
mysql> set @n=2;
mysql> execute st using @n;
+----+------+
| id | n    |
+----+------+
|  1 |    5 |
|  2 |    4 |
|  3 |    3 |
|  4 |    2 |
|  5 |    1 |
+----+------+

プリペアドステートメントの order by が無視されてる。

んー、使えないなら使えないで変な結果を返すんじゃなくてエラーにして欲しいなぁ。

ところで、MySQL のプリペアドステートメントは、上記の prepare 命令を使う方法と C API の mysql_stmt_ で始まる関数を使う方法の二種類がある。
前者は通常のクエリと同じプロトコル(COM_QUERY)が使用されるが、後者は専用のプロトコル(COM_STMT_PREPARE, COM_STMT_EXECUTE等)が使用され、パラメータや結果はテキストではなくバイナリデータで送られる。

ということでバイナリの方でどうなるか気になったので試してみた。

とりあえず簡単に Ruby で。

require 'mysql2'
m = Mysql2::Client.new(host: "127.0.0.1", username: "root", database: "test")
s = m.prepare("select id, n from t order by ?")
s.execute(2).each do |*a|
  p a
end

8.0.21:

% MYSQL_TCP_PORT=13306 ruby test.rb
[{"id"=>5, "n"=>1}]
[{"id"=>4, "n"=>2}]
[{"id"=>3, "n"=>3}]
[{"id"=>2, "n"=>4}]
[{"id"=>1, "n"=>5}]

8.0.22:

% MYSQL_TCP_PORT=23306 ruby test.rb
(…応答なし…)

あれ…?

一応 C でも試してみる(チョーめんどくさい)。

#include <stdio.h>
#include <string.h>
#include <mysql/mysql.h>

int main(int argc, char *argv[])
{
  MYSQL my;
  MYSQL_STMT *st;
  MYSQL_BIND bind[1], bind2[2];
  memset(bind, 0, sizeof(bind));
  memset(bind2, 0, sizeof(bind2));

  mysql_init(&my);
  if (mysql_real_connect(&my, "127.0.0.1", "root", "", "test", 0, NULL, 0) == NULL)
    goto error;
  if ((st = mysql_stmt_init(&my)) == NULL)
    goto error;

  char *q = "select id,n from t order by ?";
  mysql_stmt_prepare(st, q, strlen(q));

  long n = 2;
  bind[0].buffer_type = MYSQL_TYPE_LONG;
  bind[0].buffer = &n;
  bind[0].length = NULL;
  bind[0].buffer_length = sizeof(n);
  bind[0].is_null = 0;

  if (mysql_stmt_bind_param(st, bind) != 0)
    goto stmt_error;
  if (mysql_stmt_execute(st) != 0)
    goto stmt_error;

  long id = 0;
  bind2[0].buffer_type = MYSQL_TYPE_LONG;
  bind2[0].buffer = &id;
  bind2[0].buffer_length = sizeof(id);
  bind2[0].length = NULL;
  bind2[1].buffer_type = MYSQL_TYPE_LONG;
  bind2[1].buffer = &n;
  bind2[1].buffer_length = sizeof(n);
  bind2[1].length = NULL;

  if (mysql_stmt_bind_result(st, bind2) != 0)
    goto stmt_error;
  if (mysql_stmt_store_result(st) != 0)
    goto stmt_error;

  while (mysql_stmt_fetch(st) == 0) {
    printf("%ld %ld\n", id, n);
  }
  exit(0);

error:
  puts(mysql_error(&my));
  exit(1);

stmt_error:
  puts(mysql_stmt_error(st));
  exit(1);
}
% gcc test.c -lmysqlclient

8.0.21:

% MYSQL_TCP_PORT=13306 ./a.out
5 1
4 2
3 3
2 4
1 5

8.0.22:

% MYSQL_TCP_PORT=23306 ./a.out
(…応答なし…)

やっぱり応答がない。バグなのかな。

まあ order by でプレースホルダなんて使わないから別にいいんだけど、気が向いたらバグレポ書こう。

続き

Discussion