diff --git a/README.md b/README.md
index b07d6d5..b536cf7 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,7 @@
[](https://github.com/zx06/xsql/actions/workflows/ci.yml?query=branch%3Amain)
[](https://codecov.io/github/zx06/xsql)
+[](https://pkg.go.dev/github.com/zx06/xsql)
[](https://github.com/zx06/xsql/blob/main/go.mod)
[](https://goreportcard.com/report/github.com/zx06/xsql)
[](https://github.com/zx06/xsql/blob/main/LICENSE)
@@ -10,28 +11,30 @@
[](https://www.npmjs.com/package/xsql-cli)
[](https://www.npmjs.com/package/xsql-cli)
-**让 AI 安全地查询你的数据库** 🤖🔒
+**Let AI safely query your databases** 🤖🔒
-xsql 是专为 AI Agent 设计的跨数据库 CLI 工具。默认只读、结构化输出、开箱即用。
+[中文文档](README_zh.md)
+
+xsql is a cross-database CLI tool designed for AI agents. Read-only by default, structured output, ready out of the box.
```bash
-# AI 可以这样查询你的数据库
+# AI can query your database like this
xsql query "SELECT * FROM users WHERE created_at > '2024-01-01'" -p prod -f json
```
-## ✨ 为什么选择 xsql?
+## ✨ Why xsql?
-| 特性 | 说明 |
-|------|------|
-| 🔒 **默认安全** | 双重只读保护,防止 AI 误操作 |
-| 🤖 **AI-first** | JSON 结构化输出,便于 AI 解析 |
-| 🔑 **密钥安全** | 集成 OS Keyring,密码不落盘 |
-| 🌐 **SSH 隧道** | 一行配置连接内网数据库 |
-| 📦 **零依赖** | 单二进制文件,开箱即用 |
+| Feature | Description |
+|---------|-------------|
+| 🔒 **Safe by Default** | Dual-layer read-only protection prevents accidental writes by AI |
+| 🤖 **AI-first** | JSON structured output designed for machine consumption |
+| 🔑 **Secure Credentials** | OS Keyring integration — passwords never touch disk |
+| 🌐 **SSH Tunneling** | One-line config to connect to internal databases |
+| 📦 **Zero Dependencies** | Single binary, works out of the box |
-## 🚀 30 秒上手
+## 🚀 Quick Start
-### 1. 安装
+### 1. Install
```bash
# macOS
@@ -43,10 +46,10 @@ scoop bucket add zx06 https://github.com/zx06/scoop-bucket && scoop install xsql
# npm / npx
npm install -g xsql-cli
-# 或直接下载: https://github.com/zx06/xsql/releases
+# Or download directly: https://github.com/zx06/xsql/releases
```
-### 2. 配置
+### 2. Configure
```bash
mkdir -p ~/.config/xsql
@@ -59,11 +62,11 @@ profiles:
user: root
password: your_password
database: mydb
- allow_plaintext: true # 生产环境建议用 keyring
+ allow_plaintext: true # Use keyring for production
EOF
```
-### 3. 使用
+### 3. Use
```bash
xsql query "SELECT 1" -p dev -f json
@@ -72,58 +75,58 @@ xsql query "SELECT 1" -p dev -f json
---
-## 🤖 让 AI 使用 xsql
+## 🤖 Let AI Use xsql
-### 方式一:Claude Code Plugin(推荐)
+### Option 1: Claude Code Plugin (Recommended)
```bash
-# 1. 添加 marketplace
+# 1. Add marketplace
/plugin marketplace add zx06/xsql
-# 2. 安装插件
+# 2. Install plugin
/plugin install xsql@xsql
```
-安装后 Claude 自动获得 xsql 技能,可直接查询数据库。
+After installation, Claude automatically gains xsql skills and can query databases directly.
-### 方式二:复制 Skill 给任意 AI
+### Option 2: Copy Skill Prompt to Any AI
-将以下内容发送给你的 AI 助手(ChatGPT/Claude/Cursor 等):
+Send the following to your AI assistant (ChatGPT/Claude/Cursor, etc.):
-📋 点击展开 AI Skill Prompt(复制即用)
+📋 Click to expand AI Skill Prompt (copy & paste)
```
-你现在可以使用 xsql 工具查询数据库。
+You can now use the xsql tool to query databases.
-## 基本用法
+## Basic Usage
xsql query "" -p -f json
-## 可用命令
-- xsql query "SQL" -p -f json # 执行查询
-- xsql schema dump -p -f json # 导出数据库结构
-- xsql profile list -f json # 列出所有 profile
-- xsql profile show -f json # 查看 profile 详情
+## Available Commands
+- xsql query "SQL" -p -f json # Execute query
+- xsql schema dump -p -f json # Export database schema
+- xsql profile list -f json # List all profiles
+- xsql profile show -f json # Show profile details
-## 输出格式
-成功: {"ok":true,"schema_version":1,"data":{"columns":[...],"rows":[...]}}
-失败: {"ok":false,"schema_version":1,"error":{"code":"XSQL_...","message":"..."}}
+## Output Format
+Success: {"ok":true,"schema_version":1,"data":{"columns":[...],"rows":[...]}}
+Failure: {"ok":false,"schema_version":1,"error":{"code":"XSQL_...","message":"..."}}
-## 重要规则
-1. 默认只读模式,无法执行 INSERT/UPDATE/DELETE
-2. 始终使用 -f json 获取结构化输出
-3. 先用 profile list 查看可用的数据库配置
-4. 检查 ok 字段判断执行是否成功
+## Important Rules
+1. Read-only mode by default — cannot execute INSERT/UPDATE/DELETE
+2. Always use -f json for structured output
+3. Use profile list to see available database configurations
+4. Check the ok field to determine execution success
-## 退出码
-0=成功, 2=配置错误, 3=连接错误, 4=只读拦截, 5=SQL执行错误
+## Exit Codes
+0=success, 2=config error, 3=connection error, 4=read-only violation, 5=SQL execution error
```
-### 方式三:MCP Server(Claude Desktop 等)
+### Option 3: MCP Server (Claude Desktop, etc.)
-在 Claude Desktop 配置中添加 xsql MCP server:
+Add the xsql MCP server to your Claude Desktop configuration:
```json
{
@@ -136,47 +139,47 @@ xsql query "" -p -f json
}
```
-启动后,Claude 可以直接通过 MCP 协议查询数据库。
+Once started, Claude can query databases directly via the MCP protocol.
-### 方式四:AGENTS.md / Rules(Cursor/Windsurf)
+### Option 4: AGENTS.md / Rules (Cursor/Windsurf)
-在项目根目录创建 `.cursor/rules` 或编辑 `AGENTS.md`:
+Create `.cursor/rules` or edit `AGENTS.md` in your project root:
```markdown
-## 数据库查询
+## Database Queries
-使用 xsql 工具查询数据库:
-- 查询: `xsql query "SELECT ..." -p -f json`
-- 导出结构: `xsql schema dump -p -f json`
-- 列出配置: `xsql profile list -f json`
+Use xsql to query databases:
+- Query: `xsql query "SELECT ..." -p -f json`
+- Export schema: `xsql schema dump -p -f json`
+- List profiles: `xsql profile list -f json`
-注意: 默认只读模式,写操作需要 --unsafe-allow-write 标志。
+Note: Read-only mode by default. Write operations require the --unsafe-allow-write flag.
```
---
-## 📖 功能详情
+## 📖 Features
-### 命令一览
+### Command Reference
-| 命令 | 说明 |
-|------|------|
-| `xsql query ` | 执行 SQL 查询(默认只读) |
-| `xsql schema dump` | 导出数据库结构(表、列、索引、外键) |
-| `xsql profile list` | 列出所有 profile |
-| `xsql profile show ` | 查看 profile 详情(密码脱敏) |
-| `xsql mcp server` | 启动 MCP Server(AI 助手集成) |
-| `xsql spec` | 导出 AI Tool Spec(支持 `--format yaml`) |
-| `xsql version` | 显示版本信息 |
+| Command | Description |
+|---------|-------------|
+| `xsql query ` | Execute SQL queries (read-only by default) |
+| `xsql schema dump` | Export database schema (tables, columns, indexes, foreign keys) |
+| `xsql profile list` | List all profiles |
+| `xsql profile show ` | Show profile details (passwords are masked) |
+| `xsql mcp server` | Start MCP Server (AI assistant integration) |
+| `xsql spec` | Export AI Tool Spec (supports `--format yaml`) |
+| `xsql version` | Show version information |
-### 输出格式
+### Output Formats
```bash
-# JSON(AI/程序)
+# JSON (for AI/programs)
xsql query "SELECT id, name FROM users" -p dev -f json
{"ok":true,"schema_version":1,"data":{"columns":["id","name"],"rows":[{"id":1,"name":"Alice"}]}}
-# Table(终端)
+# Table (for terminals)
xsql query "SELECT id, name FROM users" -p dev -f table
id name
-- -----
@@ -185,16 +188,16 @@ id name
(1 rows)
```
-### Schema 发现(AI 自动理解数据库)
+### Schema Discovery (AI auto-understands your database)
```bash
-# 导出数据库结构(供 AI 理解表结构)
+# Export database schema (for AI to understand table structures)
xsql schema dump -p dev -f json
-# 过滤特定表
+# Filter specific tables
xsql schema dump -p dev --table "user*" -f json
-# 输出示例
+# Example output
{
"ok": true,
"data": {
@@ -212,7 +215,7 @@ xsql schema dump -p dev --table "user*" -f json
}
```
-### SSH 隧道连接
+### SSH Tunnel Connection
```yaml
ssh_proxies:
@@ -224,48 +227,48 @@ ssh_proxies:
profiles:
prod:
db: pg
- host: db.internal # 内网地址
+ host: db.internal # Internal network address
port: 5432
user: readonly
password: "keyring:prod/password"
database: mydb
- ssh_proxy: bastion # 引用 SSH 代理
+ ssh_proxy: bastion # Reference SSH proxy
```
-### 端口转发代理(xsql proxy)
+### Port Forwarding Proxy (xsql proxy)
-当需要传统的 `ssh -L` 行为或希望暴露本地端口给 GUI 客户端时,可以使用 `xsql proxy`:
+When you need traditional `ssh -L` behavior or want to expose a local port for GUI clients, use `xsql proxy`:
```bash
-# 启动端口转发(自动分配本地端口)
+# Start port forwarding (auto-assign local port)
xsql proxy -p prod
-# 指定本地端口
+# Specify local port
xsql proxy -p prod --local-port 13306
```
-> 该命令要求 profile 配置 `ssh_proxy`,并会在本地监听端口,将流量转发到目标数据库。
+> This command requires the profile to have `ssh_proxy` configured. It listens on a local port and forwards traffic to the target database.
-### 安全特性
+### Security Features
-- **双重只读保护**:SQL 静态分析 + 数据库事务级只读
-- **Keyring 集成**:`password: "keyring:prod/password"`
-- **密码脱敏**:`profile show` 不泄露密码
-- **SSH 安全**:默认验证 known_hosts
+- **Dual-layer Read-only Protection**: SQL static analysis + database transaction-level read-only
+- **Keyring Integration**: `password: "keyring:prod/password"`
+- **Password Masking**: `profile show` never exposes passwords
+- **SSH Security**: known_hosts verification enabled by default
---
-## 📚 文档
-
-| 文档 | 说明 |
-|------|------|
-| [CLI 规范](docs/cli-spec.md) | 命令行接口详细说明 |
-| [配置指南](docs/config.md) | 配置文件格式和选项 |
-| [SSH 代理](docs/ssh-proxy.md) | SSH 隧道配置 |
-| [错误处理](docs/error-contract.md) | 错误码和退出码 |
-| [AI 集成](docs/ai.md) | MCP Server 和 AI 助手集成 |
-| [RFC 文档](docs/rfcs/) | 设计变更记录 |
-| [开发指南](docs/dev.md) | 贡献和开发说明 |
+## 📚 Documentation
+
+| Document | Description |
+|----------|-------------|
+| [CLI Specification](docs/cli-spec.md) | Detailed CLI interface reference |
+| [Configuration Guide](docs/config.md) | Config file format and options |
+| [SSH Proxy](docs/ssh-proxy.md) | SSH tunnel configuration |
+| [Error Handling](docs/error-contract.md) | Error codes and exit codes |
+| [AI Integration](docs/ai.md) | MCP Server and AI assistant integration |
+| [RFC Documents](docs/rfcs/) | Design change records |
+| [Development Guide](docs/dev.md) | Contributing and development notes |
---
diff --git a/README_zh.md b/README_zh.md
new file mode 100644
index 0000000..7901a61
--- /dev/null
+++ b/README_zh.md
@@ -0,0 +1,277 @@
+# xsql
+
+[](https://github.com/zx06/xsql/actions/workflows/ci.yml?query=branch%3Amain)
+[](https://codecov.io/github/zx06/xsql)
+[](https://pkg.go.dev/github.com/zx06/xsql)
+[](https://github.com/zx06/xsql/blob/main/go.mod)
+[](https://goreportcard.com/report/github.com/zx06/xsql)
+[](https://github.com/zx06/xsql/blob/main/LICENSE)
+[](https://github.com/zx06/xsql/releases/latest)
+[](https://github.com/zx06/xsql/releases)
+[](https://www.npmjs.com/package/xsql-cli)
+[](https://www.npmjs.com/package/xsql-cli)
+
+**让 AI 安全地查询你的数据库** 🤖🔒
+
+[English](README.md)
+
+xsql 是专为 AI Agent 设计的跨数据库 CLI 工具。默认只读、结构化输出、开箱即用。
+
+```bash
+# AI 可以这样查询你的数据库
+xsql query "SELECT * FROM users WHERE created_at > '2024-01-01'" -p prod -f json
+```
+
+## ✨ 为什么选择 xsql?
+
+| 特性 | 说明 |
+|------|------|
+| 🔒 **默认安全** | 双重只读保护,防止 AI 误操作 |
+| 🤖 **AI-first** | JSON 结构化输出,便于 AI 解析 |
+| 🔑 **密钥安全** | 集成 OS Keyring,密码不落盘 |
+| 🌐 **SSH 隧道** | 一行配置连接内网数据库 |
+| 📦 **零依赖** | 单二进制文件,开箱即用 |
+
+## 🚀 30 秒上手
+
+### 1. 安装
+
+```bash
+# macOS
+brew install zx06/tap/xsql
+
+# Windows
+scoop bucket add zx06 https://github.com/zx06/scoop-bucket && scoop install xsql
+
+# npm / npx
+npm install -g xsql-cli
+
+# 或直接下载: https://github.com/zx06/xsql/releases
+```
+
+### 2. 配置
+
+```bash
+mkdir -p ~/.config/xsql
+cat > ~/.config/xsql/xsql.yaml << 'EOF'
+profiles:
+ dev:
+ db: mysql
+ host: 127.0.0.1
+ port: 3306
+ user: root
+ password: your_password
+ database: mydb
+ allow_plaintext: true # 生产环境建议用 keyring
+EOF
+```
+
+### 3. 使用
+
+```bash
+xsql query "SELECT 1" -p dev -f json
+# {"ok":true,"schema_version":1,"data":{"columns":["1"],"rows":[{"1":1}]}}
+```
+
+---
+
+## 🤖 让 AI 使用 xsql
+
+### 方式一:Claude Code Plugin(推荐)
+
+```bash
+# 1. 添加 marketplace
+/plugin marketplace add zx06/xsql
+
+# 2. 安装插件
+/plugin install xsql@xsql
+```
+
+安装后 Claude 自动获得 xsql 技能,可直接查询数据库。
+
+### 方式二:复制 Skill 给任意 AI
+
+将以下内容发送给你的 AI 助手(ChatGPT/Claude/Cursor 等):
+
+
+📋 点击展开 AI Skill Prompt(复制即用)
+
+```
+你现在可以使用 xsql 工具查询数据库。
+
+## 基本用法
+xsql query "" -p -f json
+
+## 可用命令
+- xsql query "SQL" -p -f json # 执行查询
+- xsql schema dump -p -f json # 导出数据库结构
+- xsql profile list -f json # 列出所有 profile
+- xsql profile show -f json # 查看 profile 详情
+
+## 输出格式
+成功: {"ok":true,"schema_version":1,"data":{"columns":[...],"rows":[...]}}
+失败: {"ok":false,"schema_version":1,"error":{"code":"XSQL_...","message":"..."}}
+
+## 重要规则
+1. 默认只读模式,无法执行 INSERT/UPDATE/DELETE
+2. 始终使用 -f json 获取结构化输出
+3. 先用 profile list 查看可用的数据库配置
+4. 检查 ok 字段判断执行是否成功
+
+## 退出码
+0=成功, 2=配置错误, 3=连接错误, 4=只读拦截, 5=SQL执行错误
+```
+
+
+
+### 方式三:MCP Server(Claude Desktop 等)
+
+在 Claude Desktop 配置中添加 xsql MCP server:
+
+```json
+{
+ "mcpServers": {
+ "xsql": {
+ "command": "xsql",
+ "args": ["mcp", "server", "--config", "/path/to/xsql.yaml"]
+ }
+ }
+}
+```
+
+启动后,Claude 可以直接通过 MCP 协议查询数据库。
+
+### 方式四:AGENTS.md / Rules(Cursor/Windsurf)
+
+在项目根目录创建 `.cursor/rules` 或编辑 `AGENTS.md`:
+
+```markdown
+## 数据库查询
+
+使用 xsql 工具查询数据库:
+- 查询: `xsql query "SELECT ..." -p -f json`
+- 导出结构: `xsql schema dump -p -f json`
+- 列出配置: `xsql profile list -f json`
+
+注意: 默认只读模式,写操作需要 --unsafe-allow-write 标志。
+```
+
+---
+
+## 📖 功能详情
+
+### 命令一览
+
+| 命令 | 说明 |
+|------|------|
+| `xsql query ` | 执行 SQL 查询(默认只读) |
+| `xsql schema dump` | 导出数据库结构(表、列、索引、外键) |
+| `xsql profile list` | 列出所有 profile |
+| `xsql profile show ` | 查看 profile 详情(密码脱敏) |
+| `xsql mcp server` | 启动 MCP Server(AI 助手集成) |
+| `xsql spec` | 导出 AI Tool Spec(支持 `--format yaml`) |
+| `xsql version` | 显示版本信息 |
+
+### 输出格式
+
+```bash
+# JSON(AI/程序)
+xsql query "SELECT id, name FROM users" -p dev -f json
+{"ok":true,"schema_version":1,"data":{"columns":["id","name"],"rows":[{"id":1,"name":"Alice"}]}}
+
+# Table(终端)
+xsql query "SELECT id, name FROM users" -p dev -f table
+id name
+-- -----
+1 Alice
+
+(1 rows)
+```
+
+### Schema 发现(AI 自动理解数据库)
+
+```bash
+# 导出数据库结构(供 AI 理解表结构)
+xsql schema dump -p dev -f json
+
+# 过滤特定表
+xsql schema dump -p dev --table "user*" -f json
+
+# 输出示例
+{
+ "ok": true,
+ "data": {
+ "database": "mydb",
+ "tables": [
+ {
+ "name": "users",
+ "columns": [
+ {"name": "id", "type": "bigint", "primary_key": true},
+ {"name": "email", "type": "varchar(255)", "nullable": false}
+ ]
+ }
+ ]
+ }
+}
+```
+
+### SSH 隧道连接
+
+```yaml
+ssh_proxies:
+ bastion:
+ host: jump.example.com
+ user: admin
+ identity_file: ~/.ssh/id_ed25519
+
+profiles:
+ prod:
+ db: pg
+ host: db.internal # 内网地址
+ port: 5432
+ user: readonly
+ password: "keyring:prod/password"
+ database: mydb
+ ssh_proxy: bastion # 引用 SSH 代理
+```
+
+### 端口转发代理(xsql proxy)
+
+当需要传统的 `ssh -L` 行为或希望暴露本地端口给 GUI 客户端时,可以使用 `xsql proxy`:
+
+```bash
+# 启动端口转发(自动分配本地端口)
+xsql proxy -p prod
+
+# 指定本地端口
+xsql proxy -p prod --local-port 13306
+```
+
+> 该命令要求 profile 配置 `ssh_proxy`,并会在本地监听端口,将流量转发到目标数据库。
+
+### 安全特性
+
+- **双重只读保护**:SQL 静态分析 + 数据库事务级只读
+- **Keyring 集成**:`password: "keyring:prod/password"`
+- **密码脱敏**:`profile show` 不泄露密码
+- **SSH 安全**:默认验证 known_hosts
+
+---
+
+## 📚 文档
+
+| 文档 | 说明 |
+|------|------|
+| [CLI 规范](docs/cli-spec.md) | 命令行接口详细说明 |
+| [配置指南](docs/config.md) | 配置文件格式和选项 |
+| [SSH 代理](docs/ssh-proxy.md) | SSH 隧道配置 |
+| [错误处理](docs/error-contract.md) | 错误码和退出码 |
+| [AI 集成](docs/ai.md) | MCP Server 和 AI 助手集成 |
+| [RFC 文档](docs/rfcs/) | 设计变更记录 |
+| [开发指南](docs/dev.md) | 贡献和开发说明 |
+
+---
+
+## License
+
+[MIT](LICENSE)
diff --git a/cmd/xsql/doc.go b/cmd/xsql/doc.go
new file mode 100644
index 0000000..32bc436
--- /dev/null
+++ b/cmd/xsql/doc.go
@@ -0,0 +1,3 @@
+// Command xsql is an AI-first cross-database CLI tool with default read-only
+// protection, structured output, and SSH tunnel support.
+package main
diff --git a/internal/config/load.go b/internal/config/load.go
index 344266d..e7a3dbf 100644
--- a/internal/config/load.go
+++ b/internal/config/load.go
@@ -41,7 +41,7 @@ func readFile(path string) (File, *errors.XError) {
return f, nil
}
-// LoadConfig 加载配置文件,返回完整配置和配置文件路径。
+// LoadConfig loads the configuration file and returns the full config along with its path.
func LoadConfig(opts Options) (File, string, *errors.XError) {
workDir := opts.WorkDir
if workDir == "" {
diff --git a/internal/config/resolve.go b/internal/config/resolve.go
index 3bb0d3b..b9e4f1e 100644
--- a/internal/config/resolve.go
+++ b/internal/config/resolve.go
@@ -7,7 +7,7 @@ import (
"github.com/zx06/xsql/internal/errors"
)
-// Resolve 实现第一阶段 config/profile/format 合并:CLI > ENV > Config。
+// Resolve performs phase-1 config/profile/format merging: CLI > ENV > Config.
func Resolve(opts Options) (Resolved, *errors.XError) {
workDir := opts.WorkDir
if workDir == "" {
@@ -20,7 +20,7 @@ func Resolve(opts Options) (Resolved, *errors.XError) {
}
}
- // 1) 读取配置文件(如有)
+ // 1) Read config file (if any)
var cfg File
var cfgPath string
if opts.ConfigPath != "" {
@@ -49,7 +49,7 @@ func Resolve(opts Options) (Resolved, *errors.XError) {
}
}
- // 2) 选择 profile:--profile > XSQL_PROFILE > profiles.default > 空
+ // 2) Select profile: --profile > XSQL_PROFILE > profiles.default > empty
profile := ""
if opts.CLIProfileSet {
profile = opts.CLIProfile
@@ -61,7 +61,7 @@ func Resolve(opts Options) (Resolved, *errors.XError) {
}
}
- // 3) 获取完整 profile
+ // 3) Retrieve full profile
var selectedProfile Profile
if profile != "" {
p, ok := cfg.Profiles[profile]
@@ -70,7 +70,7 @@ func Resolve(opts Options) (Resolved, *errors.XError) {
map[string]any{"profile": profile})
}
selectedProfile = p
- // 解析 ssh_proxy 引用
+ // Resolve ssh_proxy reference
if selectedProfile.SSHProxy != "" {
if proxy, ok := cfg.SSHProxies[selectedProfile.SSHProxy]; ok {
selectedProfile.SSHConfig = &proxy
@@ -79,7 +79,7 @@ func Resolve(opts Options) (Resolved, *errors.XError) {
map[string]any{"profile": profile, "ssh_proxy": selectedProfile.SSHProxy})
}
}
- // 设置默认端口
+ // Set default port
if selectedProfile.Port == 0 {
switch selectedProfile.DB {
case "mysql":
@@ -90,7 +90,7 @@ func Resolve(opts Options) (Resolved, *errors.XError) {
}
}
- // 4) 合并 format:--format > XSQL_FORMAT > profile.format > auto
+ // 4) Merge format: --format > XSQL_FORMAT > profile.format > auto
format := "auto"
if selectedProfile.Format != "" {
format = selectedProfile.Format
diff --git a/internal/config/types.go b/internal/config/types.go
index e7cb059..6eb1e0a 100644
--- a/internal/config/types.go
+++ b/internal/config/types.go
@@ -1,77 +1,77 @@
package config
-// File 表示 xsql.yaml 的配置结构。
-// 约束:配置优先级为 CLI > ENV > Config。
+// File represents the xsql.yaml configuration structure.
+// Constraint: config priority is CLI > ENV > Config.
type File struct {
SSHProxies map[string]SSHProxy `yaml:"ssh_proxies"`
Profiles map[string]Profile `yaml:"profiles"`
MCP MCPConfig `yaml:"mcp"`
}
-// SSHProxy 定义可复用的 SSH 代理配置。
+// SSHProxy defines a reusable SSH proxy configuration.
type SSHProxy struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
IdentityFile string `yaml:"identity_file"`
- Passphrase string `yaml:"passphrase"` // 支持 keyring:xxx 引用
+ Passphrase string `yaml:"passphrase"` // supports keyring:xxx reference
KnownHostsFile string `yaml:"known_hosts_file"`
- SkipHostKey bool `yaml:"skip_host_key"` // 极不推荐
+ SkipHostKey bool `yaml:"skip_host_key"` // strongly discouraged
}
type Profile struct {
- Description string `yaml:"description"` // 描述,用于区分不同数据库
+ Description string `yaml:"description"` // description to distinguish databases
Format string `yaml:"format"`
- // DB 连接
+ // DB connection
DB string `yaml:"db"` // mysql | pg
- DSN string `yaml:"dsn"` // 原生 DSN(优先)
+ DSN string `yaml:"dsn"` // raw DSN (takes precedence)
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
- Password string `yaml:"password"` // 支持 keyring:xxx 引用
+ Password string `yaml:"password"` // supports keyring:xxx reference
Database string `yaml:"database"`
- // 安全选项
- AllowPlaintext bool `yaml:"allow_plaintext"` // 允许明文密码
- UnsafeAllowWrite bool `yaml:"unsafe_allow_write"` // 允许写操作(绕过只读保护)
+ // Security options
+ AllowPlaintext bool `yaml:"allow_plaintext"` // allow plaintext password
+ UnsafeAllowWrite bool `yaml:"unsafe_allow_write"` // allow write operations (bypass read-only protection)
- // 超时配置(秒)
- QueryTimeout int `yaml:"query_timeout"` // 查询超时,默认 30 秒
- SchemaTimeout int `yaml:"schema_timeout"` // Schema 导出超时,默认 60 秒
+ // Timeout settings (seconds)
+ QueryTimeout int `yaml:"query_timeout"` // query timeout, default 30s
+ SchemaTimeout int `yaml:"schema_timeout"` // schema export timeout, default 60s
- // SSH proxy 引用(引用 ssh_proxies 中定义的名称)
+ // SSH proxy reference (refers to a name defined in ssh_proxies)
SSHProxy string `yaml:"ssh_proxy"`
- // Proxy 本地端口(用于 xsql proxy 命令)
+ // Local port for the proxy (used by xsql proxy command)
LocalPort int `yaml:"local_port"`
- // 解析后的 SSH 配置(由 Resolve 填充,不从 YAML 读取)
+ // Resolved SSH config (populated by Resolve, not read from YAML)
SSHConfig *SSHProxy `yaml:"-"`
}
-// MCPConfig 定义 MCP server 配置。
+// MCPConfig defines the MCP server configuration.
type MCPConfig struct {
Transport string `yaml:"transport"` // stdio | streamable_http
HTTP MCPHTTPConfig `yaml:"http"`
}
-// MCPHTTPConfig 定义 MCP Streamable HTTP 传输配置。
+// MCPHTTPConfig defines the MCP Streamable HTTP transport configuration.
type MCPHTTPConfig struct {
Addr string `yaml:"addr"`
- AuthToken string `yaml:"auth_token"` // 支持 keyring:xxx 引用
- AllowPlaintextToken bool `yaml:"allow_plaintext_token"` // 允许明文 token
+ AuthToken string `yaml:"auth_token"` // supports keyring:xxx reference
+ AllowPlaintextToken bool `yaml:"allow_plaintext_token"` // allow plaintext token
}
type Resolved struct {
ConfigPath string
ProfileName string
Format string
- Profile Profile // 完整 profile 供 query 使用
+ Profile Profile // full profile for query use
}
type Options struct {
- // ConfigPath: 若非空,则只读取该文件(不存在报错)。
+ // ConfigPath: if non-empty, only this file is read (error if not found).
ConfigPath string
// CLI
@@ -80,14 +80,14 @@ type Options struct {
CLIFormat string
CLIFormatSet bool
- // ENV(由调用方注入,便于测试)
+ // ENV (injected by caller for testability)
EnvProfile string
EnvFormat string
- // HomeDir 用于默认路径计算(为空则自动探测)。
+ // HomeDir is used for default path resolution (auto-detected if empty).
HomeDir string
- // WorkDir 用于默认路径(为空则使用进程当前工作目录)。
+ // WorkDir is used for default paths (falls back to process cwd if empty).
WorkDir string
}
diff --git a/internal/db/mysql/driver.go b/internal/db/mysql/driver.go
index 2d327d3..43ce78a 100644
--- a/internal/db/mysql/driver.go
+++ b/internal/db/mysql/driver.go
@@ -1,3 +1,4 @@
+// Package mysql implements the MySQL database driver.
package mysql
import (
diff --git a/internal/db/mysql/schema.go b/internal/db/mysql/schema.go
index 990b911..c3e2f42 100644
--- a/internal/db/mysql/schema.go
+++ b/internal/db/mysql/schema.go
@@ -9,40 +9,40 @@ import (
"github.com/zx06/xsql/internal/errors"
)
-// DumpSchema 导出 MySQL 数据库结构
+// DumpSchema exports the MySQL database schema.
func (d *Driver) DumpSchema(ctx context.Context, conn *sql.DB, opts db.SchemaOptions) (*db.SchemaInfo, *errors.XError) {
info := &db.SchemaInfo{}
- // 获取当前数据库名
+ // Get the current database name
var database string
if err := conn.QueryRowContext(ctx, "SELECT DATABASE()").Scan(&database); err != nil {
return nil, errors.Wrap(errors.CodeDBExecFailed, "failed to get database name", nil, err)
}
info.Database = database
- // 获取表列表
+ // Get table list
tables, xe := d.listTables(ctx, conn, database, opts)
if xe != nil {
return nil, xe
}
- // 获取每个表的详细信息
+ // Get detailed information for each table
for _, table := range tables {
- // 获取列信息
+ // Get column information
columns, xe := d.getColumns(ctx, conn, database, table.Name)
if xe != nil {
return nil, xe
}
table.Columns = columns
- // 获取索引信息
+ // Get index information
indexes, xe := d.getIndexes(ctx, conn, database, table.Name)
if xe != nil {
return nil, xe
}
table.Indexes = indexes
- // 获取外键信息
+ // Get foreign key information
fks, xe := d.getForeignKeys(ctx, conn, database, table.Name)
if xe != nil {
return nil, xe
@@ -55,7 +55,7 @@ func (d *Driver) DumpSchema(ctx context.Context, conn *sql.DB, opts db.SchemaOpt
return info, nil
}
-// listTables 获取表列表
+// listTables retrieves the list of tables.
func (d *Driver) listTables(ctx context.Context, conn *sql.DB, database string, opts db.SchemaOptions) ([]db.Table, *errors.XError) {
query := `
SELECT table_name, table_comment
@@ -64,9 +64,9 @@ func (d *Driver) listTables(ctx context.Context, conn *sql.DB, database string,
`
args := []any{database}
- // 表名过滤
+ // Table name filter
if opts.TablePattern != "" {
- // 将通配符 * 和 ? 转换为 SQL LIKE 模式
+ // Convert wildcards * and ? to SQL LIKE patterns
likePattern := strings.ReplaceAll(opts.TablePattern, "*", "%")
likePattern = strings.ReplaceAll(likePattern, "?", "_")
query += " AND table_name LIKE ?"
@@ -101,7 +101,7 @@ func (d *Driver) listTables(ctx context.Context, conn *sql.DB, database string,
return tables, nil
}
-// getColumns 获取表的列信息
+// getColumns retrieves column information for a table.
func (d *Driver) getColumns(ctx context.Context, conn *sql.DB, database, tableName string) ([]db.Column, *errors.XError) {
query := `
SELECT
@@ -152,7 +152,7 @@ func (d *Driver) getColumns(ctx context.Context, conn *sql.DB, database, tableNa
return columns, nil
}
-// getIndexes 获取表的索引信息
+// getIndexes retrieves index information for a table.
func (d *Driver) getIndexes(ctx context.Context, conn *sql.DB, database, tableName string) ([]db.Index, *errors.XError) {
query := `
SELECT
@@ -172,7 +172,7 @@ func (d *Driver) getIndexes(ctx context.Context, conn *sql.DB, database, tableNa
}
defer rows.Close()
- // 按 index_name 分组
+ // Group by index_name
indexMap := make(map[string]*db.Index)
for rows.Next() {
var indexName, columnName string
@@ -198,7 +198,7 @@ func (d *Driver) getIndexes(ctx context.Context, conn *sql.DB, database, tableNa
return nil, errors.Wrap(errors.CodeDBExecFailed, "rows iteration error", nil, err)
}
- // 转换为切片
+ // Convert to slice
indexes := make([]db.Index, 0, len(indexMap))
for _, idx := range indexMap {
indexes = append(indexes, *idx)
@@ -207,7 +207,7 @@ func (d *Driver) getIndexes(ctx context.Context, conn *sql.DB, database, tableNa
return indexes, nil
}
-// getForeignKeys 获取表的外键信息
+// getForeignKeys retrieves foreign key information for a table.
func (d *Driver) getForeignKeys(ctx context.Context, conn *sql.DB, database, tableName string) ([]db.ForeignKey, *errors.XError) {
query := `
SELECT
@@ -229,7 +229,7 @@ func (d *Driver) getForeignKeys(ctx context.Context, conn *sql.DB, database, tab
}
defer rows.Close()
- // 按 constraint_name 分组
+ // Group by constraint_name
fkMap := make(map[string]*db.ForeignKey)
for rows.Next() {
var constraintName, columnName, refTable, refColumn string
@@ -255,7 +255,7 @@ func (d *Driver) getForeignKeys(ctx context.Context, conn *sql.DB, database, tab
return nil, errors.Wrap(errors.CodeDBExecFailed, "rows iteration error", nil, err)
}
- // 转换为切片
+ // Convert to slice
fks := make([]db.ForeignKey, 0, len(fkMap))
for _, fk := range fkMap {
fks = append(fks, *fk)
diff --git a/internal/db/pg/driver.go b/internal/db/pg/driver.go
index a960a92..763a7a4 100644
--- a/internal/db/pg/driver.go
+++ b/internal/db/pg/driver.go
@@ -1,3 +1,4 @@
+// Package pg implements the PostgreSQL database driver.
package pg
import (
@@ -27,7 +28,7 @@ func (d *Driver) Open(ctx context.Context, opts db.ConnOptions) (*sql.DB, *error
dsn = buildDSN(opts)
}
- // 使用 pgx 自定义 dialer
+ // Use pgx custom dialer
if opts.Dialer != nil {
config, err := pgx.ParseConfig(dsn)
if err != nil {
diff --git a/internal/db/pg/schema.go b/internal/db/pg/schema.go
index 5f01f3c..e9197b5 100644
--- a/internal/db/pg/schema.go
+++ b/internal/db/pg/schema.go
@@ -9,47 +9,47 @@ import (
"github.com/zx06/xsql/internal/errors"
)
-// DumpSchema 导出 PostgreSQL 数据库结构
+// DumpSchema exports the PostgreSQL database schema.
func (d *Driver) DumpSchema(ctx context.Context, conn *sql.DB, opts db.SchemaOptions) (*db.SchemaInfo, *errors.XError) {
info := &db.SchemaInfo{}
- // 获取当前数据库名
+ // Get the current database name
var database string
if err := conn.QueryRowContext(ctx, "SELECT current_database()").Scan(&database); err != nil {
return nil, errors.Wrap(errors.CodeDBExecFailed, "failed to get database name", nil, err)
}
info.Database = database
- // 获取 schema 列表(排除系统 schema)
+ // Get schema list (excluding system schemas)
schemas, xe := d.listSchemas(ctx, conn, opts)
if xe != nil {
return nil, xe
}
- // 获取每个 schema 下的表
+ // Get tables under each schema
for _, schema := range schemas {
tables, xe := d.listTables(ctx, conn, schema, opts)
if xe != nil {
return nil, xe
}
- // 获取每个表的详细信息
+ // Get detailed information for each table
for _, table := range tables {
- // 获取列信息
+ // Get column information
columns, xe := d.getColumns(ctx, conn, schema, table.Name)
if xe != nil {
return nil, xe
}
table.Columns = columns
- // 获取索引信息
+ // Get index information
indexes, xe := d.getIndexes(ctx, conn, schema, table.Name)
if xe != nil {
return nil, xe
}
table.Indexes = indexes
- // 获取外键信息
+ // Get foreign key information
fks, xe := d.getForeignKeys(ctx, conn, schema, table.Name)
if xe != nil {
return nil, xe
@@ -63,7 +63,7 @@ func (d *Driver) DumpSchema(ctx context.Context, conn *sql.DB, opts db.SchemaOpt
return info, nil
}
-// listSchemas 获取 schema 列表
+// listSchemas retrieves the list of schemas.
func (d *Driver) listSchemas(ctx context.Context, conn *sql.DB, opts db.SchemaOptions) ([]string, *errors.XError) {
query := `
SELECT schema_name
@@ -72,7 +72,7 @@ func (d *Driver) listSchemas(ctx context.Context, conn *sql.DB, opts db.SchemaOp
`
if !opts.IncludeSystem {
- // 排除更多系统 schema
+ // Exclude additional system schemas
query += " AND schema_name NOT LIKE 'pg_%'"
}
@@ -100,7 +100,7 @@ func (d *Driver) listSchemas(ctx context.Context, conn *sql.DB, opts db.SchemaOp
return schemas, nil
}
-// listTables 获取表列表
+// listTables retrieves the list of tables.
func (d *Driver) listTables(ctx context.Context, conn *sql.DB, schema string, opts db.SchemaOptions) ([]db.Table, *errors.XError) {
query := `
SELECT
@@ -111,9 +111,9 @@ func (d *Driver) listTables(ctx context.Context, conn *sql.DB, schema string, op
`
args := []any{schema}
- // 表名过滤
+ // Table name filter
if opts.TablePattern != "" {
- // 将通配符 * 和 ? 转换为 SQL LIKE 模式
+ // Convert wildcards * and ? to SQL LIKE patterns
likePattern := strings.ReplaceAll(opts.TablePattern, "*", "%")
likePattern = strings.ReplaceAll(likePattern, "?", "_")
query += " AND t.table_name LIKE $2"
@@ -149,7 +149,7 @@ func (d *Driver) listTables(ctx context.Context, conn *sql.DB, schema string, op
return tables, nil
}
-// getColumns 获取表的列信息
+// getColumns retrieves column information for a table.
func (d *Driver) getColumns(ctx context.Context, conn *sql.DB, schema, tableName string) ([]db.Column, *errors.XError) {
query := `
SELECT
@@ -220,7 +220,7 @@ func (d *Driver) getColumns(ctx context.Context, conn *sql.DB, schema, tableName
return columns, nil
}
-// getIndexes 获取表的索引信息
+// getIndexes retrieves index information for a table.
func (d *Driver) getIndexes(ctx context.Context, conn *sql.DB, schema, tableName string) ([]db.Index, *errors.XError) {
query := `
SELECT
@@ -244,7 +244,7 @@ func (d *Driver) getIndexes(ctx context.Context, conn *sql.DB, schema, tableName
}
defer rows.Close()
- // 按 index_name 分组
+ // Group by index_name
indexMap := make(map[string]*db.Index)
for rows.Next() {
var indexName, columnName string
@@ -270,7 +270,7 @@ func (d *Driver) getIndexes(ctx context.Context, conn *sql.DB, schema, tableName
return nil, errors.Wrap(errors.CodeDBExecFailed, "rows iteration error", nil, err)
}
- // 转换为切片
+ // Convert to slice
indexes := make([]db.Index, 0, len(indexMap))
for _, idx := range indexMap {
indexes = append(indexes, *idx)
@@ -279,7 +279,7 @@ func (d *Driver) getIndexes(ctx context.Context, conn *sql.DB, schema, tableName
return indexes, nil
}
-// getForeignKeys 获取表的外键信息
+// getForeignKeys retrieves foreign key information for a table.
func (d *Driver) getForeignKeys(ctx context.Context, conn *sql.DB, schema, tableName string) ([]db.ForeignKey, *errors.XError) {
query := `
SELECT
@@ -307,7 +307,7 @@ func (d *Driver) getForeignKeys(ctx context.Context, conn *sql.DB, schema, table
}
defer rows.Close()
- // 按 constraint_name 分组
+ // Group by constraint_name
fkMap := make(map[string]*db.ForeignKey)
for rows.Next() {
var constraintName, columnName, refTable, refColumn string
@@ -333,7 +333,7 @@ func (d *Driver) getForeignKeys(ctx context.Context, conn *sql.DB, schema, table
return nil, errors.Wrap(errors.CodeDBExecFailed, "rows iteration error", nil, err)
}
- // 转换为切片
+ // Convert to slice
fks := make([]db.ForeignKey, 0, len(fkMap))
for _, fk := range fkMap {
fks = append(fks, *fk)
diff --git a/internal/db/query.go b/internal/db/query.go
index 42ce337..299c2a7 100644
--- a/internal/db/query.go
+++ b/internal/db/query.go
@@ -7,13 +7,13 @@ import (
"github.com/zx06/xsql/internal/errors"
)
-// QueryResult 是通用查询结果。
+// QueryResult represents a generic query result.
type QueryResult struct {
Columns []string `json:"columns" yaml:"columns"`
Rows []map[string]any `json:"rows" yaml:"rows"`
}
-// ToTableData 实现 output.TableFormatter 接口,支持无 JSON 编解码的表格输出。
+// ToTableData implements the output.TableFormatter interface for table output without JSON encoding/decoding.
func (r *QueryResult) ToTableData() (columns []string, rows []map[string]any, ok bool) {
if r == nil {
return nil, nil, false
@@ -21,40 +21,40 @@ func (r *QueryResult) ToTableData() (columns []string, rows []map[string]any, ok
return r.Columns, r.Rows, true
}
-// QueryOptions 包含查询执行的选项。
+// QueryOptions contains options for query execution.
type QueryOptions struct {
- UnsafeAllowWrite bool // 允许写操作(绕过只读保护)
- DBType string // 数据库类型:mysql 或 pg
+ UnsafeAllowWrite bool // Allow write operations (bypass read-only protection)
+ DBType string // Database type: mysql or pg
}
-// Query 执行 SQL 查询并返回结果。
-// 当 opts.UnsafeAllowWrite=false 时,会启用双重只读保护:
-// 1. SQL 语句静态分析(客户端)
-// 2. 数据库事务级只读模式(服务端)
-// 当 opts.UnsafeAllowWrite=true 时,绕过所有只读保护。
+// Query executes a SQL query and returns the result.
+// When opts.UnsafeAllowWrite is false, dual read-only protection is enabled:
+// 1. SQL statement static analysis (client-side)
+// 2. Database transaction-level read-only mode (server-side)
+// When opts.UnsafeAllowWrite is true, all read-only protections are bypassed.
func Query(ctx context.Context, db *sql.DB, query string, opts QueryOptions) (*QueryResult, *errors.XError) {
- // UnsafeAllowWrite 绕过所有只读保护
+ // UnsafeAllowWrite bypasses all read-only protections
if opts.UnsafeAllowWrite {
return executeQuery(ctx, db, query)
}
- // 默认启用双重只读保护
- // 第一层保护:SQL 静态分析
+ // Enable dual read-only protection by default
+ // First layer: SQL static analysis
if xe := EnforceReadOnly(query, false); xe != nil {
return nil, xe
}
- // 第二层保护:数据库事务级只读
+ // Second layer: database transaction-level read-only
return queryWithReadOnlyTx(ctx, db, query, opts.DBType)
}
-// queryWithReadOnlyTx 在只读事务中执行查询。
+// queryWithReadOnlyTx executes a query within a read-only transaction.
func queryWithReadOnlyTx(ctx context.Context, db *sql.DB, query string, dbType string) (*QueryResult, *errors.XError) {
tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, errors.Wrap(errors.CodeDBExecFailed, "failed to begin read-only transaction", nil, err)
}
defer func() {
- // 只读事务无需 commit,直接 rollback
+ // Read-only transaction needs no commit; just rollback
_ = tx.Rollback()
}()
@@ -67,7 +67,7 @@ func queryWithReadOnlyTx(ctx context.Context, db *sql.DB, query string, dbType s
return scanRows(rows)
}
-// executeQuery 直接执行查询(不使用事务)。
+// executeQuery executes a query directly (without a transaction).
func executeQuery(ctx context.Context, db *sql.DB, query string) (*QueryResult, *errors.XError) {
rows, err := db.QueryContext(ctx, query)
if err != nil {
@@ -78,7 +78,7 @@ func executeQuery(ctx context.Context, db *sql.DB, query string) (*QueryResult,
return scanRows(rows)
}
-// scanRows 扫描查询结果。
+// scanRows scans query result rows.
func scanRows(rows *sql.Rows) (*QueryResult, *errors.XError) {
cols, err := rows.Columns()
if err != nil {
diff --git a/internal/db/readonly.go b/internal/db/readonly.go
index e6e4357..78bf64d 100644
--- a/internal/db/readonly.go
+++ b/internal/db/readonly.go
@@ -28,7 +28,7 @@ const (
TokenEOF
)
-// 允许的首关键字(allowlist)
+// Allowed leading keywords (allowlist)
var allowedFirstKeywords = map[string]bool{
"SELECT": true,
"SHOW": true,
@@ -40,7 +40,7 @@ var allowedFirstKeywords = map[string]bool{
"VALUES": true, // PostgreSQL VALUES
}
-// 禁止的关键字(denylist)- 任何位置出现都拒绝
+// Denied keywords (denylist) - rejected if found anywhere
var forbiddenKeywords = map[string]bool{
"INSERT": true,
"UPDATE": true,
@@ -59,8 +59,8 @@ var forbiddenKeywords = map[string]bool{
"EXECUTE": true,
"PREPARE": true,
"DEALLOCATE": true,
- "SET": true, // 可能解除只读: SET TRANSACTION READ WRITE
- "BEGIN": true, // 用户可能 BEGIN READ WRITE
+ "SET": true, // may disable read-only: SET TRANSACTION READ WRITE
+ "BEGIN": true, // user may BEGIN READ WRITE
"COMMIT": true,
"ROLLBACK": true,
"SAVEPOINT": true,
@@ -71,17 +71,19 @@ var forbiddenKeywords = map[string]bool{
"REPLACE": true, // MySQL REPLACE
}
-// IsReadOnlySQL 做保守判定:默认拒绝;仅允许文档明确的只读语句。
-// 使用词法分析而非简单字符串匹配,正确处理字符串、注释。
-// 解析失败/多语句/包含禁止关键字时一律返回 false。
+// IsReadOnlySQL performs a conservative check: deny by default; only allow
+// explicitly documented read-only statements.
+// Uses lexical analysis instead of simple string matching to correctly handle
+// strings and comments. Returns false on parse failure, multiple statements,
+// or presence of forbidden keywords.
func IsReadOnlySQL(sql string) (bool, string) {
sql = strings.TrimSpace(sql)
if sql == "" {
return false, "empty"
}
- // 保守:首字符若不是字母,则视为无法解析(例如 "(select 1)")
- // 这是为了防止子查询绕过检测
+ // Conservative: if the first character is not a letter, treat as unparseable
+ // (e.g. "(select 1)") to prevent subquery bypass
trimmed := stripLeadingCommentsAndSpace(sql)
if trimmed == "" {
return false, "empty_after_comment"
@@ -90,18 +92,18 @@ func IsReadOnlySQL(sql string) (bool, string) {
return false, "non_letter_start"
}
- // 词法分析并检查
+ // Tokenize and validate
tokens, err := tokenize(sql)
if err != nil {
return false, "tokenize_error: " + err.Error()
}
- // 检查是否包含多语句(多个分号分隔的有效语句)
+ // Check for multiple statements (multiple valid statements separated by semicolons)
if hasMultipleValidStatements(tokens) {
return false, "multiple_statements"
}
- // 找到第一个有效关键字(跳过注释)
+ // Find the first valid keyword (skipping comments)
firstKeyword := ""
for _, tok := range tokens {
if tok.Type == TokenKeyword {
@@ -114,25 +116,25 @@ func IsReadOnlySQL(sql string) (bool, string) {
return false, "no_keyword"
}
- // 检查首关键字是否在 allowlist 中
+ // Check if the first keyword is in the allowlist
if !allowedFirstKeywords[firstKeyword] {
return false, "forbidden_start: " + firstKeyword
}
- // 检查是否包含任何禁止的关键字
+ // Check for any forbidden keywords
for _, tok := range tokens {
if tok.Type == TokenKeyword && forbiddenKeywords[tok.Value] {
- // WITH 后面跟 INSERT/UPDATE/DELETE 的情况
+ // e.g. WITH ... INSERT/UPDATE/DELETE
return false, "forbidden_keyword: " + tok.Value
}
}
- // 检查是否包含 SELECT ... FOR SHARE / FOR KEY SHARE
+ // Check for SELECT ... FOR SHARE / FOR KEY SHARE
if hasSelectShareLock(tokens) {
return false, "forbidden_share_lock"
}
- // 特殊处理 WITH (CTE) - 检查是否包含写入操作
+ // Special handling for WITH (CTE) - check for write operations
if firstKeyword == "WITH" {
if hasWriteInCTE(tokens) {
return false, "cte_write_operation"
@@ -173,7 +175,7 @@ func hasKeywordSequence(keywords []string, seq []string) bool {
return false
}
-// stripLeadingCommentsAndSpace 去除前导注释和空白
+// stripLeadingCommentsAndSpace strips leading comments and whitespace
func stripLeadingCommentsAndSpace(s string) string {
for {
s = strings.TrimLeftFunc(s, unicode.IsSpace)
@@ -207,7 +209,7 @@ func EnforceReadOnly(sql string, unsafeAllowWrite bool) *errors.XError {
return errors.New(errors.CodeROBlocked, "write blocked by read-only policy", map[string]any{"reason": reason})
}
-// tokenize 执行 SQL 词法分析,正确处理字符串、注释、标识符
+// tokenize performs SQL lexical analysis, correctly handling strings, comments, and identifiers
func tokenize(sql string) ([]SQLToken, error) {
var tokens []SQLToken
i := 0
@@ -216,30 +218,30 @@ func tokenize(sql string) ([]SQLToken, error) {
for i < sqlLen {
r := rune(sql[i])
- // 跳过空白字符
+ // Skip whitespace
if unicode.IsSpace(r) {
i++
continue
}
- // BOM 头
+ // BOM header
if r == '\ufeff' {
i++
continue
}
- // 行注释 --
+ // Line comment --
if r == '-' && i+1 < sqlLen && sql[i+1] == '-' {
- // 跳过到行尾
+ // Skip to end of line
for i < sqlLen && sql[i] != '\n' {
i++
}
continue
}
- // 块注释 /* */
+ // Block comment /* */
if r == '/' && i+1 < sqlLen && sql[i+1] == '*' {
- // 跳过到 */
+ // Skip to */
i += 2
for i < sqlLen-1 {
if sql[i] == '*' && sql[i+1] == '/' {
@@ -251,7 +253,7 @@ func tokenize(sql string) ([]SQLToken, error) {
continue
}
- // 字符串 '...' (MySQL/PostgreSQL/PG 支持 escape)
+ // String literal '...' (MySQL/PostgreSQL with escape support)
if r == '\'' {
str, newIdx := parseString(sql, i, '\'')
tokens = append(tokens, SQLToken{Type: TokenString, Value: str})
@@ -259,7 +261,7 @@ func tokenize(sql string) ([]SQLToken, error) {
continue
}
- // 字符串 "..." (PostgreSQL/ANSI 标准)
+ // String literal "..." (PostgreSQL/ANSI standard)
if r == '"' {
str, newIdx := parseString(sql, i, '"')
tokens = append(tokens, SQLToken{Type: TokenString, Value: str})
@@ -267,7 +269,7 @@ func tokenize(sql string) ([]SQLToken, error) {
continue
}
- // 反引号 `...` (MySQL 标识符)
+ // Backtick `...` (MySQL identifier)
if r == '`' {
str, newIdx := parseString(sql, i, '`')
tokens = append(tokens, SQLToken{Type: TokenIdentifier, Value: str})
@@ -275,7 +277,7 @@ func tokenize(sql string) ([]SQLToken, error) {
continue
}
- // PostgreSQL 美元引号字符串 $tag$...$tag$
+ // PostgreSQL dollar-quoted string $tag$...$tag$
if r == '$' {
if str, newIdx, ok := parseDollarQuotedString(sql, i); ok {
tokens = append(tokens, SQLToken{Type: TokenString, Value: str})
@@ -284,14 +286,14 @@ func tokenize(sql string) ([]SQLToken, error) {
}
}
- // 分号
+ // Semicolon
if r == ';' {
tokens = append(tokens, SQLToken{Type: TokenSemicolon, Value: ";"})
i++
continue
}
- // 数字
+ // Number
if unicode.IsDigit(r) || (r == '.' && i+1 < sqlLen && unicode.IsDigit(rune(sql[i+1]))) {
start := i
for i < sqlLen && (unicode.IsDigit(rune(sql[i])) || sql[i] == '.' || sql[i] == 'e' || sql[i] == 'E' || sql[i] == '+' || sql[i] == '-') {
@@ -301,7 +303,7 @@ func tokenize(sql string) ([]SQLToken, error) {
continue
}
- // 关键字或标识符
+ // Keyword or identifier
if unicode.IsLetter(r) || r == '_' {
start := i
for i < sqlLen && (unicode.IsLetter(rune(sql[i])) || unicode.IsDigit(rune(sql[i])) || sql[i] == '_' || sql[i] == '$') {
@@ -309,7 +311,7 @@ func tokenize(sql string) ([]SQLToken, error) {
}
originalWord := sql[start:i]
upperWord := strings.ToUpper(originalWord)
- // 检查是否为关键字(关键字存大写,标识符保留原样)
+ // Check if it's a keyword (keywords stored uppercase, identifiers keep original case)
if isKeyword(upperWord) {
tokens = append(tokens, SQLToken{Type: TokenKeyword, Value: upperWord})
} else {
@@ -318,7 +320,7 @@ func tokenize(sql string) ([]SQLToken, error) {
continue
}
- // 运算符和其他字符
+ // Operators and other characters
if isOperatorChar(r) {
start := i
for i < sqlLen && isOperatorChar(rune(sql[i])) {
@@ -328,7 +330,7 @@ func tokenize(sql string) ([]SQLToken, error) {
continue
}
- // 其他字符(可能是不支持的字符)
+ // Other characters (possibly unsupported)
tokens = append(tokens, SQLToken{Type: TokenUnknown, Value: string(r)})
i++
}
@@ -337,25 +339,25 @@ func tokenize(sql string) ([]SQLToken, error) {
return tokens, nil
}
-// parseString 解析引号包围的字符串,处理转义
+// parseString parses a quoted string, handling escape sequences
func parseString(sql string, start int, quote rune) (string, int) {
- i := start + 1 // 跳过开引号
+ i := start + 1 // skip opening quote
sqlLen := len(sql)
var result strings.Builder
for i < sqlLen {
r := rune(sql[i])
if r == quote {
- // 检查是否是转义('' 或 "" 在引号字符串中)
+ // Check for escape ('' or "" within quoted strings)
if i+1 < sqlLen && rune(sql[i+1]) == quote {
result.WriteRune(quote)
i += 2
continue
}
- // 字符串结束
+ // End of string
return result.String(), i + 1
}
- // 处理普通转义 \' 或 ''
+ // Handle standard escape \' or ''
if r == '\\' && i+1 < sqlLen && rune(sql[i+1]) == quote {
result.WriteRune(quote)
i += 2
@@ -365,18 +367,18 @@ func parseString(sql string, start int, quote rune) (string, int) {
i++
}
- // 未闭合的字符串
+ // Unclosed string
return result.String(), i
}
-// parseDollarQuotedString 解析 PostgreSQL 美元引号字符串 $tag$...$tag$
+// parseDollarQuotedString parses PostgreSQL dollar-quoted strings $tag$...$tag$
func parseDollarQuotedString(sql string, start int) (string, int, bool) {
sqlLen := len(sql)
if sql[start] != '$' {
return "", start, false
}
- // 解析标签名
+ // Parse tag name
tagStart := start + 1
tagEnd := tagStart
for tagEnd < sqlLen && (unicode.IsLetter(rune(sql[tagEnd])) || unicode.IsDigit(rune(sql[tagEnd])) || sql[tagEnd] == '_') {
@@ -384,14 +386,14 @@ func parseDollarQuotedString(sql string, start int) (string, int, bool) {
}
if tagEnd >= sqlLen || sql[tagEnd] != '$' {
- return "", start, false // 不是有效的美元引号
+ return "", start, false // not a valid dollar-quoted string
}
tag := sql[tagStart:tagEnd]
closingTag := "$" + tag + "$"
contentStart := tagEnd + 1
- // 查找结束标记
+ // Find closing tag
for i := contentStart; i < sqlLen; i++ {
if i+len(closingTag) <= sqlLen && sql[i:i+len(closingTag)] == closingTag {
content := sql[contentStart:i]
@@ -399,17 +401,17 @@ func parseDollarQuotedString(sql string, start int) (string, int, bool) {
}
}
- return "", start, false // 未找到闭合标记
+ return "", start, false // closing tag not found
}
-// isKeyword 判断是否为 SQL 关键字
+// isKeyword checks whether a word is a SQL keyword
func isKeyword(word string) bool {
upper := strings.ToUpper(word)
- // 合并允许的和禁止的关键字
+ // Check allowed and forbidden keyword lists
if allowedFirstKeywords[upper] || forbiddenKeywords[upper] {
return true
}
- // 其他常见关键字
+ // Other common keywords
commonKeywords := []string{
"FROM", "WHERE", "AND", "OR", "NOT", "NULL", "IS", "IN", "EXISTS",
"AS", "ON", "JOIN", "LEFT", "RIGHT", "INNER", "OUTER", "CROSS",
@@ -431,14 +433,14 @@ func isKeyword(word string) bool {
return false
}
-// isOperatorChar 判断是否为运算符字符
+// isOperatorChar checks whether a rune is an operator character
func isOperatorChar(r rune) bool {
return r == '+' || r == '-' || r == '*' || r == '/' || r == '=' || r == '<' ||
r == '>' || r == '!' || r == '~' || r == '|' || r == '&' || r == '%' ||
r == '^' || r == '@' || r == '#' || r == '?' || r == ':'
}
-// hasMultipleValidStatements 检查是否包含多个有效语句
+// hasMultipleValidStatements checks if tokens contain multiple valid statements
func hasMultipleValidStatements(tokens []SQLToken) bool {
stmtCount := 0
hasNonTrivialContent := false
@@ -455,7 +457,7 @@ func hasMultipleValidStatements(tokens []SQLToken) bool {
}
}
- // 最后一段(没有以分号结尾的部分)如果有内容也算一个语句
+ // The trailing segment (not terminated by semicolon) counts as a statement if non-empty
if hasNonTrivialContent {
stmtCount++
}
@@ -463,9 +465,9 @@ func hasMultipleValidStatements(tokens []SQLToken) bool {
return stmtCount > 1
}
-// hasWriteInCTE 检查 WITH 语句是否包含写入操作(data-modifying CTE)
+// hasWriteInCTE checks if a WITH statement contains write operations (data-modifying CTE)
func hasWriteInCTE(tokens []SQLToken) bool {
- // 简单的启发式检测:在 WITH 之后查找 INSERT/UPDATE/DELETE
+ // Simple heuristic: look for INSERT/UPDATE/DELETE after WITH
inCTE := false
parenDepth := 0
for i, tok := range tokens {
@@ -477,13 +479,13 @@ func hasWriteInCTE(tokens []SQLToken) bool {
continue
}
- // 跟踪括号深度以识别 CTE 边界
+ // Track parenthesis depth to identify CTE boundaries
switch tok.Value {
case "(":
parenDepth++
case ")":
parenDepth--
- // CTE 定义结束后的 SELECT 是正常的
+ // SELECT after CTE definition is allowed
if parenDepth == 0 && i+1 < len(tokens) {
nextTok := tokens[i+1]
switch nextTok.Value {
@@ -494,7 +496,7 @@ func hasWriteInCTE(tokens []SQLToken) bool {
}
}
default:
- // 在 CTE 体内检查写入关键字
+ // Check for write keywords inside CTE body
if parenDepth > 0 {
switch tok.Value {
case "INSERT", "UPDATE", "DELETE":
diff --git a/internal/db/registry.go b/internal/db/registry.go
index b737e93..50197d6 100644
--- a/internal/db/registry.go
+++ b/internal/db/registry.go
@@ -1,3 +1,4 @@
+// Package db provides the database driver registry and query execution engine.
package db
import (
@@ -9,27 +10,27 @@ import (
"github.com/zx06/xsql/internal/errors"
)
-// Dialer 用于自定义网络连接(如 SSH tunnel)。
+// Dialer is used for custom network connections (e.g. SSH tunnel).
type Dialer interface {
DialContext(ctx context.Context, network, addr string) (net.Conn, error)
}
-// Driver 是数据库驱动的最小抽象。
+// Driver is the minimal abstraction for a database driver.
type Driver interface {
- // Open 返回 *sql.DB;由具体 driver 实现连接参数解析。
+ // Open returns a *sql.DB; connection parameter parsing is handled by each driver.
Open(ctx context.Context, opts ConnOptions) (*sql.DB, *errors.XError)
}
-// ConnOptions 是通用连接参数(由 config/CLI/ENV 合并而来)。
+// ConnOptions holds common connection parameters (merged from config/CLI/ENV).
type ConnOptions struct {
- DSN string // 原生 DSN(优先级最高)
+ DSN string // Raw DSN (highest priority)
Host string
Port int
User string
Password string
Database string
- Params map[string]string // 额外参数
- Dialer Dialer // 自定义 dialer(如 SSH tunnel)
+ Params map[string]string // Extra parameters
+ Dialer Dialer // Custom dialer (e.g. SSH tunnel)
// RegisterCloseHook allows drivers to register cleanup callbacks that should run
// when the owning connection is closed or setup fails.
RegisterCloseHook func(fn func())
diff --git a/internal/db/schema.go b/internal/db/schema.go
index 79d5a41..41626b8 100644
--- a/internal/db/schema.go
+++ b/internal/db/schema.go
@@ -8,13 +8,13 @@ import (
"github.com/zx06/xsql/internal/output"
)
-// SchemaInfo 数据库 schema 信息
+// SchemaInfo represents database schema information.
type SchemaInfo struct {
Database string `json:"database" yaml:"database"`
Tables []Table `json:"tables" yaml:"tables"`
}
-// ToSchemaData 实现 output.SchemaFormatter 接口
+// ToSchemaData implements the output.SchemaFormatter interface.
func (s *SchemaInfo) ToSchemaData() (string, []output.SchemaTable, bool) {
if s == nil || len(s.Tables) == 0 {
return "", nil, false
@@ -41,58 +41,58 @@ func (s *SchemaInfo) ToSchemaData() (string, []output.SchemaTable, bool) {
return s.Database, tables, true
}
-// Table 表信息
+// Table represents table information.
type Table struct {
- Schema string `json:"schema" yaml:"schema"` // PostgreSQL schema,MySQL 为数据库名
- Name string `json:"name" yaml:"name"` // 表名
+ Schema string `json:"schema" yaml:"schema"` // PostgreSQL schema; database name for MySQL
+ Name string `json:"name" yaml:"name"` // Table name
Comment string `json:"comment,omitempty" yaml:"comment,omitempty"`
Columns []Column `json:"columns" yaml:"columns"`
Indexes []Index `json:"indexes,omitempty" yaml:"indexes,omitempty"`
ForeignKeys []ForeignKey `json:"foreign_keys,omitempty" yaml:"foreign_keys,omitempty"`
}
-// Column 列信息
+// Column represents column information.
type Column struct {
Name string `json:"name" yaml:"name"`
- Type string `json:"type" yaml:"type"` // 数据类型,如 varchar(255)、bigint
- Nullable bool `json:"nullable" yaml:"nullable"` // 是否允许 NULL
- Default string `json:"default,omitempty" yaml:"default,omitempty"` // 默认值
- Comment string `json:"comment,omitempty" yaml:"comment,omitempty"` // 列注释
- PrimaryKey bool `json:"primary_key" yaml:"primary_key"` // 是否为主键
+ Type string `json:"type" yaml:"type"` // Data type, e.g. varchar(255), bigint
+ Nullable bool `json:"nullable" yaml:"nullable"` // Whether NULL is allowed
+ Default string `json:"default,omitempty" yaml:"default,omitempty"` // Default value
+ Comment string `json:"comment,omitempty" yaml:"comment,omitempty"` // Column comment
+ PrimaryKey bool `json:"primary_key" yaml:"primary_key"` // Whether this is a primary key
}
-// Index 索引信息
+// Index represents index information.
type Index struct {
- Name string `json:"name" yaml:"name"` // 索引名
- Columns []string `json:"columns" yaml:"columns"` // 索引列
- Unique bool `json:"unique" yaml:"unique"` // 是否唯一索引
- Primary bool `json:"primary" yaml:"primary"` // 是否主键索引
+ Name string `json:"name" yaml:"name"` // Index name
+ Columns []string `json:"columns" yaml:"columns"` // Indexed columns
+ Unique bool `json:"unique" yaml:"unique"` // Whether this is a unique index
+ Primary bool `json:"primary" yaml:"primary"` // Whether this is a primary key index
}
-// ForeignKey 外键信息
+// ForeignKey represents foreign key information.
type ForeignKey struct {
- Name string `json:"name" yaml:"name"` // 外键名
- Columns []string `json:"columns" yaml:"columns"` // 本表列
- ReferencedTable string `json:"referenced_table" yaml:"referenced_table"` // 引用表
- ReferencedColumns []string `json:"referenced_columns" yaml:"referenced_columns"` // 引用列
+ Name string `json:"name" yaml:"name"` // Foreign key name
+ Columns []string `json:"columns" yaml:"columns"` // Local columns
+ ReferencedTable string `json:"referenced_table" yaml:"referenced_table"` // Referenced table
+ ReferencedColumns []string `json:"referenced_columns" yaml:"referenced_columns"` // Referenced columns
}
-// SchemaOptions schema 导出选项
+// SchemaOptions holds options for schema export.
type SchemaOptions struct {
- TablePattern string // 表名过滤(支持通配符)
- IncludeSystem bool // 是否包含系统表
+ TablePattern string // Table name filter (supports wildcards)
+ IncludeSystem bool // Whether to include system tables
}
-// SchemaDriver schema 导出接口
-// Driver 可选择实现此接口以支持 schema 导出
+// SchemaDriver is the schema export interface.
+// A Driver may optionally implement this interface to support schema export.
type SchemaDriver interface {
Driver
- // DumpSchema 导出数据库结构
+ // DumpSchema exports the database schema.
DumpSchema(ctx context.Context, db *sql.DB, opts SchemaOptions) (*SchemaInfo, *errors.XError)
}
-// DumpSchema 导出数据库结构
-// 会检查 driver 是否实现了 SchemaDriver 接口
+// DumpSchema exports the database schema.
+// It checks whether the driver implements the SchemaDriver interface.
func DumpSchema(ctx context.Context, driverName string, db *sql.DB, opts SchemaOptions) (*SchemaInfo, *errors.XError) {
d, ok := Get(driverName)
if !ok {
diff --git a/internal/errors/codes.go b/internal/errors/codes.go
index f9224c3..6119faf 100644
--- a/internal/errors/codes.go
+++ b/internal/errors/codes.go
@@ -1,7 +1,7 @@
package errors
-// Code 是稳定错误码(字符串),供 AI/agent 与程序判断。
-// 只增不改、不复用旧含义。
+// Code is a stable error code (string) for AI/agent and programmatic consumption.
+// Codes are append-only; existing meanings must not be changed or reused.
type Code string
const (
diff --git a/internal/errors/error.go b/internal/errors/error.go
index 3e55273..9068153 100644
--- a/internal/errors/error.go
+++ b/internal/errors/error.go
@@ -1,3 +1,4 @@
+// Package errors provides structured error types, error codes, and exit codes for xsql.
package errors
import (
@@ -5,7 +6,7 @@ import (
"fmt"
)
-// XError 是结构化错误,满足 docs/error-contract.md。
+// XError is a structured error that conforms to docs/error-contract.md.
type XError struct {
Code Code `json:"code" yaml:"code"`
Message string `json:"message" yaml:"message"`
diff --git a/internal/errors/exitcode.go b/internal/errors/exitcode.go
index 8fedf2d..c999b91 100644
--- a/internal/errors/exitcode.go
+++ b/internal/errors/exitcode.go
@@ -1,24 +1,24 @@
package errors
-// ExitCode 是进程退出码(稳定契约);详见 docs/error-contract.md。
+// ExitCode represents process exit codes (stable contract); see docs/error-contract.md.
type ExitCode int
const (
ExitOK ExitCode = 0
- // 2: 参数/配置错误
+ // 2: argument/configuration error
ExitConfig ExitCode = 2
- // 3: 连接错误(DB/SSH)
+ // 3: connection error (DB/SSH)
ExitConnect ExitCode = 3
- // 4: 只读策略拦截写入
+ // 4: read-only policy blocked a write
ExitReadOnly ExitCode = 4
- // 5: DB 执行错误
+ // 5: DB execution error
ExitDBExec ExitCode = 5
- // 10: 内部错误
+ // 10: internal error
ExitInternal ExitCode = 10
)
diff --git a/internal/log/log.go b/internal/log/log.go
index a191c01..54ff9bc 100644
--- a/internal/log/log.go
+++ b/internal/log/log.go
@@ -1,3 +1,4 @@
+// Package log provides logging utilities for xsql.
package log
import (
@@ -5,8 +6,8 @@ import (
"log/slog"
)
-// New 返回写入到 w 的 slog.Logger(默认 level=INFO)。
-// 注意:stdout=数据,日志应始终写 stderr(由调用方传入)。
+// New returns a slog.Logger that writes to w (default level=INFO).
+// Note: stdout is for data; logs should always be written to stderr (passed by caller).
func New(w io.Writer) *slog.Logger {
h := slog.NewTextHandler(w, &slog.HandlerOptions{Level: slog.LevelInfo})
return slog.New(h)
diff --git a/internal/output/writer.go b/internal/output/writer.go
index 0093f4c..c2b88ab 100644
--- a/internal/output/writer.go
+++ b/internal/output/writer.go
@@ -61,17 +61,17 @@ func (w Writer) write(format Format, env Envelope) error {
}
}
-// TableFormatter 接口:支持表格输出的数据结构实现此接口
+// TableFormatter is the interface for data structures that support table output.
type TableFormatter interface {
ToTableData() (columns []string, rows []map[string]any, ok bool)
}
-// SchemaFormatter 接口:支持 schema 输出的结构实现此接口
+// SchemaFormatter is the interface for data structures that support schema output.
type SchemaFormatter interface {
ToSchemaData() (database string, tables []SchemaTable, ok bool)
}
-// SchemaTable schema 表格输出的简化结构
+// SchemaTable is a simplified structure for schema table output.
type SchemaTable struct {
Schema string
Name string
@@ -79,51 +79,51 @@ type SchemaTable struct {
Columns []SchemaColumn
}
-// ProfileListFormatter 接口:支持 profile list 输出的结构实现此接口
+// ProfileListFormatter is the interface for data structures that support profile list output.
type ProfileListFormatter interface {
ToProfileListData() (configPath string, profiles []profileListItem, ok bool)
}
func writeTable(out io.Writer, env Envelope) error {
if !env.OK {
- // 错误输出:简洁显示错误信息
+ // Error output: display error message concisely
if env.Error != nil {
_, _ = fmt.Fprintf(out, "Error [%s]: %s\n", env.Error.Code, env.Error.Message)
}
return nil
}
- // 优先检查是否实现了 TableFormatter 接口(无 JSON 编解码)
+ // First check if the data implements the TableFormatter interface (no JSON encode/decode)
if formatter, ok := env.Data.(TableFormatter); ok {
if cols, rows, ok := formatter.ToTableData(); ok {
return writeQueryResultTable(out, cols, rows)
}
}
- // 优先检查是否实现了 ProfileListFormatter 接口
+ // Check if the data implements the ProfileListFormatter interface
if formatter, ok := env.Data.(ProfileListFormatter); ok {
if cfgPath, profiles, ok := formatter.ToProfileListData(); ok {
return writeProfileListTable(out, cfgPath, profiles)
}
}
- // 检查是否实现了 SchemaFormatter 接口
+ // Check if the data implements the SchemaFormatter interface
if formatter, ok := env.Data.(SchemaFormatter); ok {
if database, tables, ok := formatter.ToSchemaData(); ok {
return writeSchemaTable(out, database, tables)
}
}
- // 回退:使用类型断言从 map[string]any 提取
+ // Fallback: use type assertion to extract from map[string]any
if m, ok := env.Data.(map[string]any); ok {
- // 尝试提取查询结果
+ // Try to extract query result
if cols, ok := extractStringSlice(m["columns"]); ok {
if rows, ok := extractMapSlice(m["rows"]); ok {
return writeQueryResultTable(out, cols, rows)
}
}
- // 尝试提取 profile list
+ // Try to extract profile list
if profilesRaw, hasProfiles := m["profiles"]; hasProfiles {
if profileList, ok := tryAsProfileList(profilesRaw); ok {
cfgPath, _ := m["config_path"].(string)
@@ -131,7 +131,7 @@ func writeTable(out io.Writer, env Envelope) error {
}
}
- // 默认:直接输出 key-value(按 key 排序以保证稳定性)
+ // Default: output key-value pairs (sorted by key for stability)
tw := tabwriter.NewWriter(out, 0, 2, 2, ' ', 0)
for _, k := range sortedMapKeys(m) {
_, _ = fmt.Fprintf(tw, "%s\t%v\n", k, m[k])
@@ -139,12 +139,12 @@ func writeTable(out io.Writer, env Envelope) error {
return tw.Flush()
}
- // 最后尝试反射提取(无 JSON 编解码)
+ // Last resort: try reflection-based extraction (no JSON encode/decode)
if result, ok := tryAsQueryResultReflect(env.Data); ok {
return writeQueryResultTable(out, result.columns, result.rows)
}
- // 默认:直接输出数据的 key-value
+ // Default: output data as key-value pairs
tw := tabwriter.NewWriter(out, 0, 2, 2, ' ', 0)
if env.Data != nil {
if m, ok := env.Data.(map[string]any); ok {
@@ -163,20 +163,20 @@ func writeTable(out io.Writer, env Envelope) error {
return tw.Flush()
}
-// writeProfileListTable 输出 profile list 表格
+// writeProfileListTable writes the profile list as a table.
func writeProfileListTable(out io.Writer, cfgPath string, profiles []profileListItem) error {
tw := tabwriter.NewWriter(out, 0, 2, 2, ' ', 0)
- // 先输出 config_path
+ // Output config_path first
if cfgPath != "" {
_, _ = fmt.Fprintf(tw, "Config: %s\n\n", cfgPath)
}
- // 输出 profiles 表格
+ // Output profiles table
_, _ = fmt.Fprintln(tw, "NAME\tDESCRIPTION\tDB\tMODE")
_, _ = fmt.Fprintln(tw, "----\t-----------\t--\t----")
for _, p := range profiles {
_, _ = fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", p.Name, p.Description, p.DB, p.Mode)
}
- // 使用正确的单数/复数形式
+ // Use correct singular/plural form
suffix := "profiles"
if len(profiles) == 1 {
suffix = "profile"
@@ -185,16 +185,16 @@ func writeProfileListTable(out io.Writer, cfgPath string, profiles []profileList
return tw.Flush()
}
-// extractStringSlice 从 any 提取 []string
+// extractStringSlice extracts a []string from any.
func extractStringSlice(v any) ([]string, bool) {
if v == nil {
return nil, false
}
- // 已经是 []string
+ // Already a []string
if ss, ok := v.([]string); ok {
return ss, true
}
- // 尝试 []any
+ // Try []any
if arr, ok := v.([]any); ok {
result := make([]string, len(arr))
for i, item := range arr {
@@ -209,7 +209,7 @@ func extractStringSlice(v any) ([]string, bool) {
return nil, false
}
-// extractMapSlice 从 any 提取 []map[string]any
+// extractMapSlice extracts a []map[string]any from any.
func extractMapSlice(v any) ([]map[string]any, bool) {
if v == nil {
return nil, false
@@ -239,12 +239,12 @@ type profileListItem struct {
}
func tryAsProfileList(data any) ([]profileListItem, bool) {
- // 处理 []profileListItem
+ // Handle []profileListItem
if arr, ok := data.([]profileListItem); ok {
return arr, len(arr) > 0
}
- // 处理 []map[string]any
+ // Handle []map[string]any
if arr, ok := data.([]map[string]any); ok {
result := make([]profileListItem, 0, len(arr))
for _, m := range arr {
@@ -269,22 +269,22 @@ func tryAsProfileList(data any) ([]profileListItem, bool) {
return result, len(result) > 0
}
- // 使用反射处理任意结构体切片(如 cmd/xsql/profile.go 中的 []profileInfo)
+ // Use reflection to handle arbitrary struct slices (e.g., []profileInfo in cmd/xsql/profile.go)
v := reflect.ValueOf(data)
if v.IsValid() && v.Kind() == reflect.Slice {
result := make([]profileListItem, 0, v.Len())
for i := 0; i < v.Len(); i++ {
elem := v.Index(i)
- // 解引用指针
+ // Dereference pointer
if elem.Kind() == reflect.Ptr {
elem = elem.Elem()
}
- // 只处理结构体
+ // Only handle structs
if elem.Kind() != reflect.Struct {
return nil, false
}
p := profileListItem{}
- // 读取字段
+ // Read fields
if f := elem.FieldByName("Name"); f.IsValid() && f.Kind() == reflect.String {
p.Name = f.String()
}
@@ -305,7 +305,7 @@ func tryAsProfileList(data any) ([]profileListItem, bool) {
return result, len(result) > 0
}
- // 处理 []any
+ // Handle []any
arr, ok := data.([]any)
if !ok {
return nil, false
@@ -343,7 +343,7 @@ type queryResultLike struct {
rows []map[string]any
}
-// tryAsQueryResultReflect 使用反射检查字段 Columns 和 Rows(无 JSON 编解码)
+// tryAsQueryResultReflect uses reflection to check for Columns and Rows fields (no JSON encode/decode).
func tryAsQueryResultReflect(data any) (*queryResultLike, bool) {
if data == nil {
return nil, false
@@ -357,7 +357,7 @@ func tryAsQueryResultReflect(data any) (*queryResultLike, bool) {
return nil, false
}
- // 查找 Columns 和 Rows 字段
+ // Find the Columns and Rows fields
var colsValue, rowsValue reflect.Value
t := v.Type()
for i := 0; i < v.NumField(); i++ {
@@ -374,7 +374,7 @@ func tryAsQueryResultReflect(data any) (*queryResultLike, bool) {
return nil, false
}
- // 提取 columns
+ // Extract columns
if colsValue.Kind() != reflect.Slice {
return nil, false
}
@@ -394,7 +394,7 @@ func tryAsQueryResultReflect(data any) (*queryResultLike, bool) {
}
}
- // 提取 rows
+ // Extract rows
if rowsValue.Kind() != reflect.Slice {
return nil, false
}
@@ -429,17 +429,17 @@ func tryAsQueryResultReflect(data any) (*queryResultLike, bool) {
func writeQueryResultTable(out io.Writer, cols []string, rows []map[string]any) error {
tw := tabwriter.NewWriter(out, 0, 2, 2, ' ', 0)
- // 表头
+ // Header
_, _ = fmt.Fprintln(tw, strings.Join(cols, "\t"))
- // 分隔线
+ // Separator line
dashes := make([]string, len(cols))
for i, c := range cols {
dashes[i] = strings.Repeat("-", len(c))
}
_, _ = fmt.Fprintln(tw, strings.Join(dashes, "\t"))
- // 数据行
+ // Data rows
for _, row := range rows {
vals := make([]string, len(cols))
for i, c := range cols {
@@ -448,7 +448,7 @@ func writeQueryResultTable(out io.Writer, cols []string, rows []map[string]any)
_, _ = fmt.Fprintln(tw, strings.Join(vals, "\t"))
}
- // 行数统计
+ // Row count
_, _ = fmt.Fprintf(tw, "\n(%d rows)\n", len(rows))
return tw.Flush()
@@ -462,7 +462,7 @@ func formatCellValue(v any, nullValue string) string {
case string:
return val
case float64:
- // JSON 数字都是 float64
+ // JSON numbers are always float64
if val == float64(int64(val)) {
return fmt.Sprintf("%d", int64(val))
}
@@ -477,24 +477,24 @@ func writeCSV(out io.Writer, env Envelope) error {
defer cw.Flush()
if !env.OK {
- // 错误输出
+ // Error output
if env.Error != nil {
_ = cw.Write([]string{"error", string(env.Error.Code), env.Error.Message})
}
return cw.Error()
}
- // 尝试解析为查询结果(优先使用接口,然后反射)
+ // Try to parse as query result (prefer interface, then reflection)
var cols []string
var rows []map[string]any
var dataOK bool
- // 先尝试 TableFormatter 接口
+ // First try the TableFormatter interface
if formatter, isFormatter := env.Data.(TableFormatter); isFormatter {
cols, rows, dataOK = formatter.ToTableData()
}
- // 回退:使用反射
+ // Fallback: use reflection
if !dataOK {
if result, ok2 := tryAsQueryResultReflect(env.Data); ok2 {
cols = result.columns
@@ -503,7 +503,7 @@ func writeCSV(out io.Writer, env Envelope) error {
}
}
- // 回退:使用 map 提取
+ // Fallback: extract from map
if !dataOK {
if m, ok2 := env.Data.(map[string]any); ok2 {
var ok3 bool
@@ -515,9 +515,9 @@ func writeCSV(out io.Writer, env Envelope) error {
}
if dataOK {
- // 输出表头
+ // Write header
_ = cw.Write(cols)
- // 输出数据行
+ // Write data rows
for _, row := range rows {
vals := make([]string, len(cols))
for i, c := range cols {
@@ -528,7 +528,7 @@ func writeCSV(out io.Writer, env Envelope) error {
return cw.Error()
}
- // 默认:输出为 key,value 格式
+ // Default: output as key,value format
if m, ok := env.Data.(map[string]any); ok {
for _, k := range sortedMapKeys(m) {
_ = cw.Write([]string{k, fmt.Sprintf("%v", m[k])})
@@ -546,20 +546,20 @@ func sortedMapKeys(m map[string]any) []string {
return keys
}
-// writeSchemaTable 输出 schema 表格
+// writeSchemaTable writes the schema as a table.
func writeSchemaTable(out io.Writer, database string, tables []SchemaTable) error {
- // 输出数据库名
+ // Output database name
if database != "" {
_, _ = fmt.Fprintf(out, "Database: %s\n\n", database)
}
- // 遍历每个表
+ // Iterate over each table
for i, table := range tables {
if i > 0 {
- _, _ = fmt.Fprintln(out) // 表之间空一行
+ _, _ = fmt.Fprintln(out) // Blank line between tables
}
- // 表头
+ // Table header
header := table.Name
if table.Schema != "" && table.Schema != database {
header = table.Schema + "." + table.Name
@@ -569,7 +569,7 @@ func writeSchemaTable(out io.Writer, database string, tables []SchemaTable) erro
}
_, _ = fmt.Fprintf(out, "Table: %s\n", header)
- // 列信息
+ // Column information
if len(table.Columns) > 0 {
_, _ = fmt.Fprintln(out, " Columns:")
tw := tabwriter.NewWriter(out, 0, 2, 2, ' ', 0)
@@ -595,7 +595,7 @@ func writeSchemaTable(out io.Writer, database string, tables []SchemaTable) erro
}
}
- // 表数量统计
+ // Table count
suffix := "tables"
if len(tables) == 1 {
suffix = "table"
@@ -604,7 +604,7 @@ func writeSchemaTable(out io.Writer, database string, tables []SchemaTable) erro
return nil
}
-// SchemaColumn schema 列输出的简化结构
+// SchemaColumn is a simplified structure for schema column output.
type SchemaColumn struct {
Name string
Type string
diff --git a/internal/secret/keyring.go b/internal/secret/keyring.go
index fef22bd..05b9f02 100644
--- a/internal/secret/keyring.go
+++ b/internal/secret/keyring.go
@@ -1,19 +1,20 @@
+// Package secret handles credential resolution including OS keyring integration.
package secret
-// KeyringAPI 是对 OS keyring 的最小抽象,便于测试与跨平台。
-// service 对应 keyring 的 service name,account 对应 user/account。
+// KeyringAPI is a minimal abstraction over the OS keyring for testability and cross-platform support.
+// service corresponds to the keyring service name; account corresponds to the user/account.
type KeyringAPI interface {
Get(service, account string) (string, error)
Set(service, account, value string) error
Delete(service, account string) error
}
-// 默认 keyring 实现(使用 zalando/go-keyring)
-// 本文件仅定义接口;实现见 keyring_*.go(按平台编译)。
+// defaultKeyring returns the default keyring implementation (using zalando/go-keyring).
+// This file only defines the interface; implementations are in keyring_*.go (platform-specific builds).
func defaultKeyring() KeyringAPI {
return &osKeyring{}
}
type osKeyring struct{}
-// Get/Set/Delete 见 keyring_default.go。
+// Get/Set/Delete are implemented in keyring_default.go.
diff --git a/internal/secret/keyring_windows.go b/internal/secret/keyring_windows.go
index cf81afd..8629de8 100644
--- a/internal/secret/keyring_windows.go
+++ b/internal/secret/keyring_windows.go
@@ -13,7 +13,7 @@ func (o *osKeyring) Get(service, account string) (string, error) {
if err != nil {
return "", err
}
- // Windows cmdkey 在字符间插入 null 字节(UTF-16 遗留问题)
+ // Windows cmdkey inserts null bytes between characters (UTF-16 legacy issue).
val = strings.ReplaceAll(val, "\x00", "")
return val, nil
}
diff --git a/internal/secret/resolve.go b/internal/secret/resolve.go
index f7b7555..7731da1 100644
--- a/internal/secret/resolve.go
+++ b/internal/secret/resolve.go
@@ -8,17 +8,17 @@ import (
const keyringPrefix = "keyring:"
-// Options 控制 secret 解析行为。
+// Options controls secret resolution behavior.
type Options struct {
- AllowPlaintext bool // 是否允许明文(默认 false)
- Keyring KeyringAPI // 可注入的 keyring 实现(nil 则用默认)
+ AllowPlaintext bool // whether to allow plaintext secrets (default false)
+ Keyring KeyringAPI // injectable keyring implementation (nil uses default)
}
const defaultService = "xsql"
-// parseKeyringRef 解析 keyring 引用。
-// 整个引用作为 account,service 固定为 "xsql"。
-// 例如 "prod/db_password" → service="xsql", account="prod/db_password"
+// parseKeyringRef parses a keyring reference.
+// The entire reference is used as the account; service is fixed to "xsql".
+// For example, "prod/db_password" → service="xsql", account="prod/db_password".
func parseKeyringRef(ref string) (service, account string, err *errors.XError) {
if ref == "" {
return "", "", errors.New(errors.CodeCfgInvalid,
@@ -28,12 +28,12 @@ func parseKeyringRef(ref string) (service, account string, err *errors.XError) {
return defaultService, ref, nil
}
-// Resolve 解析 secret 值,遵循 docs/config.md 的顺序:
-// 1. keyring:/ → 从 keyring 读取
-// 2. 否则若为明文且允许明文 → 直接返回
-// 3. 否则报错
+// Resolve resolves a secret value following the order defined in docs/config.md:
+// 1. keyring:/ → read from the keyring
+// 2. otherwise, if plaintext and plaintext is allowed → return as-is
+// 3. otherwise → return an error
//
-// 注意:TTY 交互输入本阶段不实现(留给 cmd 层处理)。
+// Note: TTY interactive input is not implemented at this layer (left to the cmd layer).
func Resolve(raw string, opts Options) (string, *errors.XError) {
if strings.HasPrefix(raw, keyringPrefix) {
ref := strings.TrimPrefix(raw, keyringPrefix)
@@ -52,14 +52,14 @@ func Resolve(raw string, opts Options) (string, *errors.XError) {
}
return val, nil
}
- // 明文
+ // Plaintext
if opts.AllowPlaintext {
return raw, nil
}
return "", errors.New(errors.CodeCfgInvalid, "plaintext secret not allowed; use keyring: reference or enable --allow-plaintext", nil)
}
-// IsKeyringRef 判断值是否为 keyring 引用。
+// IsKeyringRef reports whether s is a keyring reference.
func IsKeyringRef(s string) bool {
return strings.HasPrefix(s, keyringPrefix)
}
diff --git a/internal/ssh/client.go b/internal/ssh/client.go
index 66820dc..ae8052e 100644
--- a/internal/ssh/client.go
+++ b/internal/ssh/client.go
@@ -1,3 +1,4 @@
+// Package ssh provides SSH tunnel connectivity for database drivers.
package ssh
import (
@@ -14,12 +15,12 @@ import (
"github.com/zx06/xsql/internal/errors"
)
-// Client 包装 ssh.Client,提供 DialContext 供 driver 使用。
+// Client wraps ssh.Client and provides DialContext for use by database drivers.
type Client struct {
client *ssh.Client
}
-// Connect 建立 SSH 连接。
+// Connect establishes an SSH connection.
func Connect(ctx context.Context, opts Options) (*Client, *errors.XError) {
if opts.Host == "" {
return nil, errors.New(errors.CodeCfgInvalid, "ssh host is required", nil)
@@ -61,12 +62,12 @@ func Connect(ctx context.Context, opts Options) (*Client, *errors.XError) {
return &Client{client: client}, nil
}
-// DialContext 通过 SSH 通道建立到 target 的连接。
+// DialContext establishes a connection to the target through the SSH tunnel.
func (c *Client) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
return c.client.Dial(network, addr)
}
-// Close 关闭 SSH 连接。
+// Close closes the SSH connection.
func (c *Client) Close() error {
if c.client != nil {
return c.client.Close()
@@ -77,7 +78,7 @@ func (c *Client) Close() error {
func buildAuthMethods(opts Options) ([]ssh.AuthMethod, *errors.XError) {
var methods []ssh.AuthMethod
- // 私钥认证
+ // Private key authentication
if opts.IdentityFile != "" {
keyPath := expandPath(opts.IdentityFile)
keyData, err := os.ReadFile(keyPath)
@@ -96,7 +97,7 @@ func buildAuthMethods(opts Options) ([]ssh.AuthMethod, *errors.XError) {
methods = append(methods, ssh.PublicKeys(signer))
}
- // 尝试默认私钥路径
+ // Try default private key paths
if len(methods) == 0 {
for _, name := range []string{"id_ed25519", "id_rsa", "id_ecdsa"} {
keyPath := expandPath("~/.ssh/" + name)
diff --git a/internal/ssh/options.go b/internal/ssh/options.go
index 1567e4a..3482f34 100644
--- a/internal/ssh/options.go
+++ b/internal/ssh/options.go
@@ -1,15 +1,15 @@
package ssh
-// Options 包含 SSH 连接所需参数。
+// Options contains the parameters required for an SSH connection.
type Options struct {
Host string
Port int
User string
- IdentityFile string // 私钥路径
- Passphrase string // 私钥 passphrase(若有)
- KnownHostsFile string // 默认 ~/.ssh/known_hosts
+ IdentityFile string // path to the private key
+ Passphrase string // private key passphrase (if any)
+ KnownHostsFile string // defaults to ~/.ssh/known_hosts
- // SkipKnownHostsCheck 跳过 known_hosts 校验(极不推荐!)
+ // SkipKnownHostsCheck disables known_hosts verification (strongly discouraged!).
SkipKnownHostsCheck bool
}