SQLite3にリストを渡した際のOperationalError


GUIアプリを作る際，実行時エラーを起こすと原因不明のまま落ちるので，tracebackライブラリのtraceback.print_exc()を利用している．

しかしSQL周りの処理で，traceback.print_exc()では解明できないエラーに出会ってしまった．

<b>リスト(配列に似たもの)に入った数値を送ってエラーを吐くと，リストが原因であることを教えてくれない．</b>

例えば本来1を送るところ，
[1]
を送ってしまうとエラーとなるが，エラー文には「1のようなcolumnは無いです」と表示されるので1がリストに入っていることがわからない．

# エラーの再現プログラム
##概要
以下のようなデータの挿入・取得プログラムで再現してみる．
sample.dbにAccountsテーブルを作り，usernameとpasswordを挿入したり取得したりする．

* データ挿入後のAccountsテーブル

|id:int 主キー|username:string|password:string|
|:--|:--|:--|
|1|user1|pass1|
|2|user2|pass2|

```python:sample.py
# -*- coding: utf-8 -*-
import sqlite3
import traceback


class Database:
    def __init__(self):
        self.dbName = 'sample.db'
        self.accounts_table = 'Accounts'
        
        con = sqlite3.connect(self.dbName)
        # もしテーブルが無ければ作る
        # integerはprimary keyにすると自動でauto incrementになっている
        con.execute("create table if not exists %s("
                    "id integer primary key,"  # 連番(主キー)
                    "username string,"
                    "password string)"
                    % (self.accounts_table))
        con.commit()  # SQLを確定
        con.close()
    
    def setAccount(self, userName: str, passWord: str):
        con = sqlite3.connect(self.dbName)
        con.execute(
            "insert into %s(username,password) "
            "values('%s','%s')"
            % (self.accounts_table, userName, passWord))
        con.commit()
        con.close()
    
    # 引数のaccountIDを使って，Accountsテーブルからサービス情報を取得
    # 見つかれば辞書型で['username', 'password']を返す
    # 見つからなければ空リストを返す
        def getAccount(self, accountID: int) -> list:
        accountData = []
        try:
            con = sqlite3.connect(self.dbName)
            cur = con.cursor()
            cur.execute(
                "select * from %s where id = %s"
                % (self.accounts_table, accountID)
            )
            result = cur.fetchall()
            # 見つからなかったら空リストを返す
            if len(result) == 0:
                return accountData
            # 辞書型に変換
            accountData = dict(zip(
                ['userName', 'passWord'],
                [result[0][1], result[0][2]]
            ))
            con.close()
        except:
            traceback.print_exc()
        return accountData


db = Database()
# DBにデータ挿入
db.setAccount('user1', 'pass1')
db.setAccount('user2', 'pass2')
# 挿入したデータをidから取得
result = db.getAccount(1)
print('id 1: ', end='')
print(result)
print('id 2: ', end='')
result = db.getAccount(2)
print(result)
```

※再現プログラムなのでtraceback.print_exc()はgetAccount()でのみ利用している．

getAccount()の結果

```console
id 1: {'userName': 'user1', 'passWord': 'pass1'}
id 2: {'userName': 'user2', 'passWord': 'pass2'}
```

ちゃんと取れている．

##エラーとなる場合(リストに入れて送る)
ではgetAccount()に数値入りのリスト
[1]
を送ってみる．


上プログラムの後半を以下のように書き換えて実行(データ挿入はもう必要ないのでコメント化)．

```diff
  db = Database()
  # DBにデータ挿入
- db.setAccount('user1', 'pass1')
- db.setAccount('user2', 'pass2')
+ # db.setAccount('user1', 'pass1')
+ # db.setAccount('user2', 'pass2')
  # 挿入したデータをidから取得
- result = db.getAccount(1)
+ result = db.getAccount([1])
  print('id 1: ', end='')
  print(result)
- print('id 2: ', end='')
- result = db.getAccount(2)
- print(result)
```

getAccount()の結果

```console
Traceback (most recent call last):
  File "[パス]/sample.py", line 41, in getAccount
    % (self.accounts_table, accountID)
sqlite3.OperationalError: no such column: 1
id 1: []
```

<b>「1というcolumnは無いです」と怒られるだけ</b>．

<b>もちろんid:1は存在する．最後のトレースしか表示されないのでこれ以上どうしようもない．数値を送ったつもりがポカしてリストに入ったまま送ってしまうと，原因究明に時間を奪われる．</b>

# 実行時エラーには含まれている
後で気付いたが，traceback.print_exc()を利用せず生で実行時エラーを吐かせれば原因を教えてくれる．

先ほどの書き換えをそのままに，以下のようにプログラムを書き換える．
traceback.print_exc()を使わないver.

```diff
  # -*- coding: utf-8 -*-
  import sqlite3
  import traceback


  class Database:
      def __init__(self):
          self.dbName = 'sample.db'
          self.accounts_table = 'Accounts'
          
          con = sqlite3.connect(self.dbName)
          # もしテーブルが無ければ作る
          # integerはprimary keyにすると自動でauto incrementになっている
          con.execute("create table if not exists %s("
                      "id integer primary key,"  # 連番(主キー)
                      "username string,"
                      "password string)"
                      % (self.accounts_table))
          con.commit()  # SQLを確定
          con.close()
    
      def setAccount(self, userName: str, passWord: str):
          con = sqlite3.connect(self.dbName)
          con.execute(
              "insert into %s(username,password) "
              "values('%s','%s')"
              % (self.accounts_table, userName, passWord))
          con.commit()
          con.close()
    
    # 引数のaccountIDを使って，Accountsテーブルからサービス情報を取得
    # 見つかれば辞書型で['username', 'password']を返す
    # 見つからなければ空リストを返す
-     def getAccount(self, accountID: int) -> list:
-         accountData = []
-         try:
-             con = sqlite3.connect(self.dbName)
-             cur = con.cursor()
-             cur.execute(
-                 "select * from %s where id = %s"
-                 % (self.accounts_table, accountID)
-             )
-             result = cur.fetchall()
-             # 見つからなかったら空リストを返す
-             if len(result) == 0:
-                 return accountData
-             # 辞書型に変換
-             accountData = dict(zip(
-                 ['userName', 'passWord'],
-                 [result[0][1], result[0][2]]
-             ))
-             con.close()
-         except:
-             traceback.print_exc()
-         return accountData

+     def getAccount(self, accountID: int) -> list:
+         accountData = []
+         con = sqlite3.connect(self.dbName)
+         cur = con.cursor()
+         cur.execute(
+             "select * from %s where id = %s"
+             % (self.accounts_table, accountID)
+         )
+         result = cur.fetchall()
+         # 見つからなかったら空リストを返す
+         if len(result) == 0:
+             return accountData
+         # 辞書型に変換
+         accountData = dict(zip(
+             ['userName', 'passWord'],
+             [result[0][1], result[0][2]]
+         ))
+         con.close()
+         return accountData
    

  db = Database()
  # DBにデータ挿入
  # db.setAccount('user1', 'pass1')
  # db.setAccount('user2', 'pass2')
  # 挿入したデータをidから取得
  result = db.getAccount([1])
  print('id 1: ', end='')
  print(result)
```

getAccount()の結果
※わかりやすく最後のほうだけ表示

```console
  File "[パス]/sample.py", line 60, in <module>
    result = db.getAccount([1])
  File "[パス]/sample.py", line 40, in getAccount
    % (self.accounts_table, accountID)
sqlite3.OperationalError: no such column: 1
```

最後が「1のようなcolumnは無いです」なのは同じだが，少し上に
result = db.getAccount([1])
という一文があるので気付ける．

#まとめ
* SQL処理で本来数値を送るところ，誤ってリストごと送ってしまうとハマる
* 生の実行時エラーなら気付ける
* tracebackいらないじゃんと思われるが，そもそもGUIアプリで実行時エラーを起こすとトレースされずに落ちて困るから入れていた
* GUIアプリでSQL関連のエラーを起こすとトレースしてくれるのかは未確認．もしトレースしてくれないとしたら八方塞がり