OpenCode 实践课:5 分钟用 Python 写出一个命令行 SQLite 查询工具

项目介绍

日常开发中经常需要快速查看 SQLite 数据库——调试本地应用、分析浏览器历史记录、检查爬虫数据,场景非常多。虽然市面上有 DB Browser 等 GUI 工具,但在终端里一条命令搞定显然更快。

今天用 OpenCode 写一个命令行 SQLite 查询工具 sqliteq,支持:

  • 执行任意 SQL 查询,结果以 ASCII 表格展示
  • 导出为 JSON 或 CSV 格式
  • 列出所有表、查看建表语句

技术栈:Python 3,零依赖(仅用标准库 sqlite3argparsejsoncsv),约 90 行代码。

准备工作

python --version       # Python 3.8+
mkdir sqliteq && cd sqliteq
opencode .

准备一个测试数据库:

sqlite3 test.db "CREATE TABLE users(id INTEGER, name TEXT, score REAL);"
sqlite3 test.db "INSERT INTO users VALUES(1,'张三',95.5),(2,'李四',87.0),(3,'王五',92.3);"

实践过程

第一步:搭建命令行框架

我对 OpenCode 说:

> 用 Python 写一个命令行 SQLite 查询工具 sqliteq.py。用 argparse 解析参数:第一个位置参数是数据库路径,-q 指定 SQL 查询语句,-f 指定输出格式(table/csv/json,默认 table)。先搭好框架,sqlite3 连接和查询逻辑用 TODO 注释占位。

OpenCode 生成:

#!/usr/bin/env python3
"""SQLite 命令行查询工具"""
import sys
import sqlite3
import argparse

def main():
    parser = argparse.ArgumentParser(
        description="sqliteq — 命令行 SQLite 查询工具"
    )
    parser.add_argument("db", help="SQLite 数据库文件路径")
    parser.add_argument("-q", "--query", help="要执行的 SQL 查询")
    parser.add_argument(
        "-f", "--format", choices=["table", "csv", "json"],
        default="table", help="输出格式(默认 table)"
    )

    args = parser.parse_args()

    if not args.query:
        print("请用 -q 指定查询语句,例如: python sqliteq.py test.db -q 'SELECT * FROM users'")
        sys.exit(1)

    # TODO: 连接数据库并执行查询
    # TODO: 按指定格式输出结果
    print(f"数据库: {args.db}")
    print(f"查询: {args.query}")

if __name__ == "__main__":
    main()

关键要点: 先让 OpenCode 把骨架搭好——参数解析 + 输入校验。不用一次说清所有细节,第一版能跑起来最重要。choices=["table","csv","json"] 这个参数约束是 OpenCode 自己加的,比手动指定 type=str 更严谨。

第二步:实现查询和表格输出

我对 OpenCode 说:

> 把两行 TODO 换成真正的实现。用 sqlite3 连接数据库,执行 query,拿到列名和数据行。默认 table 格式输出一个 ASCII 表格——像 MySQL 命令行那种带边框的对齐表格。用 sqlite3.Row 让列名可访问。

OpenCode 生成了核心逻辑:

conn = sqlite3.connect(args.db)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()

try:
    cursor.execute(args.query)
    rows = cursor.fetchall()
    headers = [desc[0] for desc in cursor.description]

    if args.format == "table":
        # 计算每列宽度
        col_widths = [len(h) for h in headers]
        for row in rows:
            for i, val in enumerate(row):
                col_widths[i] = max(col_widths[i], len(str(val)))

        sep = "+" + "+".join("-" * (w + 2) for w in col_widths) + "+"
        header_line = "|" + "|".join(
            f" {h:<{col_widths[i]}} " for i, h in enumerate(headers)
        ) + "|"

        lines = [sep, header_line, sep]
        for row in rows:
            line = "|" + "|".join(
                f" {str(v):<{col_widths[i]}} " for i, v in enumerate(row)
            ) + "|"
            lines.append(line)
        lines.append(sep)
        print("\n".join(lines))

except sqlite3.Error as e:
    print(f"SQLite 错误: {e}", file=sys.stderr)
    sys.exit(1)
finally:
    conn.close()

运行效果:

$ python sqliteq.py test.db -q "SELECT * FROM users"
+----+------+-------+
| id | name | score |
+----+------+-------+
| 1  | 张三 | 95.5  |
| 2  | 李四 | 87.0  |
| 3  | 王五 | 92.3  |
+----+------+-------+

关键要点: 表格绘制逻辑约 20 行,OpenCode 用 max() 算出每列最大宽度再拼接边框,和你在网上搜到的"Python ASCII table"教程写法几乎一样。conn.row_factory = sqlite3.Row 让遍历行更干净,这个细节说明 OpenCode 对标准库很熟悉。

第三步:加 JSON/CSV 导出和辅助功能

我对 OpenCode 说:

> 补全 csv 和 json 两种输出格式。另外加两个参数:-l 列出数据库所有表名,-s TABLE_NAME 查看某张表的建表 SQL。-o 参数把结果输出到文件而不是终端。

OpenCode 一次性补全了整个文件(加上前面的代码,完整版见文末)。

JSON 导出部分:

if args.format == "json":
    result = [dict(zip(headers, row)) for row in rows]
    output = json.dumps(result, ensure_ascii=False, indent=2)
elif args.format == "csv":
    buf = StringIO()
    writer = csv.writer(buf)
    if headers:
        writer.writerow(headers)
    writer.writerows(rows)
    output = buf.getvalue()

列出表:

$ python sqliteq.py test.db -l
数据库表:
  - users

查看建表语句:

$ python sqliteq.py test.db -s users
CREATE TABLE users(id INTEGER, name TEXT, score REAL);

导出为 JSON 文件:

$ python sqliteq.py test.db -q "SELECT * FROM users" -f json -o result.json
结果已写入 result.json

关键要点: 这一步需求不少(3 种输出格式 + 2 个辅助命令 + 文件输出),但 OpenCode 能一次处理多个改动。dict(zip(headers, row)) 把 sqlite3.Row 转成字典很 Pythonic,StringIO 做 CSV 缓冲也很合理。这说明只要把需求列清楚,OpenCode 喜欢一次性搞定。

完整代码

#!/usr/bin/env python3
"""SQLite 命令行查询工具"""
import sys
import sqlite3
import argparse
import json
import csv
from io import StringIO


def format_table(rows, headers):
    if not rows:
        return "(空结果)"

    col_widths = [len(h) for h in headers]
    for row in rows:
        for i, val in enumerate(row):
            col_widths[i] = max(col_widths[i], len(str(val)))

    sep = "+" + "+".join("-" * (w + 2) for w in col_widths) + "+"
    header_line = "|" + "|".join(
        f" {h:<{col_widths[i]}} " for i, h in enumerate(headers)
    ) + "|"

    lines = [sep, header_line, sep]
    for row in rows:
        line = "|" + "|".join(
            f" {str(v):<{col_widths[i]}} " for i, v in enumerate(row)
        ) + "|"
        lines.append(line)
    lines.append(sep)
    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(description="sqliteq — 命令行 SQLite 查询工具")
    parser.add_argument("db", help="SQLite 数据库文件路径")
    parser.add_argument("-q", "--query", help="要执行的 SQL 查询")
    parser.add_argument("-l", "--list-tables", action="store_true", help="列出所有表")
    parser.add_argument("-s", "--schema", help="查看指定表的建表语句")
    parser.add_argument("-f", "--format", choices=["table", "csv", "json"],
                        default="table", help="输出格式(默认 table)")
    parser.add_argument("-o", "--output", help="输出到文件(默认输出到终端)")

    args = parser.parse_args()

    conn = sqlite3.connect(args.db)
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()

    try:
        if args.list_tables:
            cursor.execute(
                "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
            )
            tables = [row[0] for row in cursor.fetchall()]
            if tables:
                print("数据库表:")
                for t in tables:
                    print(f"  - {t}")
            else:
                print("数据库中无表")
            return

        if args.schema:
            cursor.execute(
                "SELECT sql FROM sqlite_master WHERE type='table' AND name=?",
                (args.schema,),
            )
            row = cursor.fetchone()
            if row and row[0]:
                print(row[0] + ";")
            else:
                print(f"表 '{args.schema}' 不存在")
            return

        if not args.query:
            print("请用 -q 指定查询,或用 -l 列出表")
            sys.exit(1)

        cursor.execute(args.query)
        rows = cursor.fetchall()
        headers = [desc[0] for desc in cursor.description] if cursor.description else []

        if args.format == "json":
            result = [dict(zip(headers, row)) for row in rows]
            output = json.dumps(result, ensure_ascii=False, indent=2)
        elif args.format == "csv":
            buf = StringIO()
            writer = csv.writer(buf)
            if headers:
                writer.writerow(headers)
            writer.writerows(rows)
            output = buf.getvalue()
        else:
            output = format_table([list(r) for r in rows], headers)

        if args.output:
            with open(args.output, "w", encoding="utf-8") as f:
                f.write(output)
            print(f"结果已写入 {args.output}")
        else:
            print(output)

    except sqlite3.Error as e:
        print(f"SQLite 错误: {e}", file=sys.stderr)
        sys.exit(1)
    finally:
        conn.close()


if __name__ == "__main__":
    main()

小结

整个工具从想法到可用,3 段对话、不到 5 分钟。一次代码都没手写。

| 方式 | 耗时 | 操作 |
|------|------|------|
| 手动编码 | ~20 分钟 | 查 sqlite3 文档、写表格对齐逻辑、调试 CSV/JSON 导出 |
| OpenCode | ~5 分钟 | 3 段对话,逐层迭代 |

体会最深的是迭代开发节奏:搭框架 → 加核心逻辑 → 补辅助功能,每一步都产出可运行的结果。中间表格对齐出了个小问题,直接对 OpenCode 说"列宽没对齐",它立刻修正了 f-string 里的宽度参数。这种即时反馈让人很安心。

如果你也需要频繁和 SQLite 打交道,不妨花几分钟让 OpenCode 给你写个顺手工具。