私が歌川です

@utgwkk が書いている

MySQLやPostgreSQLのWHERE INに空のリストを渡そうとすると文法エラーになる、SQLiteはエラーにならない

小ネタかつ備忘録です。

MySQL

この記事ではMySQL 8での実行結果を書いてるけど、5.6でも同様に文法エラーでした。

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 13615
Server version: 8.0.21-0ubuntu0.20.04.4 (Ubuntu)

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> SELECT 1 FROM DUAL WHERE 1 IN ();
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ')' at line 1
mysql> SELECT 1 FROM DUAL WHERE 1 IN (1);
+---+
| 1 |
+---+
| 1 |
+---+
1 row in set (0.00 sec)

mysql> SELECT 1 FROM DUAL WHERE 1 IN (2);
Empty set (0.00 sec)

mysql> SELECT 1 FROM DUAL WHERE 1 IN (1);
+---+
| 1 |
+---+
| 1 |
+---+
1 row in set (0.00 sec)

mysql>

MySQL :: MySQL 5.6 リファレンスマニュアル :: 9.5 式の構文を読むと、IN式の構文は以下のように定義されていて(抜粋)、

predicate:
    bit_expr [NOT] IN (subquery)
  | bit_expr [NOT] IN (expr [, expr] ...)

このうち後者は、括弧の中に1個以上の expr がないといけないことを表しているのですぐ分かる。 subquery はサブクエリのことで、これも空文字列にはならない。

PostgreSQL

psql (12.4 (Ubuntu 12.4-0ubuntu0.20.04.1))
Type "help" for help.

postgres=# SELECT 1 WHERE 1 IN ();
ERROR:  syntax error at or near ")"
LINE 1: SELECT 1 WHERE 1 IN ();
                             ^
postgres=# SELECT 1 WHERE 1 IN (1);
 ?column?
----------
        1
(1 row)

postgres=#

9.23. 行と配列の比較 の 9.23.1. IN を見ると、これもMySQLと同様に1個以上の value が求められているので、空だと文法エラーになる。subquery の場合もMySQLと同様。

SQLite

一方SQLiteではエラーになりません。

SQLite version 3.31.1 2020-01-27 19:55:54
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> SELECT 1 WHERE 1 IN ();
sqlite> SELECT 1 WHERE 1 IN (1);
1
sqlite>

これは SQL Language Expressions の The IN and NOT IN operators 節を読むとちゃんと書いてあった。

When the right operand is an empty set, the result of IN is false and the result of NOT IN is true, regardless of the left operand and even if the left operand is NULL.

いかがでしたか

SELECT * FROM tbl WHERE id IN (?) みたいなクエリにbindするオブジェクトとしてうっかり空リストを渡して爆死、というのがないように気をつけたいですね。各種O/Rマッパーの実装はどうなっているのか気になるところでもあります。標準SQL的にはどうなのかというのも気になります。

追記

標準SQL (SQL:2003) では空リストは取れない、ということはSQLiteが独自実装をしていることになりそう。

github.com


この記事の本題と関係ないけど、SQLの文法エラーを踏み抜くことはかなり多い気がしている。O/Rマッパーを使っていないときは(使っていても?)、本質的に文字列のやり取りになるのでそういう印象がある。そういうときはBNFや状態機械の気持ちになると多少はましになるかもしれない。あるいは文法をつかみづらいほどに複雑なことをSQLだけで完結させないほうがよいという考え方もある。

Query Language Understood by SQLite はよいドキュメントで、状態機械を眺めながらSQLiteの文法をつかむことができる。慣れてくると、状態機械的にはこのトークンが来ることはないな、みたいな感覚が出てくる。それでも間違えて文法エラーになることはあるのでやっていくしかなさそう。SQLiteのIN句に空リストを渡せるのはこの状態機械を読んでて気づきました。

追記

ActiveRecordは WHERE 1=0 に変換する、というのを id:onk さんに教えてもらいました。コードを追うとたしかにそうなっていそう。

追記2

SQL::Maker では WHERE 0=1 に変換される(そういうテストがある)。

SQL::NamedPlaceholder では WHERE id IN (NULL) に変換される。