「そのSQLに祝福を」SQL-Bless 開発スレ
hymkor/sqlbless: The Command-line Database Client とは
- マルチDB(Oracle/SQL Server/PostgreSQL/MySQL)に対応した、ターミナル用データベースクライアント
- 複数行対応 ReadLine を内蔵、巨大な SQL でもインラインで編集可能(上矢印キーで入力済みの前の行にカーソルを移動して修正できる)
- SELECT の結果は、ロールアップ・ダウン可能な内蔵CSVビューアで、無理なく閲覧できる(その場で CSV 形式でファイル保存も可能)
次のステップ
- CSVエディターで、Microsoft Access みたいにレコードを編集できるようにする。
-
EDIT テーブル名 [WHERE 条件]
というコマンドを実行すると、ヒットした行を CSVエディターで開く。 - エディターで修正した内容は、UPDATE/INSERT/DELETE文で表現して、それを実行してよいか、適宜ユーザに確認する。
- 実行した後も ROLLBACK 可能(デフォルトで AUTO COMMIT オフでトランザクションで実行するようになっているため)
-
進捗
- EDIT文自体は実装済み。テストコードが実装しきれていない
- 更新時の日時の型は変換関数が各DBバラバラで対応しきれていない
- 読みだす時は結構雑でも大丈夫が、書き出す時は厳密にしなくてはいけない
- 普通のクライアントであれば、バインド変数を使えばよい(使うべき)
- だが、本ツールでは SQL を目視確認させた上で、それを実行するという形をとっているため、フルテキスト化した SQL と実際に実行させる SQL の差異があるのはよくないということで、どうしても一旦フルテキストな SQL にしなくてはいけない。
- DATE, TIME, DATETIME, TIMESTAMP…。これらのためのテストデータを作成する SQL を4DBそれぞれのために作らないといけない(げっそり)
なんで、EDIT文を作るために、急に日時表現をガチでやらないといけないかというと
- レコードを更新する際には、普通、レコードを一意にする主キーを把握しておかなくてはいけない(Microsoft Access でリンクテーブルを作る際にもいちいち主キーを指定しなければいけない)
- マルチDB対応となると、主キーをいちいち調べるのもたいへん(プログラムもユーザ、どっちがやるにしてもたいへん)
- 別に主キーだけで指定しなくても、変更前の全カラムについて WHERE で指定すれば、主キー調べなくてもいいじゃん (俺天才)
- そうすると、日時を含むテーブルを編集する際に、
… AND 日時のカラム = TO_DATE('変更前の日付','YYYY-MM-DD HH24:MI:SS')
(ORACLEの場合)みたいに、日時を正確に一致させる等式を書かなくてはいけない
というわけ
日本語版の README 追加
csviのオートパイロットに、SQL-Bless(厳密にはgo-readline-ny)のオートパイロットを連結するのに成功。ようやく自動テストが書ける。
自動テストではキーボード操作を外部から挿入してやらんといけないので、オートパイロット的な仕組みが必要なんだが、各パッケージによって微妙に使用が違うんだよな。だからアダプタ的なタイプを作らないといけなかった。
RDBMS | 日付+時刻 | 日付 | 時刻 |
---|---|---|---|
Oracle | Date | x | x |
PostgreSQL | TimeStamp | Date | Time |
SQL Server | DateTime | Date | Time |
MySQL | DateTime | Date | Time |
日時リテラル表現 (検証中)
RDBMS | 種類 | 型 | <2006年1月2日15時4分5秒>を表す表現 |
---|---|---|---|
Oracle | 日時 | DATE | TO_DATE('2006-01-02 15:04:05','YYYY-MM-DD HH24:MI:SS') |
PostgreSQL | 日時 | TIMESTAMP |
TIMESTAMP '2006-01-02 15:04:05' [1]
|
日付 | DATE | DATE '2006-01-02' |
|
時刻 | TIME | TIME '15:04:05' |
|
SQL Server | 日時 | DATETIME |
CONVERT(DATETIME,'2006-01-02 15:04:05',120) [2]
|
日付 | DATE | CONVERT(DATE,'2006-01-02',23) |
|
時刻 | TIME | CONVERT(TIME,'15:04:05',108) |
|
MySQL | 日時 | DATETIME |
STR_TO_DATE('2006-01-02 15:04:05','%Y-%m-%d %H:%i:%s') [3]
|
DATETIME '2006-01-02 15:04:05' [4]
|
|||
TIMESTAMP | TIMESTAMP '2006-01-02 15:04:05' |
||
日付 | DATE | STR_TO_DATE('2006-01-02','%Y-%m-%d') |
|
DATE '2006-01-02' |
|||
時刻 | TIME | STR_TO_DATE('15:04:05',%H:%i:%s') |
|
TIME '15:04:05' |
-
MySQL :: MySQL 8.0 Reference Manual :: 13.2.6 Fractional Seconds in Time Values
他にも1秒未満を表現可能な型があるが、省略(やってられん)
-
https://www.postgresql.jp/document/9.2/html/datatype-datetime.html ↩︎
-
https://learn.microsoft.com/ja-jp/sql/t-sql/functions/cast-and-convert-transact-sql ↩︎
-
https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html ↩︎
-
MySQL :: MySQL 8.0 Reference Manual :: 11.1.3 Date and Time Literals ↩︎
DB が起動しているかどうかチェック
- Oracle
-
oracle.exe
というプロセスが一つ起動している
-
- PostgreSQL
-
postgres.exe
というプロセスが6つ起動している
-
- SQL Server
-
sqlservr.exe
,SQLAGENT.EXE
というプロセスが起動している
-
- MySQL
-
mysqld.exe
というプロセスが3つ起動している
-
5/25 進捗 v0.11 への道
-
{Oracle,PostgreSQL,SQL Server,MySQL} × {日付時刻,日付,時刻} の組み合わせを
EDIT
文で取り扱うテストコードの実装 -
EDIT
文を使うデモンストレーションGIFアニメーション(まだ) -
csviのオートパイロットに、SQL-Bless(厳密にはgo-readline-ny)のオートパイロットを連結
⇒ 一応できたものの、不具合があり、それを直した結果、汚い実装になってしまった。技術的負債化 -
csvi → SQL-Bless へ戻る際のキーやメッセージを検討
-
c
キー:Apply the changes ? [y/n]
:y
で反映 -
q
キー:反映を行わずに終了
-
-
コマンドラインパラメーターの改善(今のちょっと今回は見送りsqlbless DB種類 URL
という形式は、DB種類ごといバラバラでちょっと分かりづらい可能性がある) -
README改訂(
_README.md
で作成してリリース直前で差し替え) -
セミコロン以外のスラッシュ、GOなどをターミネーターにする今回は見送り -
-tsv
などのオプションを指定した時でも、EDIT
はちゃんと動くか?
→SELECT
,EDIT
ともに列がおかしくなっていた。修正 - 入力しかけの状態から、ヒストリ過去→ヒストリ未来とすると、最初に入力したものが消えてしまうのはつらいから復元できるようにする(bash だとそういう動作になってた:本当は最新以外のヒストリも編集したら残るようだが、そこまでエミュレートするのは go-readline-ny のヒストリの仕様では難しい)
SQLite3 サポート依頼。着手
- (1)"database/sql" ドライバーの import
- (2)sqlbless だけで起動したときの説明テキスト
- (3)テーブルの一覧を得る SQL を用意 (DESC 文で表示する)
-
(4)テーブルのカラム一覧を得る SQL を用意 (DESC テーブル名 文で表示する)
→ SELECT文ではなく PRAGMA 文を使う。テーブル名を引数にするプレースホルダが使えなかったので、そこだけ直接文字列を置換する方法で実装 -
(5)日付+日時・日付・時刻の型名と time.Time から リテラルへの変換する関数 (EDIT 文で UPDATE するときに必要)
→ SQLite に日付・時刻型はないので不要 -
(6)(5)のテストスクリプト(共通処理は既存のスクリプトがそのまま使えるはず)
→ (5)が不要なので、不要
scoop install sqlite
でインストールした sqlite3 で作成したDBファイル、普通に読み書き出来たので、これで対応完了かな。
リクエストあっての対応だったけど、おかげで簡単な動作検証のためだけに Oracle インスタンスの起動をいちいちしなくてもよくなったのは嬉しい。
実行ファイルが巨大なので、これ、パッケージサイズ大丈夫かなと思って、ためしにmake dist でパッケージを仮に作ってみようとしたら、エラー発生
Windows の 32bit版のビルドの際、シンボル未定義エラーが出てる(64bit版なら作れてる)
# なお、パッケージサイズは許容範囲だった
glebarez/go-sqlite のかわりに、mattn/go-sqlite3 に切り替えてみたが、32bit版の gcc がローカルマシンにインストールされていないので、こっちはこっちでビルドできねぇ…
(64bit 版だと、テストも一通り通ったんだけどねぇ)
sqlite3 package - github.com/mattn/go-sqlite3 - Go Packages
- Open a terminal for the TDM-GCC toolchain, which can be found in the Windows Start menu.
For example the TDM-GCC Toolchain can be found here
あー、TDM-GCC がないといけないのかー。ネイティブ版Luaを使ってたころお世話になったヤツだ。
$ scoop uninstall gcc
Uninstalling 'gcc' (11.2.0).
Unlinking ~\scoop\apps\gcc\current
Removing ~\scoop\apps\gcc\current\bin from your path.
$ scoop install tdm-gcc
Couldn't find manifest for 'tdm-gcc'.
$ scoop bucket add versions
Checking repo... OK
The versions bucket was added successfully.
$ scoop install versions/tdm-gcc
Installing 'tdm-gcc' (10.3.0) [64bit] from 'versions' bucket
tdm64-gcc-10.3.0-2.exe (76.7 MB) [==============================================] 100%
Checking hash of tdm64-gcc-10.3.0-2.exe ... ok.
Extracting tdm64-gcc-10.3.0-2.exe#dl.7z ... done.
Running pre_install script...
Linking ~\scoop\apps\tdm-gcc\current => ~\scoop\apps\tdm-gcc\10.3.0
Adding ~\scoop\apps\tdm-gcc\current\bin to your path.
'tdm-gcc' (10.3.0) was installed successfully!
$ GOARCH=386 make
go fmt
set "CGO_ENABLED=1" && go build -ldflags "-s -w -X main.version=v0.11.0-9-g572344e"
$ pwsh test.ps1 tstdblst
# SQL-Bless v0.11.0-9-g572344e-windows-386 by go1.22.3
:(中略)
SQL> EXIT
--> OK
SQLite3 用のドライバーのチョイスだけど、結局
OS | 386 | amd64 |
---|---|---|
Linux | glebarez/go-sqlite | glebarez/go-sqlite |
Windows | mattn/go-sqlite3 | glebarez/go-sqlite |
というチャンポンにしちゃったヨ。TDM-GCC だと、Linux 用バイナリを作れないみたいなんだ…
今後の課題は
- テストコードが PowerShell(pwsh.exe)だが、Linux でもテストできるようにした方がよいかも
- DB固有の細かいカラム型にも対応
ってところだろうか
wsl の Ubuntu に PowerShell 7.4 をインストールして、SQL-Bless の SQLite3 モードのテストスクリプト( pwsh ./test/test-sqlite3.ps1
)を動かしてみたが、とくに問題ないから、ヨシ!
(SQLite3 、メモリーDBがあるから、テストに最適なんだよな)
いつものように解説記事かいた。
あー、Edit したときの SQL が Spool で記録されていないなぁ。これはよくない。よくないぞぉぉぉ
-
o
やO
を入力すると、変更セルが常に新レコードの最初のセルになってしまう。カーソルを同じ列でないとまずい。
→ Csvi側の問題なので、Csvi側で対応(リビジョンはまだ上げず、SQL-Bless側でgo get -u github.com/hymkor/csvi@master
で取り込み) -
(主に csvi の話) ヘッダー行を編集できてしまうのもまずい。ヘッダー行を READONLY にすべき。案が複数ある
-
-ph
(protect-headerフラグ) 。ヘッダー行を変更不能にするオプションを追加。柔軟性があるが、面倒くさい -
-h num
オプションを使ったとき、自動的にヘッダー行を READONLY にする。現在、ヘッダー行はデフォルトで1行になっており、ユーザが特に意識していない時に編集不可能になっているのはよくない- オプション指定していない時はヘッダー1行だが、READONLY にしないようにする。
- デフォルト値をマイナスなどにして、指定した時と区別が必要だが、ヘルプで表示されるデフォルト値が不自然
-
-h num
のデフォルト値をゼロ行にしてしまう。そうすれば整合性はあう- README の例を書き直す必要がある
- オプション指定していない時はヘッダー1行だが、READONLY にしないようにする。
-
でも、CSVエディターとしては見出しが編集できてしまっても別に問題ないか…
データベースのテーブルエディターでは変更できてしまうと誤動作の元なので、それは別途プロテクトフラグを設けるか…
- 結局、
-p
オプションで、ヘッダ行を編集できないようにできるようにした。 - パッケージとしては
csvi.Config.ProtectHeader bool
というフィールドを設けた。
-
EDIT
で編集した後、SQL でエラーが発生したときの処理を改善- 複数修正レコードがある場合、
c
: Continue(残りも実行),a
: Abort(全部中断) を選べるようにした。 - Abort を選んだ時、残りの SQL は実行しないが、キャンセルした旨を spool ファイル・標準エラー出力に記録するようにした。
- 複数修正レコードがある場合、
- 将来的に、エディター側での入力チェックを設けてもよいかもしれない(要csviパッケージ修正:直近のリリースでは見送る)
- 今のところ、EDIT文で不適切な SQL を生成する可能性が結構ある。そこはユーザによる目視チェックでカバーするとして、それゆえに「全てのレコードについて、各SQLをノーチェックで実行する」という選択肢は当面設けないことにする
- 「エディター側での入力チェック」実装しちゃった
SQL-Bless で、データベースの NULL を U+2400 で表現することを検討したが、フォントの要素もあるが、ちょっと見えにくいねぇ(なお、カスタマイズ可能)
# 入力時は x や d を押下すると、NULL化できるようにした。なお、NOT NULL チェックは忘れている模様
エディター上での入力時のNULLチェックも実装はしたが、ドライバーの仕様で効かない場合があるので「チェックできたらチェックする」になった。
(SQL実行時にエラーを吐いてもらうしかない)
The driver has Go-based implementation of SQLite embedded in itself
ChatGTPによる翻訳:
私の記憶が正しければ、windows/386のサポートは一度も動作しませんでした。再度有効にして、windows/386用の適切なクロスコンパイラがインストールされたLinuxマシンでジェネレータを実行してどこで失敗するか確認することはできます。しかし、ccgo/v3はSQLite 3.41で停滞しています。詳細は#154(閉鎖済み)を参照してください。
この問題を解決する過程で、現在サポートされているすべてのターゲットをccgo/v4を使用して生成する進展があります。現時点では、windows/amd64とwindows/arm64だけが未完了です。進捗状況はmodernc.org/{libc,libz,libtcl8.6,libsqlite3}リポジトリのそれぞれのwinブランチで追跡できます。
したがって、windows/386サポートを追加する努力は、ccgo/v4ライブラリのために行うべきだと思います。この点でのご協力は大変ありがたいです。
現時点では、libtcl8.6のwindows/amd64サポートの完成に非常に近づいており、その後にlibsqlite3が依存しています。libsqlite3もwindows/amd64をサポートするようになったら、windows/386の追加に取り組むのが適切なタイミングになるでしょう。
全DB小数秒対応完了
- SQL Server が一番難産
- 日付時刻フォーマットの選択を定数(マジックナンバー)でやるところが分かりにくい
- さりげに小数点に
.
ではなく:
を使っているフォーマットが混ざっている(ヨーロッパ向け仕様) -
SMALLDATETIME
型、分までしか記録しない型だと知らなかったので、なぜ秒がクリアされるんだろうと小一時間悩んだ
- MySQL、今までリテラル書くのに
STR_TO_DATE
関数を通していたが、PostgreSQL 同様に型名 'リテラル'
という表現があったのに気づいたので、全部修正 - Oracle、データディクショナリ―(ALL_TAB_COLUMNS)で出る TIMESTAMP型の名前が
TIMESTAMP(精度)
になっていて変則的(精度は別カラムに格納して、名前は ‘TIMESTAMP‘ だけにするのが普通)
一応、日付・時刻型はすべてサポートできたはず。残すは時差・TZ対応
タイムゾーン対応
Oracle
DATE型にはなし
型名 | リテラル表現 |
---|---|
TIMESTAMP |
TO_TIMESTAMP_TZ('%s','YYYY-MM-DD HH24:MI:SS.FF TZH:TZM')" |
PostgreSQL
型名 | リテラル表現 |
---|---|
TIMESTAMPTZ |
TIMESTAMP WITH TIME ZONE '2006-01-02 15:04:05.999999999 -07:00' |
TIMETZ |
TIME WITH TIME ZONE '15:04:05.999999999 -07:00' |
MySQL
'2020-01-01 10:10:10+05:30'
- Go言語のドライバーはデフォルトでは
time.Time
ではなく、[]byte
型へ展開してしまう[1] → SELECT した時刻のタイムゾーン成分が +00:00 になってしまう - タイムゾーンは TIMESTAMP型にはあるが、DATETIME型にはない。[2]
中途半端なタイムゾーン対応をすると、WHERE句のマッチングが動作しなくなってしまう。今回は見送りすべきか
参考
- MySQL :: MySQL 8.0 リファレンスマニュアル :: 9.1.3 日付リテラルと時間リテラル
- MySQL :: MySQL 8.0 リファレンスマニュアル :: 5.1.15 MySQL Server でのタイムゾーンのサポート
よい記事
OracleおよびPostgreSQLのtimestamp with time zone型が格納する値はその名前に反し、絶対時刻です。 タイムゾーン情報はまったく含まれていません
しっくりくる話だ。なぜなら、世界時の12時と、同じ日の日本時の21時は表現が違うだけで同じ時刻だからイコール関係にないとおかしいはず。だから、格納する時に添えられたタイムゾーン情報から世界時に変換して格納する、取得時にクライアントのタイムゾーン情報に合わせて現地時に直すという動作は期待どおりでヨシ!
SQL Serverでは
datetime型やdatetime2型が概念時刻を格納し、datetimeoffset型が、なんと概念時刻+タイムゾーンを格納します。
datetime,datetime2型は OK(概念時間=タイムゾーン情報はない)。datetimeoffset型って、これも時刻だったのか。time.Duration みたいな時間差型かと勘違いしてたわ
go-sql-driver/mysqlと日時データ型とタイムゾーン
- parseTimeはtrue
- locに指定するタイムゾーンとMySQLのタイムゾーンを一致させる
後者についてはこれらがずれてしまっていると、例えばTIMESTAMP型に内部的に保存されている値は通常UTCで解釈すべき値ですが、JSTとして解釈しないといけない値が入ってしまっているといった事態が発生します
これ、マジでそうだった。接続文字列に ?parseTime=true&loc=Asia%2FTokyo
を指定するようにしたら、今まで WHERE で TIMESTAMP が全然イコールで一致させられなかったのが、期待どおり動くようになった。
MySQL のGoドライバー、loc=Asia%2FTokyo
と書かねばならないところを、loc=Local
でも一応期待動作したんだけど、これ大丈夫なのかしら…
Goでの time.Location のあれこれ #Go - Qiita
サーバーのローカルタイムゾーン情報は、以下のように明示的にタイムゾーン名を指定せずに、"Local" を引数とすると取得できる
マジか(「サーバー」という表現が気になったが、プログラムを実行してるマシンだよね)
接続文字列にフィルターをかませられるようにして、短く書けるようにした(一応、上書き可能)
sqlbless mysql "root:@/mydb?parseTime=true&loc=Asia%2FTokyo"
↓
sqlbless mysql "root:@/mydb?parseTime=true&loc=Local"
↓
sqlbless mysql "root:@/mydb"
末尾のリンク集が有用
PostgreSQL, MySQL, SQLite3 and Oracle 対応の TUI クライアント。オプション体系が参考になる