SQLite3ツール 操作ガイド・仕様書
【概要】
本ツールは、SQLiteデータベースに対するデータベースの作成・オープン、構造確認、テーブルブラウズ、SQL実行を単一の画面上で行うための、データベース学習・管理用ツールである。Python標準ライブラリの sqlite3 と GUIツールキットの tkinter で構成され、追加のパッケージなしで動作する。
本資料では、ツールの操作手順、機能仕様、実行ファイル(exe)化の手順、および全ソースコードを示す。
【目次】
- 第1章 操作ガイド
- 第2章 概要
- 第3章 画面構成の基本方針
- 第4章 画面レイアウト・領域構成(トポロジー)
- 第5章 機能一覧
- 第6章 トランザクション制御
- 第7章 実行ファイル(exe)化の手順
- Python実行環境の準備とPythonコード全体
【関連する外部ページ】
【サイト内の関連情報】
- SQL の基本文法(SELECT・INSERT・UPDATE・DELETE など)
- Python 環境構築とパッケージ管理(pip)
第1章 操作ガイド
- DBの作成・接続: ツールバーの「新規DB作成」または「DBオープン (Ctrl+O)」から、対象のデータベースファイルを選択します。
- オブジェクトの検索: 「スキーマナビゲータ」上部の検索ボックスに文字列を入力し、テーブルやインデックスを絞り込みます。
- 定義とデータの確認: リストから対象を選択すると、「テーブル定義ビューワ」にCREATE文が、「出力」のデータグリッド(Treeview)に先頭1000件のレコードが表示されます。
- 命令の入力: 「SQLエディタ」に、標準SQL文またはドットコマンド(
.import/.export)を記述します。 - SQLの一括実行: 「▶ SQL実行 (F5)」を押下すると、「SQLエディタ」内の命令が一括実行され、「出力」に結果や完了メッセージが表示されます。
- 実行計画の解析: 「実行計画解析 (F6)」を押下すると、「出力」のテキスト枠(Text)に、物理操作計画と仮想機械(VDBE:SQLiteがSQLを変換して実行する内部のバイトコードエンジン)命令の日本語解説が表示されます。
- CSVデータの読込: ツールバーの「CSV読込」ボタン、または
.import "ファイルパス" "テーブル名"の実行によりデータを取り込みます。 - CSVデータの保存: データグリッド上の右クリックメニュー「この結果をCSVで保存」、または
.export "ファイルパス" "テーブル名"を実行します。 - SQLファイルの入出力: ツールバーの「SQL読込」または「SQL保存 (Ctrl+S)」で、「SQLエディタ」の内容を
.sqlファイルとして読み書きします。 - トランザクション制御: 自動コミットは無効化されているため、変更操作後は必ずツールバーの「コミット」または「ロールバック」ボタンで手動制御してください。
第2章 概要
本ツールは、データベースの作成・オープン、テーブルやインデックスの構造確認、データの閲覧、SQL文の実行を、1つのウィンドウ内で完結させることを目的とする。特別なサーバを必要としないSQLiteの特性を活かし、初学者が手元でSQLの挙動を確認しながら学習できるように設計されている。
第3章 画面構成の基本方針
- ファイル操作の直感化: ファイルの選択・保存にはOS標準ダイアログを使用する。
- 非ダイアログ・エラー通知: 強制ポップアップ(モーダルダイアログ)は使用せず、画面内の専用領域に直接テキスト(エラー時は赤字)で表示する。
- フォントの使い分け: UI全体の基本フォントは「Meiryo(メイリオ)」とする。ただし、コードやデータの視認性を高めるため、「SQLエディタ」「テーブル定義ビューワ」「出力」の各コンポーネントには等幅フォント(Consolas/Menlo)を適用する。
- 配置・文字切れ防止: ウィンドウ内はPanedWindow(区切り線で領域を分割できるウィジェット)で区切り、左側の領域には適切な初期幅(width=28)を確保することでラベルの文字切れを防ぐ。
第4章 画面レイアウト・領域構成(トポロジー)
画面全体の可視性を高めるため、以下の4つの画面を配置する。
- スキーマナビゲータ(画面左側): データベース内のメタデータ(テーブル/インデックスなど、データ構造を定義する情報)を一覧表示する領域。
- テーブル定義ビューワ(画面中央上): 選択されたオブジェクトの CREATE 文を提示する領域。
- SQLエディタ(画面中央下): ユーザーがSQLやドットコマンドを記述する領域。
- 出力(画面右側・縦長): テーブルデータ、実行成否、エラー、および実行計画(操作順序・内部命令)を縦長に広く展開して提示する、中心的な解析領域。
第5章 機能一覧
5.1 ツールバー・ステータスバー領域
- OS標準ダイアログを用いたDBの「新規作成」「オープン(Ctrl+O)」を行う。各ボタンにはショートカットキーを明記する。
- 画面最下部のステータスバーを左右分割構造とし、左端に接続中のDBの絶対パス(OS準拠のフルパス)、右端に使用中のSQLiteバージョン情報を常時表示する。
5.2 スキーマナビゲータ
- データベース内のテーブルおよびインデックスの一覧を表示する。
- リスト上部の検索ボックスに入力された文字列で、オブジェクト名をリアルタイムに絞り込む(インクリメンタルサーチ:入力するたびに候補を絞り込む方式)。
- 対象の選択時に各領域の内容を連動更新し、別のDBへ切り替えた際は表示をクリアする。
5.3 テーブル定義ビューワ
- 選択したオブジェクトの定義(CREATE文等)を、等幅フォント・簡易シンタックスハイライト付きで表示する。
5.4 SQLエディタ
- 標準SQL文およびドットコマンド(
.import/.export)を記述・実行できる。左端に行番号を常時表示し、リアルタイムでのシンタックスハイライト(キーワード、文字列、コメントの色分け)を行う。 - 「▶ SQL実行 (F5)」により、セミコロン(;)で区切られた複数のSQL文を一括実行する。
5.5 出力(SQL実行結果、テーブル、エラー、実行計画)
- 排他制御の競合回避: 通常のSQL実行時は結果レコードをデータグリッド(Treeview)で表示し、実行成功メッセージやエラー等は同領域のテキスト枠(Text)に切り替えて表示する。ただし、一括実行において最後にSELECT文が実行された場合は、結果を可視化するためTreeviewの表示を優先する(完了メッセージによる上書きを防ぐ)。
- 選択行のクリップボードコピー(Ctrl+C)や、右クリックメニューからのCSVエクスポートに対応する。
- 実行計画解析 (F6): エディタ内のすべてのSQL文に対して EXPLAIN QUERY PLAN(高レベルの物理操作計画)および EXPLAIN(低レベルの仮想機械命令)を自動実行し、その生データをそのまま表形式で出力する。さらに、出現する数値引数(P1, P2, P3など)が指し示す「列番号」「レジスタ位置」「ジャンプ先アドレス」の意味を翻訳した日本語解説を、カラー強調付きで併記する。
5.6 CSV・ファイル入出力機能
- CSV読込: 選択したCSVのファイル名をテーブル名とし、列名を自動でクォーテーション保護してインポートする(テーブル名および列名に含まれる二重引用符は適切にエスケープ処理を行う)。
- ドットコマンド対応:
.importや.exportコマンドの引数において、スペースを含むパス(クォーテーション囲み)を安全にパースして処理する。 - SQL保存/読込: エディタの内容をBOM付きUTF-8の
.sqlファイルとして保存(Ctrl+S)または読み込む。
第6章 トランザクション制御
- 暗黙的な自動コミットを抑止する設定(
isolation_level="")とし、手動トランザクション制御を徹底する。専用ボタンまたはSQL文での制御を可能とする。
第7章 実行ファイル(exe)化の手順
本ツールを、Pythonがインストールされていない環境でも起動できる単一の実行ファイル(exe)にまとめる手順を示す。ここでは PyInstaller(Pythonアプリを実行ファイルにパッケージ化するツール)を使用する。
7.1 PyInstallerのインストール
PyInstallerが未インストールの場合は、次のコマンドで導入します。
pip install -U --no-user pyinstaller
7.2 コードをファイルに保存
上記のコードを、例えば sqlite_manager.py という名前で保存します(UTF-8で保存してください。日本語コメントが含まれるため重要です)。
7.3 exe化コマンドの実行
コマンドプロンプトで、保存したファイルがあるフォルダに移動してから実行します。
pyinstaller --onefile --noconsole sqlite_manager.py
各オプションの意味は次のとおりです。--onefile は全体を1つのexeにまとめる指定、--noconsole(--windowed でも同じ)は GUI アプリのため背後の黒いコンソール窓を出さない指定です。このコードは tkinter の GUI アプリなので、この2つを付けるのがおすすめです。
dist フォルダの中に sqlite_manager.exe が生成されます。第8章 Python実行環境の準備とPythonコード全体
Python 3.12 のインストール
Pythonのインストールを行い、Pythonのプログラムを実行する環境を整える。扱う環境は、Windows搭載パソコンである。金子研究室では、Python 3.12.10を推奨する。
[Windows での Python 3.12 のインストール手順を見るには、ここをクリック]
Windows での Python 3.12 のインストール
以下のいずれかの方法でPython 3.12をインストールする。Pythonがインストール済みの場合、この手順は不要である。
方法 1:winget によるインストール
【インストールコマンドの実行方法】
管理者権限でコマンドプロンプトを起動する(手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして、コマンド全体をコマンドプロンプトにコピー&ペーストする。
--scope machine を指定することで、システム全体(全ユーザー向け)にインストールされる。このオプションの実行には管理者権限が必要である。インストール完了後、コマンドプロンプトを再起動するとPATHが反映される。
REM Python 3.12 をシステム領域にインストール
winget install --id Python.Python.3.12 -e --scope machine --silent --accept-source-agreements --accept-package-agreements --override "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0 Include_pip=1 Include_launcher=1 InstallLauncherAllUsers=1 TargetDir=\"C:\Program Files\Python312\""
REM Python と Scripts を PATH 先頭に追加
powershell -NoProfile -Command "$p='C:\Program Files\Python312'; $s=\"$p\Scripts\"; $c=[Environment]::GetEnvironmentVariable('Path','Machine'); if((Test-Path $p) -and (';'+$c+';' -notlike \"*;$p;*\") -and (';'+$c+';' -notlike \"*;$s;*\")){[Environment]::SetEnvironmentVariable('Path',\"$p;$s;$c\",'Machine')}"
方法 2:インストーラーによるインストール
- Python公式サイト(https://www.python.org/downloads/)にアクセスし、「Download Python 3.x.x」ボタンからWindows用インストーラーをダウンロードする。
- ダウンロードしたインストーラーを実行する。
- 初期画面の下部に表示される「Add python.exe to PATH」にチェックを入れてから「Customize installation」を選択する。このチェックを入れ忘れると、コマンドプロンプトから
pythonコマンドを実行できない。 - 「Install Python 3.xx for all users」にチェックを入れ、「Install」をクリックする。
インストールの確認
コマンドプロンプトで以下を実行する。
python --version
バージョン番号(例:Python 3.12.x)が表示されればインストール成功である。「'python' は、内部コマンドまたは外部コマンドとして認識されていません。」と表示される場合は、インストールが正常に完了していない。
Python の開発環境 Visual Studio Code のインストールと Python 用の設定
Python の開発環境Visual Studio Code(プログラムを編集するソフトウェア。以下、VS Code)を整える。
[Windows での Visual Studio Code のインストールと Python 用の設定手順を見るには、ここをクリック]
Windows での Visual Studio Code のインストールと Python 用の設定手順
1. VS Code と拡張機能のインストール
以下のコマンドにより,既存の VS Code を削除し,全ユーザー共有の設定で再インストールしたうえで,拡張機能(VS Code に機能を追加するソフトウェア)をまとめて導入する.
【インストールコマンドの実行方法】
管理者権限でコマンドプロンプトを起動する(手順:Windows キーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして,コマンド全体をコマンドプロンプトにコピー&ペーストする。
インストールコマンド
REM ============================================================
REM Microsoft Visual Studio Code
REM ============================================================
winget uninstall -e --id Microsoft.VisualStudioCode --silent --disable-interactivity --accept-source-agreements
rmdir /s /q C:\ProgramData\vscode-extensions 2>nul
rmdir /s /q "%APPDATA%\Code" 2>nul
rmdir /s /q "%USERPROFILE%\.vscode" 2>nul
rmdir /s /q "%LOCALAPPDATA%\Microsoft\vscode-update" 2>nul
REM VS Code をシステム領域に新規インストール
winget install --scope machine --id Microsoft.VisualStudioCode -e --silent --accept-source-agreements --accept-package-agreements
REM 全ユーザー共有の拡張機能フォルダ
mkdir C:\ProgramData\vscode-extensions 2>nul
icacls "C:\ProgramData\vscode-extensions" /grant "Everyone:(OI)(CI)M" /T
REM スタートメニューのショートカットを --extensions-dir 付きで再作成
rmdir /s /q "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code" 2>nul
del "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk" 2>nul
powershell -NoProfile -Command "$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk'); $lnk.TargetPath='C:\Program Files\Microsoft VS Code\Code.exe'; $lnk.Arguments='--extensions-dir \"C:\ProgramData\vscode-extensions\"'; $lnk.Save()"
REM ショートカットの検証
powershell -NoProfile -Command "$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk'); Write-Host 'TargetPath:' $lnk.TargetPath; Write-Host 'Arguments:' $lnk.Arguments"
REM ファイル / フォルダ右クリックの「Code で開く」を登録
reg add "HKLM\SOFTWARE\Classes\*\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%1\"" /f
reg add "HKLM\SOFTWARE\Classes\Directory\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%1\"" /f
reg add "HKLM\SOFTWARE\Classes\Directory\Background\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%V\"" /f
REM --extensions-dir 付きで起動する code.cmd ラッパを作成
REM (%* を echo で書くと対話的 cmd で失われるため、PowerShell で [char]37+'*' を書き出す)
powershell -NoProfile -Command "$pct=[char]37; $q=[char]34; $c='@echo off'+[char]13+[char]10+$q+'C:\Program Files\Microsoft VS Code\bin\code.cmd'+$q+' --extensions-dir '+$q+'C:\ProgramData\vscode-extensions'+$q+' '+$pct+'*'+[char]13+[char]10; [IO.File]::WriteAllText('C:\ProgramData\vscode-extensions\vscode.cmd',$c,[Text.Encoding]::ASCII)"
REM 拡張機能のインストール
set "CODE=C:\Program Files\Microsoft VS Code\bin\code.cmd"
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --uninstall-extension GitHub.copilot
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --uninstall-extension GitHub.copilot-chat
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.python
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.vscode-pylance
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.debugpy
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension MS-CEINTL.vscode-language-pack-ja
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension saoudrizwan.claude-dev
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension rust-lang.rust-analyzer
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension tamasfe.even-better-toml
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension anthropic.claude-code
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension almenon.arepl
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --list-extensions --show-versions
echo === セットアップ完了 ===
2. Python インタプリタの選択
同一マシンに複数の Python がインストールされている場合,VS Code で使用する Python 本体(インタプリタ:Python プログラムを解釈・実行するソフトウェア)を選択する必要がある.
- コマンドパレット(コマンド名で機能を呼び出す VS Code の入力欄)を開く(
Ctrl+Shift+P) Python: Select Interpreterと入力する
- 表示される一覧から,使用する Python(例:
C:\Program Files\Python312\python.exe)を選択する.
Python プログラム実行手順
[Windows での Python プログラム実行手順を見るには、ここをクリック]
Windows での Python 実行手順(Visual Studio Codeを使用)
プログラムファイルの作成と保存
- 左サイドバーの「エクスプローラー」アイコン(
Ctrl+Shift+E)をクリックする
- 「NO FOLDER OPENED」(作業対象フォルダが未選択の状態)と表示される場合は,「Open Folder」をクリックし,プログラムを保存するフォルダを選択する
続いて「フォルダを信用するか」を確認する画面(フォルダ内のコードを実行してよいか確認する VS Code の仕組み)が表示されるので,チェックして Yes を選択する
- フォルダ名の右側に表示される「新しいファイル」アイコンをクリックする
- ファイル名(例:
aitask.py.ファイル名は何でも良い)を入力しEnterを押す.拡張子は.py(Python ファイルを示す拡張子)とする
- 実行したいコードを選択し,
Ctrl+Cでコピーする.VS Code のエディタ領域にCtrl+Vで貼り付ける Ctrl+Sで保存する
プログラムの実行
- エディタ右上の三角形「▷」アイコン(Run Python File:現在開いている Python ファイルを実行するボタン)をクリックする.または,エディタ上で右クリックし「ターミナルで Python ファイルを実行」を選択する
- VS Code 下部のターミナル(コマンドの入出力を表示する画面)に,実行結果(
print関数の出力等)が表示される
- tkinter(Python 標準の GUI ライブラリ)のファイル選択ダイアログを使うプログラムを実行した場合は,ダイアログが開くので対象画像を選択する
- VS Code 下部のターミナルで実行結果を確認する.OpenCV ウィンドウ(OpenCV が画像を表示するために開く専用ウィンドウ)が開いた場合はそちらも確認する.OpenCV ウィンドウは,マウスクリックでウィンドウをアクティブ(操作対象の状態)にしてからキーを押すと終了する
import tkinter as tk
import tkinter.ttk as ttk
import sqlite3
import csv
import os
import re
import shlex
from tkinter import filedialog
conn = None
def set_msg(m, err=False):
tv.pack_forget()
msg.pack(fill=tk.BOTH, expand=True)
msg.config(state=tk.NORMAL, fg="red" if err else "black")
msg.delete("1.0", tk.END)
msg.insert("1.0", m)
msg.config(state=tk.DISABLED)
def show_tv(cur=None, clear=False):
msg.pack_forget()
tv.pack(fill=tk.BOTH, expand=True)
tv.delete(*tv.get_children())
if clear or not cur or not cur.description:
if clear:
tv["columns"] = ()
return
cols = [d[0] for d in cur.description]
tv["columns"] = cols
for c in cols:
tv.heading(c, text=c)
tv.column(c, width=120)
for r in cur.fetchall():
tv.insert("", tk.END, values=[str(v) if v is not None else "NULL" for v in r])
def clear_ui():
t_def.config(state=tk.NORMAL)
t_def.delete("1.0", tk.END)
t_def.config(state=tk.DISABLED)
show_tv(clear=True)
set_msg("")
filter_var.set("")
def _db_connect(p, is_new):
global conn
if not p:
return
p = os.path.abspath(p)
if is_new and os.path.exists(p):
os.remove(p)
if conn:
conn.close()
conn = sqlite3.connect(p, isolation_level="")
status_var.set(f"接続中: {p}")
clear_ui()
upd_list()
set_msg(f"データベースを{'作成・' if is_new else ''}オープンしました:\n{p}")
def _csv_import_logic(p, t_name):
t_name_esc = t_name.replace('"', '""')
with open(p, 'r', encoding='utf-8-sig') as f:
r = list(csv.reader(f))
if not r:
raise ValueError("CSVファイルが空です")
h = r.pop(0)
h_esc = [x.replace('"', '""') for x in h]
cols_def = ", ".join(f'"{x}" TEXT' for x in h_esc)
conn.execute(f'CREATE TABLE IF NOT EXISTS "{t_name_esc}" ({cols_def})')
if r:
conn.executemany(f'INSERT INTO "{t_name_esc}" VALUES ({",".join(["?"]*len(h))})', r)
def load_sql():
p = filedialog.askopenfilename(title="SQL読込", filetypes=[("SQL", "*.sql"), ("すべて", "*.*")])
if not p:
return
te.delete("1.0", tk.END)
te.insert("1.0", open(p, 'r', encoding='utf-8-sig').read())
sync_ui()
set_msg(f"SQLを読み込みました:\n{p}")
def save_sql(e=None):
p = filedialog.asksaveasfilename(title="SQL保存", defaultextension=".sql", filetypes=[("SQL", "*.sql"), ("すべて", "*.*")])
if not p:
return
open(p, 'w', encoding='utf-8-sig').write(te.get("1.0", tk.END))
set_msg(f"SQLを保存しました:\n{p}")
def import_csv():
if not conn:
return set_msg("エラー: 先にDBを作成かオープンしてください。", True)
p = filedialog.askopenfilename(title="CSVインポート", filetypes=[("CSV", "*.csv"), ("すべて", "*.*")])
if not p:
return
t_name = os.path.splitext(os.path.basename(p))[0]
_csv_import_logic(p, t_name)
upd_list()
set_msg(f"CSV読込成功: '{t_name}' に {os.path.basename(p)} を取り込みました。")
def tx(cmd):
if not conn:
return set_msg("エラー: DBが開かれていません", True)
conn.execute(cmd)
set_msg(f"{cmd.strip(';')} を実行しました")
def upd_list(e=None):
lb.delete(0, tk.END)
if not conn:
return
for r in conn.execute("SELECT type, name FROM sqlite_master WHERE type IN ('table', 'index') ORDER BY type DESC, name"):
if filter_var.get().lower() in r[1].lower():
lb.insert(tk.END, f"[{r[0]}] {r[1]}")
def sel_obj(e):
if not lb.curselection() or not conn:
return
typ, name = lb.get(lb.curselection()[0]).split('] ', 1)
name_esc = name.replace('"', '""')
t_def.config(state=tk.NORMAL)
t_def.delete("1.0", tk.END)
r = conn.execute("SELECT sql FROM sqlite_master WHERE name=?", (name,)).fetchone()
t_def.insert("1.0", r[0] if r and r[0] else "-- 定義なし")
highlight_def()
t_def.config(state=tk.DISABLED)
if typ == '[table':
show_tv(conn.execute(f'SELECT * FROM "{name_esc}" LIMIT 1000;'))
else:
set_msg(f"インデックス '{name}' の定義を表示しました。")
def parse_sql(sql):
cmds, cur_s, in_s, q = [], [], False, ''
for line in sql.splitlines():
if not cur_s and not in_s and line.strip().startswith('.'):
cmds.append(line.strip())
continue
for c in line:
cur_s.append(c)
if not in_s and c in "'\"":
in_s, q = True, c
elif in_s and c == q:
in_s = False
elif not in_s and c == ';':
if (st := "".join(cur_s).strip()):
cmds.append(st)
cur_s = []
if cur_s:
cur_s.append('\n')
if (st := "".join(cur_s).strip()):
cmds.append(st)
return cmds
def exec_sql(e=None):
if not conn:
return set_msg("エラー: DBが開かれていません", True)
if not (cmds := parse_sql(te.get("1.0", tk.END))):
return set_msg("エラー: 実行するSQLがありません", True)
cur, last_msg = conn.cursor(), ""
has_select = False
for i, c in enumerate(cmds, 1):
if c.startswith('.'):
p = shlex.split(c)
if p[0].lower() == '.export' and len(p) >= 3:
tbl_esc = p[2].replace('"', '""')
_c = conn.execute(f'SELECT * FROM "{tbl_esc}"')
with open(p[1], 'w', encoding='utf-8-sig', newline='') as f:
csv.writer(f).writerow([d[0] for d in _c.description])
csv.writerows(_c.fetchall())
last_msg = f"エクスポート成功: '{p[2]}' -> '{p[1]}'"
has_select = False
elif p[0].lower() == '.import' and len(p) >= 3:
_csv_import_logic(p[1], p[2])
last_msg = f"インポート成功: '{p[1]}' -> '{p[2]}'"
has_select = False
else:
raise ValueError(f"サポート外または引数不足: {c}")
else:
cur.execute(c)
if cur.description:
show_tv(cur)
last_msg = ""
has_select = True
else:
last_msg = f"実行成功: {cur.rowcount}件に影響"
has_select = False
if not has_select:
if last_msg:
set_msg(f"一括実行完了 (全{len(cmds)}件)\n{last_msg}")
else:
set_msg(f"一括実行完了 (全{len(cmds)}件)")
upd_list()
def exec_explain(e=None):
if not conn:
return set_msg("エラー: DBが開かれていません", True)
if not (cmds := parse_sql(te.get("1.0", tk.END))):
return set_msg("エラー: 解析するSQLがありません", True)
tv.pack_forget()
msg.pack(fill=tk.BOTH, expand=True)
msg.config(state=tk.NORMAL)
msg.delete("1.0", tk.END)
for i, c in enumerate(cmds, 1):
if c.startswith('.'):
continue
clean_c = re.sub(r'^(EXPLAIN\s+(QUERY\s+PLAN\s+)?)', '', c.strip(), flags=re.IGNORECASE).strip()
msg.insert(tk.END, f"=== 【{i}件目のクエリ内部構造解析】 ===\n[対象SQL]: {clean_c}\n\n", "explain_header")
# 1. EXPLAIN QUERY PLAN の実行
cur = conn.cursor()
cur.execute(f"EXPLAIN QUERY PLAN {clean_c}")
cols_qp = [d[0] for d in cur.description]
rows_qp = cur.fetchall()
msg.insert(tk.END, "1. 物理的な操作計画の生データ (EXPLAIN QUERY PLAN)\n", "explain_header")
header_qp = " | ".join(f"{col:<8}" for col in cols_qp)
msg.insert(tk.END, f" {header_qp}\n")
msg.insert(tk.END, " " + "-" * (len(header_qp) + 4) + "\n")
for r in rows_qp:
msg.insert(tk.END, " " + " | ".join(f"{str(v):<8}" for v in r) + "\n")
msg.insert(tk.END, "\n [操作方針の概略]:\n")
for r in rows_qp:
detail = r[-1]
if "SCAN" in detail:
msg.insert(tk.END, f" ・{detail} ⇒ テーブルの全件走査が発生(インデックス未利用)\n", "explain_scan")
elif "SEARCH" in detail:
msg.insert(tk.END, f" ・{detail} ⇒ インデックスを用いた特定行の高速探索を実行\n", "explain_search")
elif "USE TEMP B-TREE" in detail:
msg.insert(tk.END, f" ・{detail} ⇒ ソートやグループ化のための一時B-Treeの動的生成が発生\n", "explain_temp")
msg.insert(tk.END, "\n" + "="*50 + "\n\n")
# 2. EXPLAIN (VDBE命令) の実行と解釈
cur = conn.cursor()
cur.execute(f"EXPLAIN {clean_c}")
cols_vdbe = [d[0] for d in cur.description]
rows_vdbe = cur.fetchall()
msg.insert(tk.END, "2. 仮想機械(VDBE)命令コードおよび各種数値引数 (EXPLAIN)\n", "explain_header")
header_vdbe = " | ".join(f"{col:<8}" for col in cols_vdbe)
msg.insert(tk.END, f" {header_vdbe}\n")
msg.insert(tk.END, " " + "-" * (len(header_vdbe) + 4) + "\n")
for r in rows_vdbe:
msg.insert(tk.END, " " + " | ".join(f"{str(v):<8}" for v in r) + "\n")
msg.insert(tk.END, "\n [出現する数値・レジスタ・列番号の解釈]:\n")
for r in rows_vdbe:
addr = r[0] if len(r) > 0 else None
opcode = str(r[1]).strip() if len(r) > 1 else ""
p1 = r[2] if len(r) > 2 else 0
p2 = r[3] if len(r) > 3 else 0
p3 = r[4] if len(r) > 4 else 0
explanation = ""
if opcode == "Init":
explanation = f"プログラムの初期化。P2が0でなければ【 行アドレス {p2} 】へジャンプします。"
elif opcode == "Transaction":
explanation = f"データベースファイルのトランザクションを開始します(P1={p1}:対象DBのインデックス、P2={p2}:0は読み取り・非0は書き込みトランザクション)。"
elif opcode in ("OpenRead", "OpenWrite"):
explanation = f"カーソル番号 {p1} を割り当ててテーブル/インデックスを開きます(該当オブジェクトのルートページ番号: {p2})。"
elif opcode == "Rewind":
explanation = f"カーソル {p1} を最初のレコードに移動させます。データが空の場合はループをスキップして【 行アドレス {p2} 】へジャンプします。"
elif opcode == "Column":
explanation = f"カーソル {p1} が指す現在の行から【 {p2} 番目の列(列インデックス番号) 】を読み出し、作業用レジスタ {p3} に格納します。"
elif opcode == "ResultRow":
explanation = f"【 レジスタ {p1} をデータの先頭位置 】とする、連続した {p2} 個のデータを取り出し、クエリ結果の1行として外部へ返します。"
elif opcode in ("Next", "Prev"):
explanation = f"カーソル {p1} を次行へ進めます。未走査のデータが残っている場合は走査を継続するため【 行アドレス {p2} 】へジャンプします。"
elif opcode == "Goto":
explanation = f"条件に関わらず、制御の流れを【 行アドレス {p2} 】へ無条件にジャンプさせます。"
elif opcode == "Halt":
explanation = f"仮想機械の実行を終了します(P1={p1}:ステータスコード、0は正常終了を意味します)。"
elif opcode == "MakeRecord":
explanation = f"レジスタ {p1} から連続する {p2} 個の値をデータベースレコード形式(シリアライズ)にまとめ、レジスタ {p3} に格納します。"
elif opcode == "Insert":
explanation = f"カーソル {p1} を通じて、レジスタ {p2} のレコードデータをデータベースのストレージへ挿入します。"
if explanation:
msg.insert(tk.END, f" ・行アドレス {addr:<3} [{opcode:<12}]: {explanation}\n")
msg.insert(tk.END, "\n\n" + "#"*60 + "\n\n")
msg.config(state=tk.DISABLED)
def copy_tree(e=None):
if tv.selection():
rows = []
for i in tv.selection():
rows.append("\t".join(str(x) for x in tv.item(i, 'values')))
root.clipboard_clear()
root.clipboard_append("\n".join(rows))
def export_grid_csv():
if not tv.get_children():
return
p = filedialog.asksaveasfilename(title="CSV出力", defaultextension=".csv", filetypes=[("CSV", "*.csv")])
if not p:
return
with open(p, 'w', encoding='utf-8-sig', newline='') as f:
w = csv.writer(f)
w.writerow(tv["columns"])
for item in tv.get_children():
w.writerow(tv.item(item, "values"))
set_msg(f"グリッドデータを保存しました: {p}")
def sync_ui(e=None):
lines.config(state=tk.NORMAL)
lines.delete("1.0", tk.END)
line_count = int(te.index(tk.END).split('.')[0])
lines.insert("1.0", "\n".join(str(i) for i in range(1, line_count)))
lines.config(state=tk.DISABLED)
lines.yview_moveto(te.yview()[0])
_apply_highlight(te)
def _apply_highlight(w):
content = w.get("1.0", tk.END)
for tag in ["keyword", "string", "comment"]:
w.tag_remove(tag, "1.0", tk.END)
kw = r'\b(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|TABLE|INDEX|FROM|WHERE|INTO|VALUES|SET|AND|OR|NOT|NULL|LIMIT|BEGIN|COMMIT|ROLLBACK|AS|ON|JOIN|INNER|LEFT|RIGHT|OUTER|PRAGMA|IF|EXISTS)\b'
for m in re.finditer(kw, content, re.IGNORECASE):
w.tag_add("keyword", f"1.0+{m.start()}c", f"1.0+{m.end()}c")
for m in re.finditer(r"'.*?'|\".*?\"", content):
w.tag_add("string", f"1.0+{m.start()}c", f"1.0+{m.end()}c")
for m in re.finditer(r"--.*", content):
w.tag_add("comment", f"1.0+{m.start()}c", f"1.0+{m.end()}c")
def highlight_def():
_apply_highlight(t_def)
# === GUI 構築 ===
root = tk.Tk()
root.title("SQLite Manager Tool")
root.geometry("1100x750")
mono_font = ("Consolas", 10) if os.name == 'nt' else ("Menlo", 10)
root.option_add("*Font", ("Meiryo", 10))
style = ttk.Style()
style.configure(".", font=("Meiryo", 10))
style.configure("Treeview", font=mono_font)
style.configure("Treeview.Heading", font=("Meiryo", 10, "bold"))
# ツールバー
tb = tk.Frame(root, bd=1, relief=tk.RAISED, padx=5, pady=5)
tb.pack(side=tk.TOP, fill=tk.X)
tk.Button(
tb, text="新規DB作成", bg="#e0f7fa",
command=lambda: _db_connect(filedialog.asksaveasfilename(title="新規DB作成", defaultextension=".db", filetypes=[("SQLite DB", "*.db *.sqlite"), ("すべて", "*.*")]), True)
).pack(side=tk.LEFT, padx=2)
tk.Button(
tb, text="DBオープン (Ctrl+O)", bg="#e0f7fa",
command=lambda: _db_connect(filedialog.askopenfilename(title="DBオープン", filetypes=[("SQLite DB", "*.db *.sqlite"), ("すべて", "*.*")]), False)
).pack(side=tk.LEFT, padx=2)
buttons_config = [
("CSV読込", import_csv, "#fff9c4"),
("SQL読込", load_sql, None),
("SQL保存 (Ctrl+S)", save_sql, None),
("▶ SQL実行 (F5)", exec_sql, "#c8e6c9"),
("実行計画解析 (F6)", exec_explain, "#e1bee7"),
("コミット", lambda: tx("COMMIT;"), None),
("ロールバック", lambda: tx("ROLLBACK;"), None)
]
for t, c, bg in buttons_config:
if t in ["CSV読込", "SQL読込", "コミット"]:
ttk.Separator(tb, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=8, pady=2)
tk.Button(tb, text=t, command=c, bg=bg).pack(side=tk.LEFT, padx=15 if "▶" in t else 2)
# 最下部ステータスバー領域
status_frame = tk.Frame(root, relief=tk.SUNKEN, bd=1)
status_frame.pack(side=tk.BOTTOM, fill=tk.X)
status_var = tk.StringVar(value="DB未接続")
status_label = tk.Label(status_frame, textvariable=status_var, fg="blue", anchor=tk.W)
status_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
version_label = tk.Label(status_frame, text=f"SQLite: {sqlite3.sqlite_version}", fg="#666666")
version_label.pack(side=tk.RIGHT, padx=5)
# メイン分割構造(水平分割)
pw = ttk.PanedWindow(root, orient=tk.HORIZONTAL)
pw.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 1. スキーマナビゲータ (左ペイン)
lf = tk.Frame(pw)
pw.add(lf, weight=1)
tk.Label(lf, text="スキーマナビゲータ").pack(anchor=tk.W)
filter_var = tk.StringVar()
filter_var.trace_add("write", lambda *a: upd_list())
tk.Entry(lf, textvariable=filter_var).pack(fill=tk.X, pady=(0, 2))
lb = tk.Listbox(lf, width=28, exportselection=False)
lb.pack(fill=tk.BOTH, expand=True)
lb.bind('<>', sel_obj)
# 右側全域のコントロールコンポーネント (rpw)
rpw = ttk.PanedWindow(pw, orient=tk.HORIZONTAL)
pw.add(rpw, weight=4)
# 画面中央ペイン (cpw:垂直分割構造)
cpw = ttk.PanedWindow(rpw, orient=tk.VERTICAL)
rpw.add(cpw, weight=2)
# 2. テーブル定義ビューワ (中央ペイン・上部)
df = tk.Frame(cpw)
cpw.add(df, weight=1)
tk.Label(df, text="テーブル定義ビューワ").pack(anchor=tk.W)
t_def = tk.Text(df, state=tk.DISABLED, font=mono_font)
t_def.pack(fill=tk.BOTH, expand=True)
tags_config = [
("keyword", "blue", (mono_font[0], 10, "bold")),
("string", "green", None),
("comment", "#888888", None)
]
for tag, color, font in tags_config:
t_def.tag_configure(tag, foreground=color, font=font)
# 3. SQLエディタ (中央ペイン・下部)
ef = tk.Frame(cpw)
cpw.add(ef, weight=1)
tk.Label(ef, text="SQLエディタ").pack(anchor=tk.W)
edf = tk.Frame(ef)
edf.pack(fill=tk.BOTH, expand=True)
lines = tk.Text(edf, width=4, padx=4, bg="#f0f0f0", fg="#888888", state=tk.DISABLED, font=mono_font)
lines.pack(side=tk.LEFT, fill=tk.Y)
te = tk.Text(edf, font=mono_font, undo=True)
te.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
esy = ttk.Scrollbar(edf, orient=tk.VERTICAL, command=lambda *a: (te.yview(*a), lines.yview(*a)))
esy.pack(side=tk.RIGHT, fill=tk.Y)
te.config(yscrollcommand=esy.set)
for tag, color, font in tags_config:
te.tag_configure(tag, foreground=color, font=font)
te.bind("", sync_ui)
te.bind("", lambda e: root.after(10, sync_ui))
te.bind("", lambda e: root.after(10, sync_ui))
# 4. 出力(右ペイン:独立した大画面縦長領域)
rf = tk.Frame(rpw)
rpw.add(rf, weight=3)
tk.Label(rf, text="出力 (SQL実行結果、テーブル、エラー、実行計画)").pack(anchor=tk.W)
tv = ttk.Treeview(rf, show="headings")
sy = ttk.Scrollbar(tv, orient=tk.VERTICAL, command=tv.yview)
sx = ttk.Scrollbar(tv, orient=tk.HORIZONTAL, command=tv.xview)
tv.config(yscrollcommand=sy.set, xscrollcommand=sx.set)
sy.pack(side=tk.RIGHT, fill=tk.Y)
sx.pack(side=tk.BOTTOM, fill=tk.X)
msg = tk.Text(rf, state=tk.DISABLED, font=mono_font)
msg.tag_configure("explain_header", foreground="blue", font=(mono_font[0], 10, "bold"))
msg.tag_configure("explain_scan", foreground="#c62828")
msg.tag_configure("explain_search", foreground="#2e7d32")
msg.tag_configure("explain_temp", foreground="#6a1b9a")
msg.tag_configure("explain_error", foreground="#b71c1c", font=(mono_font[0], 10, "bold"))
menu = tk.Menu(root, tearoff=0)
menu.add_command(label="この結果をCSVで保存", command=export_grid_csv)
tv.bind("", lambda e: menu.tk_popup(e.x_root, e.y_root))
root.bind("", copy_tree)
# 全局ショートカットキー
root.bind('', exec_sql)
root.bind('', exec_explain)
root.bind('', save_sql)
root.bind('', lambda e: _db_connect(filedialog.askopenfilename(title="DBオープン", filetypes=[("SQLite DB", "*.db *.sqlite"), ("すべて", "*.*")]), False))
root.mainloop()