やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

SQLite3ドットコマンド(.selftest)

 テストケースを表に入れて実行する。

成果物

.help

.selftest ?OPTIONS?      Run tests defined in the SELFTEST table
sqlite3 :memory: ".help selftest"
.selftest ?OPTIONS?      Run tests defined in the SELFTEST table
    Options:
       --init               Create a new SELFTEST table
       -v                   Verbose output

.selftest

引数なし

sqlite3 :memory: ".selftest"
Missing SELFTEST table - default checks only
0 errors out of 1 tests

--init

sqlite3 :memory: ".selftest --init"
Tests generated by --init
0 errors out of 2 tests

-v

sqlite3 :memory: ".selftest -v"
0: memo Missing SELFTEST table - default checks only
Missing SELFTEST table - default checks only
1: run PRAGMA integrity_check
Result: ok
0 errors out of 1 tests

SELFTEST

sqlite3 :memory: ".selftest --init" ".headers on" "select * from SELFTEST;" "select sql from sqlite_master;"
Tests generated by --init
0 errors out of 2 tests
tno|op|cmd|ans
100|memo|Tests generated by --init|
110|run|SELECT hex(sha3_query('SELECT type,name,tbl_name,sql FROM sqlite_master ORDER BY 2',224))|EFDBCA69E0514C06E30E3C0428946EDEACCB90B48A952950A8651BF1
120|run|PRAGMA integrity_check|ok
CREATE TABLE selftest(
  tno INTEGER PRIMARY KEY,
  op TEXT,
  cmd TEXT,
  ans TEXT
)

 これをマネすればテストできるってわけか。

列名 英名予想 意味予想
tno Test No テスト番号。値が小さい順に実行する
op Operation type memo,runのいずれか。memoは実行せず飛ばす。runは実行対象。
cmd Command テスト内容。.testcaseで指定するSQL文と同じ。
ans Answer 期待値。

 レコードをみてみるとopは以下。

op 意味予想
run テストコード。cmdの出力結果がansと一致するか確認する
memo コメント。標準出力される

テスト作成

sqlite3 :memory: \
".selftest --init" \
"delete from SELFTEST;" \
"insert into SELFTEST(op,cmd,ans) values('run', 'select 1;', '1');" \
"insert into SELFTEST(op,cmd,ans) values('run', 'select 1.2;', '1.2');" \
"insert into SELFTEST(op,cmd,ans) values('run', 'select ''A'';', 'A');" \
"insert into SELFTEST(op,cmd,ans) values('run', 'select x''FF'';', x'FF');" \
"insert into SELFTEST(op,cmd,ans) values('run', 'select 1, ''A'' union select 2, ''B'';', '1,A|2,B');" \
"select * from SELFTEST;" \
".selftest"

 実行結果は以下。

Tests generated by --init
0 errors out of 2 tests
1|run|select 1;|1
2|run|select 1.2;|1.2
3|run|select 'A';|A
4|run|select x'FF';|5|run|select 1, 'A' union select 2, 'B';|1,A|2,B
0 errors out of 5 tests

列デリミタ,、行デリミタ|

sqlite3 :memory: \
".selftest --init" \
"delete from SELFTEST;" \
"insert into SELFTEST(op,cmd,ans) values('run', 'select 1;', '1');" \
"insert into SELFTEST(op,cmd,ans) values('run', 'select 1.2;', '1.2');" \
"insert into SELFTEST(op,cmd,ans) values('run', 'select ''A'';', 'A');" \
"insert into SELFTEST(op,cmd,ans) values('run', 'select x''FF'';', x'FF');" \
"insert into SELFTEST(op,cmd,ans) values('run', 'select 1, ''A'' union select 2, ''B'';', '1|A\n2|B');" \
"select * from SELFTEST;" \
".selftest"

 結果は以下。

Tests generated by --init
0 errors out of 2 tests
1|run|select 1;|1
2|run|select 1.2;|1.2
3|run|select 'A';|A
4|run|select x'FF';|5|run|select 1, 'A' union select 2, 'B';|1|A\n2|B
5: Expected: [1|A\n2|B]
5:      Got: [1,A|2,B]
1 errors out of 5 tests

 注目すべきは以下。列デリミタ,、行デリミタ|になるらしい。

5: Expected: [1|A\n2|B]
5:      Got: [1,A|2,B]
1 errors out of 5 tests

 変更する方法は見当たらない。

op未指定エラー

sqlite3 :memory: \
".selftest --init" \
"delete from SELFTEST;" \
"insert into SELFTEST(op,cmd,ans) values('', 'select 1;', '1');" \
"select * from SELFTEST;" \
".selftest"
Unknown operation "" on selftest line 1

 opmemorunを指定すべき。

自作selftestテーブル

 opmemorunを指定すべきなら、テーブル定義をop TEXT default('run') check(op='memo' or op='run'),にして欲しかった。

 自作したらできた。

echo "create table selftest(
  tno INTEGER PRIMARY KEY,
  op TEXT default('run') check(op='memo' or op='run'),
  cmd TEXT,
  ans TEXT
);" > selftest_create.sql
sqlite3 :memory: \
".read selftest_create.sql" \
"insert into SELFTEST(cmd,ans) values('select 1;', '1');" \
"insert into SELFTEST(op,cmd) values('memo','コメント');" \
".selftest"
コメント
0 errors out of 1 tests

 おお、このほうがいいじゃん。まあこれでもinsert文が冗長だと思うが。

.testcase + .checkのほうが簡単

 これと同じことは以下ドットコマンドでできる。

sqlite3 :memory: \
".testcase 001" \
"select 1;" \
".check 1"
testcase-001 ok

 どうみてもこっちのほうが簡単。

 でも.checkは期待値が複数行にまたがっていると指定できない……。

期待値が複数行なら.selftestでやる

 .checkでは指定できない。確認してみる。まずは準備。

sqlite3
create table T(A text);
insert into T values('1行目
2行目');

.selftest

.selftest --init
delete from selftest;
insert into selftest values(100,'run','select * from T;','1行目
2行目');
.selftest

 結果は以下。成功。

0 errors out of 1 tests

.testcase + .check

 いろいろ試したがダメだった。

改行そのまま

.testcase multi_line
select * from T;
.check '1行目
2行目'

 結果は以下。

testcase-multi_line FAILED
 Expected: [1行目]
      Got: [1行目
2行目
]
sqlite> 2行目'
   ...> 
  • 1行目と2行目の改行でコマンド終端とみなされてしまう
    • 文字列終端の'が開始とみなされて...>になってしまう

\n

.testcase multi_line
select * from T;
.check '1行目\n2行目'
testcase-multi_line FAILED
 Expected: [1行目\n2行目]
      Got: [1行目
2行目
]

 \nはそのまま文字とみなされてしまう。

char(10)

.testcase multi_line
select * from T;
.check '1行目' || char(10) || '2行目'
Usage: .check GLOB-PATTERN

 SQL文内なら\nchar(10)で表現できる。だが、.checkドットコマンド構文では無効。

(select char(10))

 先述と同じくダメ。

.testcase multi_line
select * from T;
.check (select '1行目' || char(10) || '2行目')
Usage: .check GLOB-PATTERN

.import + .selftestでテストケースをまとめる

 selftestテーブルのデータをCSVにして.importする手もある。

echo "tno,op,cmd,ans
1,run,\"select 1;\",1
2,memo,\"コメント\"," > selftests.csv
sqlite3 :memory: \
".mode csv" \
".import selftests.csv selftest" \
".selftest"
コメント
0 errors out of 1 tests

 だが、そうまでしてテーブルにまとめる意味や価値があるだろうか。一覧性があがる?

cat selftests.csv
tno,op,cmd,ans
1,run,"select 1;",1
2,memo,"コメント",

 一覧性はあるが、tno,opが冗長で微妙。

 あと、CSVのメタ文字,は、select 1, 'A';などSQL文でも使う。エスケープするためにダブルクォートする。だがSQLでも識別子化するためにダブルクォートを使う。そのときは\"でダブルクォートをエスケープすることになるだろう。面倒くせぇ……。

 でも一括実行&保存するには一番便利な方法か。

SQLテストの最適な方法は?

 知らん。.testcaseは1件ずつしかできないし、.selftestは冗長で行デリミタが|という謎仕様。

 だったらcsvで出力してdiffすればいいのでは? でもシェルがクソ言語すぎてうんぬん。

蛇足

 もっとこう、以下みたいな形式で書けないの?

# テスト名
期待値
SQL文

# 次のテストとの間に空行をあける
期待値
SQL文

(テスト名は省略できる)
期待値
SQL文

 たとえば以下みたいな。

testcases.case

# 1であることを確認する
1
select 1;

# 2であることを確認する
2
select 
  2;

3
select 3;

 改行がウザいなら期待値とSQL文との間を,で区切るとか。

1,select 1;

 コメントも含めて一行でやるならSQL文の終端;以降をすべてコメントにするとか。

1,select 1;1かどうかを確認する

 併用できるとか。

1,select 1;1かどうかを確認する
2,select 2;2かどうかを確認する
3,select 3;

# 複数レコード確認
1,A|2,B
select 
  1, 'A'
union
select 
  2, 'B';

 期待値の行デリミタを|から\nにしたいが、テスト間の区切りや、期待値とテスト間の区切りと区別がつけられなくなってしまう。さりとて余計なメタ文字をつくれば元の木阿弥。

1,select 1;1かどうかを確認する
2,select 2;2かどうかを確認する
3,select 3;
# 4かどうかを確認する
4,select 4;

# 複数レコード確認
1,A
2,B
select 
  1, 'A'
union
select 
  2, 'B';

 2,Bも期待値であると、どうやって判定する? selectでないこと? もし期待値が文字列select ...を期待していたら? 完結にうまく表現できない。XMLみたいにタグで囲って意味づけすれば元より冗長になってしまう。

 空行する。もし期待値とSELECT文が揃っていないのに空行があれば、それは期待値が複数行であるということ。そう解釈させればいけるか。

# 複数レコード確認
1,A
2,B

select 
  1, 'A'
union
select 
  2, 'B';

# 複数レコード確認
1,A
2,B

select 
  1, 'A'
union
select 
  2, 'B';

 パーサ実装するの面倒そう。

類似コマンド

  • .testcase: SELFTEST.cmd相当。直下のSQLをそれにする
  • .check: SELFTEST.ans相当。引数をそれにする。テスト実行も兼ねる。

対象環境

$ uname -a
Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux

前回まで