Add AI SQL generation and update project docs
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+53
-36
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
可行性分析用于判断 HyperSql 在当前技术条件、开发时间、经济成本和实际使用场景下是否能够顺利完成。
|
可行性分析用于判断 HyperSql 在当前技术条件、开发时间、经济成本和实际使用场景下是否能够顺利完成。
|
||||||
|
|
||||||
HyperSql 是一个基于 Java 24、JavaFX、Maven 和 SQLite 的轻量级数据库图形化管理工具,主要面向学生和 SQLite 初学者。目前项目已经完成了核心数据库浏览、表管理、数据行编辑和 SQLite 支持范围内的表结构编辑功能,后续将继续扩展备份恢复和 AI 辅助 SQL 生成。
|
HyperSql 是一个基于 Java 24、JavaFX、Maven 和 SQLite 的轻量级数据库图形化管理工具,主要面向学生和 SQLite 初学者。目前项目已经完成了核心数据库浏览、表管理、数据行编辑、SQLite 支持范围内的表结构编辑、数据库备份恢复和 AI 辅助 SQL 生成功能。
|
||||||
|
|
||||||
本项目从以下四个方面进行分析:
|
本项目从以下四个方面进行分析:
|
||||||
|
|
||||||
@@ -20,11 +20,13 @@ HyperSql 采用以下技术栈:
|
|||||||
| 技术 | 作用 | 当前使用情况 |
|
| 技术 | 作用 | 当前使用情况 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Java 24 | 主要开发语言 | 已用于项目主要代码开发 |
|
| Java 24 | 主要开发语言 | 已用于项目主要代码开发 |
|
||||||
| JavaFX | 构建图形化用户界面 | 已实现主界面、表格、菜单、工具栏、创建表对话框、数据行编辑和结构编辑操作入口 |
|
| JavaFX | 构建图形化用户界面 | 已实现主界面、表格、菜单、工具栏、创建表对话框、数据行编辑、结构编辑、备份恢复入口和 AI 设置/生成入口 |
|
||||||
| Maven | 项目构建与依赖管理 | 已完成项目构建配置 |
|
| Maven | 项目构建与依赖管理 | 已完成项目构建配置,并管理 JavaFX、SQLite JDBC、Jackson 等依赖 |
|
||||||
| SQLite | 本地轻量级数据库 | 作为系统管理对象,并通过原生 ALTER TABLE 支持部分表结构编辑 |
|
| SQLite | 本地轻量级数据库 | 作为系统管理对象,并通过原生 ALTER TABLE 支持部分表结构编辑 |
|
||||||
| SQLite JDBC | Java 程序访问 SQLite 数据库 | 已实现数据库连接、元数据读取、SQL 执行、数据行写入和表结构变更 |
|
| SQLite JDBC | Java 程序访问 SQLite 数据库 | 已实现数据库连接、元数据读取、SQL 执行、数据行写入、表结构变更、备份前检查点等操作 |
|
||||||
| AI API | 根据表结构和用户需求生成 SQL | 作为后续扩展功能,当前尚未实现 |
|
| JDK HttpClient | 调用 AI API | 已用于请求 OpenAI 兼容接口和 Anthropic Claude Messages API |
|
||||||
|
| Jackson | JSON 构造与解析 | 已用于构造 AI 请求体和解析 AI 返回结果 |
|
||||||
|
| AI API | 根据表结构和用户需求生成 SQL | 已支持 OpenAI 兼容接口和 Anthropic Claude,API Key 由用户在 UI 中输入并仅保存在本次运行内存中 |
|
||||||
|
|
||||||
### 技术可行性说明
|
### 技术可行性说明
|
||||||
|
|
||||||
@@ -33,16 +35,16 @@ HyperSql 采用以下技术栈:
|
|||||||
- 当前项目已经通过 Maven 编译验证,说明技术环境可正常运行。
|
- 当前项目已经通过 Maven 编译验证,说明技术环境可正常运行。
|
||||||
|
|
||||||
2. **JavaFX 能满足 GUI 需求**
|
2. **JavaFX 能满足 GUI 需求**
|
||||||
- JavaFX 提供菜单栏、按钮、标签、文本框、表格、分页按钮、弹窗和 FXML 等组件。
|
- JavaFX 提供菜单栏、按钮、标签、文本框、密码输入框、下拉框、表格、分页按钮、弹窗和 FXML 等组件。
|
||||||
- 当前项目已经实现左侧表/视图列表,右侧表数据、表结构和 SQL 执行区域,并提供数据行新增/删除按钮、可编辑单元格和表结构编辑按钮,能够满足数据库管理工具的基本界面需求。
|
- 当前项目已经实现左侧表/视图列表,右侧表数据、表结构和 SQL 执行区域,并提供数据行新增/删除按钮、可编辑单元格、表结构编辑按钮、备份恢复菜单和 AI SQL 生成入口。
|
||||||
|
|
||||||
3. **SQLite 集成难度低**
|
3. **SQLite 集成难度低**
|
||||||
- SQLite 是文件型数据库,不需要单独部署数据库服务器,适合课程项目和本地数据库管理。
|
- SQLite 是文件型数据库,不需要单独部署数据库服务器,适合课程项目和本地数据库管理。
|
||||||
- 当前项目已经支持打开、新建、关闭和删除 SQLite 数据库文件。
|
- 当前项目已经支持打开、新建、关闭、删除、备份和恢复 SQLite 数据库文件。
|
||||||
|
|
||||||
4. **SQLite JDBC 访问方式成熟**
|
4. **SQLite JDBC 访问方式成熟**
|
||||||
- 通过 JDBC 可以完成数据库连接、查询、执行 SQL、读取表结构等操作。
|
- 通过 JDBC 可以完成数据库连接、查询、执行 SQL、读取表结构等操作。
|
||||||
- 当前项目已经实现表/视图列表读取、字段信息读取、表数据分页查询、SQL 执行、数据行插入/更新/删除和部分表结构变更。
|
- 当前项目已经实现表/视图列表读取、字段信息读取、表数据分页查询、SQL 执行、数据行插入/更新/删除、部分表结构变更和备份恢复前后的连接管理。
|
||||||
|
|
||||||
5. **表管理和结构编辑功能可以通过 SQL 和 JavaFX UI 实现**
|
5. **表管理和结构编辑功能可以通过 SQL 和 JavaFX UI 实现**
|
||||||
- 当前项目已经通过独立创建表对话框实现了表名输入、字段定义、字段类型选择、主键/非空设置、默认值填写和 SQL 预览。
|
- 当前项目已经通过独立创建表对话框实现了表名输入、字段定义、字段类型选择、主键/非空设置、默认值填写和 SQL 预览。
|
||||||
@@ -53,20 +55,26 @@ HyperSql 采用以下技术栈:
|
|||||||
- 当前项目已经通过主键或 SQLite `rowid` 定位数据行,使用参数化 SQL 完成新增、修改和删除。
|
- 当前项目已经通过主键或 SQLite `rowid` 定位数据行,使用参数化 SQL 完成新增、修改和删除。
|
||||||
- SQL 返回结果只对可识别的单表查询启用编辑,复杂查询保持只读,降低误操作风险。
|
- SQL 返回结果只对可识别的单表查询启用编辑,复杂查询保持只读,降低误操作风险。
|
||||||
|
|
||||||
7. **AI SQL 生成功能技术上可行,但需要控制范围**
|
7. **数据库备份恢复功能技术上可行**
|
||||||
- 后续可以读取当前数据库表结构,结合用户自然语言描述构造提示词,然后调用 AI API 生成 SQL。
|
- SQLite 数据库本质上是本地文件,因此可以通过文件复制完成备份。
|
||||||
- 该功能涉及 API Key 管理、网络请求、调用成本和生成结果校验,因此适合作为扩展功能。
|
- 当前项目在备份前执行检查点操作,并在恢复前关闭数据库连接,使用临时文件替换目标数据库,恢复后重新打开数据库并刷新界面。
|
||||||
|
- 恢复操作前会弹出确认提示,避免误覆盖当前数据库。
|
||||||
|
|
||||||
|
8. **AI SQL 生成功能技术上可行**
|
||||||
|
- 当前项目已经能够读取数据库表和视图结构,并将结构信息与用户自然语言需求组合成提示词。
|
||||||
|
- 系统通过 JDK HttpClient 调用 OpenAI 兼容接口或 Anthropic Claude Messages API,并用 Jackson 处理 JSON 请求和响应。
|
||||||
|
- API Key 只保存在运行内存中,不写入本地文件;AI 生成的 SQL 只填入 SQL 输入框,不自动执行,降低安全风险。
|
||||||
|
|
||||||
### 技术可行性结论
|
### 技术可行性结论
|
||||||
|
|
||||||
HyperSql 所采用的核心技术已经在当前项目中得到验证。数据库连接、表结构读取、表数据分页显示、SQL 执行、数据库文件管理、表管理、数据行编辑和 SQLite 安全结构编辑功能均已实现,因此项目核心功能在技术上可行。AI SQL 生成属于后续扩展,技术上可实现,但需要在安全性和调用成本方面进行控制。
|
HyperSql 所采用的核心技术已经在当前项目中得到验证。数据库连接、表结构读取、表数据分页显示、SQL 执行、数据库文件管理、表管理、数据行编辑、SQLite 安全结构编辑、备份恢复和 AI SQL 生成均已实现,因此项目核心功能和主要扩展功能在技术上可行。
|
||||||
|
|
||||||
## 3. 经济可行性
|
## 3. 经济可行性
|
||||||
|
|
||||||
HyperSql 的开发成本较低,主要体现在以下方面:
|
HyperSql 的开发成本较低,主要体现在以下方面:
|
||||||
|
|
||||||
1. **开发工具成本低**
|
1. **开发工具成本低**
|
||||||
- Java、JavaFX、Maven、SQLite 和 SQLite JDBC 均可免费使用。
|
- Java、JavaFX、Maven、SQLite、SQLite JDBC 和 Jackson 均可免费使用。
|
||||||
- 可使用 IntelliJ IDEA Community、VS Code 等免费开发工具。
|
- 可使用 IntelliJ IDEA Community、VS Code 等免费开发工具。
|
||||||
|
|
||||||
2. **运行环境成本低**
|
2. **运行环境成本低**
|
||||||
@@ -78,12 +86,13 @@ HyperSql 的开发成本较低,主要体现在以下方面:
|
|||||||
- 当前已完成的功能不涉及商业授权费用。
|
- 当前已完成的功能不涉及商业授权费用。
|
||||||
|
|
||||||
4. **AI API 成本可控**
|
4. **AI API 成本可控**
|
||||||
- AI SQL 生成尚未实现,可以作为后续扩展功能。
|
- AI SQL 生成功能由用户自行输入 API Key,系统本身不内置付费密钥。
|
||||||
- 若后续实现,可限制调用次数,或只在演示场景使用少量 API 调用,避免高额费用。
|
- 该功能只在用户主动点击生成时调用 API,不会自动后台调用。
|
||||||
|
- 生成 SQL 时只发送数据库结构和用户需求,不发送表数据,减少请求内容规模。
|
||||||
|
|
||||||
### 经济可行性结论
|
### 经济可行性结论
|
||||||
|
|
||||||
HyperSql 不需要额外硬件和商业软件投入,当前已实现功能均基于免费技术完成,整体开发与运行成本较低,经济上可行。
|
HyperSql 不需要额外硬件和商业软件投入,当前主要功能均基于免费技术完成。AI 功能可能产生的调用费用由用户配置的 API 服务决定,并且只在用户主动使用时发生,因此整体开发与运行成本较低,经济上可行。
|
||||||
|
|
||||||
## 4. 时间可行性
|
## 4. 时间可行性
|
||||||
|
|
||||||
@@ -104,21 +113,22 @@ HyperSql 采用增量迭代模型,可以按照功能优先级逐步完成,
|
|||||||
| 交互修复 | 修复选择表不同步、刷新表列表保留当前表、SQL 结果区启动空状态 | 已完成 |
|
| 交互修复 | 修复选择表不同步、刷新表列表保留当前表、SQL 结果区启动空状态 | 已完成 |
|
||||||
| 数据行编辑 | 表数据界面新增、修改、删除数据行;可编辑单表 SQL 结果 | 已完成 |
|
| 数据行编辑 | 表数据界面新增、修改、删除数据行;可编辑单表 SQL 结果 | 已完成 |
|
||||||
| 表结构安全编辑 | 重命名表、新增字段、重命名字段 | 已完成 |
|
| 表结构安全编辑 | 重命名表、新增字段、重命名字段 | 已完成 |
|
||||||
| 备份恢复 | 数据库备份和恢复 | 待实现 |
|
| 备份恢复 | 数据库备份和从备份恢复 | 已完成 |
|
||||||
| AI SQL 生成 | 根据表结构和自然语言生成 SQL | 待实现 |
|
| 界面优化 | 整理菜单和工具栏,降低界面拥挤程度 | 已完成 |
|
||||||
|
| AI SQL 生成 | 根据表结构和自然语言生成 SQL,支持 OpenAI 兼容接口和 Anthropic Claude | 已完成 |
|
||||||
| 测试优化 | 功能测试、界面优化、最终演示准备 | 持续进行 |
|
| 测试优化 | 功能测试、界面优化、最终演示准备 | 持续进行 |
|
||||||
|
|
||||||
### 时间可行性说明
|
### 时间可行性说明
|
||||||
|
|
||||||
1. 当前项目已经完成数据库管理工具的核心可运行版本,可以满足基本演示要求。
|
1. 当前项目已经完成数据库管理工具的核心可运行版本,可以满足基本演示要求。
|
||||||
2. 数据行编辑和 SQLite 支持范围内的表结构编辑已经完成,软件实用性进一步提高。
|
2. 数据行编辑和 SQLite 支持范围内的表结构编辑已经完成,软件实用性进一步提高。
|
||||||
3. 后续开发可以优先完成备份恢复,因为它与数据库管理工具的数据安全性直接相关。
|
3. 备份恢复功能已经完成,增强了数据库文件管理和数据安全能力。
|
||||||
4. AI SQL 生成可以作为亮点功能,根据剩余时间决定实现深度。
|
4. AI SQL 生成功能已经完成基础版本,可以作为课程项目亮点功能展示。
|
||||||
5. 采用增量迭代方式后,即使扩展功能未全部完成,系统仍然具备可运行、可演示的核心功能。
|
5. 后续主要工作集中在测试、演示数据准备、界面细节优化和文档完善,风险相对较低。
|
||||||
|
|
||||||
### 时间可行性结论
|
### 时间可行性结论
|
||||||
|
|
||||||
从当前进展看,HyperSql 的核心功能和主要编辑功能已经完成,课程周期内完成一个可演示版本具有较高可行性。后续需要合理控制扩展功能范围,避免 AI 调用和复杂数据库结构重建功能占用过多开发时间。
|
从当前进展看,HyperSql 的核心功能和主要扩展功能已经完成,课程周期内形成可运行、可演示版本具有较高可行性。后续只需继续进行测试和演示准备即可。
|
||||||
|
|
||||||
## 5. 操作可行性
|
## 5. 操作可行性
|
||||||
|
|
||||||
@@ -139,34 +149,41 @@ HyperSql 面向学生和 SQLite 初学者,软件操作流程较为简单。
|
|||||||
↓
|
↓
|
||||||
在 SQLite 支持范围内编辑表结构
|
在 SQLite 支持范围内编辑表结构
|
||||||
↓
|
↓
|
||||||
在 SQL 执行区输入并执行 SQL
|
根据需要备份或恢复数据库
|
||||||
↓
|
↓
|
||||||
根据需要创建表、删除表或刷新表列表
|
在 SQL 执行区输入 SQL 或使用 AI 生成 SQL
|
||||||
|
↓
|
||||||
|
检查 SQL 后手动执行
|
||||||
```
|
```
|
||||||
|
|
||||||
### 操作可行性说明
|
### 操作可行性说明
|
||||||
|
|
||||||
1. **界面符合用户习惯**
|
1. **界面符合用户习惯**
|
||||||
- 采用类似数据库管理工具的布局:左侧显示表/视图列表,右侧显示表数据、表结构和 SQL 执行区域。
|
- 采用类似数据库管理工具的布局:左侧显示表/视图列表,右侧显示表数据、表结构和 SQL 执行区域。
|
||||||
|
- 菜单和工具栏经过整理,高频操作更明显,危险或低频操作放在菜单中,界面更清晰。
|
||||||
|
|
||||||
2. **降低数据库操作门槛**
|
2. **降低数据库操作门槛**
|
||||||
- 用户可以通过图形界面打开数据库、浏览表结构、查看表数据、新增修改删除数据行、创建表、删除表和进行部分表结构编辑,不必完全依赖命令行工具。
|
- 用户可以通过图形界面打开数据库、浏览表结构、查看表数据、新增修改删除数据行、创建表、删除表、进行部分表结构编辑、备份恢复数据库,不必完全依赖命令行工具。
|
||||||
|
|
||||||
3. **分页降低操作压力**
|
3. **分页降低操作压力**
|
||||||
- 表数据和 SQL 查询结果都使用分页显示,每页 100 行,避免一次显示大量数据导致界面卡顿或不便查看。
|
- 表数据和 SQL 查询结果都使用分页显示,每页 100 行,避免一次显示大量数据导致界面卡顿或不便查看。
|
||||||
|
|
||||||
4. **错误提示清晰**
|
4. **错误提示清晰**
|
||||||
- 当未连接数据库、数据库连接失败、SQL 执行错误、创建表失败或删除表失败时,系统会给出提示信息。
|
- 当未连接数据库、数据库连接失败、SQL 执行错误、创建表失败、删除表失败、备份恢复失败或 AI 调用失败时,系统会给出提示信息。
|
||||||
|
|
||||||
5. **危险操作有确认流程**
|
5. **危险操作有确认流程**
|
||||||
- 删除数据库文件、删除表和删除数据行都属于破坏性操作,系统在执行前会弹出确认提示。
|
- 删除数据库文件、删除表、删除数据行、恢复数据库和结构变更等操作都属于破坏性操作,系统在执行前会弹出确认提示。
|
||||||
|
|
||||||
6. **当前限制清晰**
|
6. **AI 生成 SQL 使用方式安全**
|
||||||
- 当前版本已经支持创建表、删除普通表、数据行编辑和 SQLite 原生支持的部分结构编辑,但尚未实现数据库备份恢复和 AI SQL 生成。
|
- 用户在 UI 中设置 API Key、Base URL 和模型。
|
||||||
|
- API Key 只在本次运行中保存。
|
||||||
|
- 系统只将数据库结构和用户需求发送给 AI,不发送表数据。
|
||||||
|
- AI 生成的 SQL 只填入 SQL 输入框,不自动执行,用户可以检查后再手动运行。
|
||||||
|
- 如果生成内容包含可能修改数据或结构的关键词,系统会提示用户谨慎检查。
|
||||||
|
|
||||||
### 操作可行性结论
|
### 操作可行性结论
|
||||||
|
|
||||||
HyperSql 当前功能界面直观,基本操作流程清晰,已经支持常见数据浏览、数据编辑和部分结构编辑操作,适合学生和 SQLite 初学者使用。随着后续备份恢复功能完善,软件的数据安全性会进一步提高。
|
HyperSql 当前功能界面直观,基本操作流程清晰,已经支持常见数据浏览、数据编辑、部分结构编辑、备份恢复和 AI 辅助 SQL 生成操作,适合学生和 SQLite 初学者使用。
|
||||||
|
|
||||||
## 6. 可行性分析总结
|
## 6. 可行性分析总结
|
||||||
|
|
||||||
@@ -174,11 +191,11 @@ HyperSql 当前功能界面直观,基本操作流程清晰,已经支持常
|
|||||||
|
|
||||||
| 分析方面 | 结论 |
|
| 分析方面 | 结论 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 技术可行性 | JavaFX、SQLite JDBC 和 Maven 已经支撑当前核心功能、数据编辑和结构编辑实现,AI API 可作为后续扩展 |
|
| 技术可行性 | JavaFX、SQLite JDBC、Maven、JDK HttpClient 和 Jackson 已经支撑当前核心功能、数据编辑、结构编辑、备份恢复和 AI SQL 生成实现 |
|
||||||
| 经济可行性 | 开发工具和数据库免费,当前功能无额外运行成本 |
|
| 经济可行性 | 开发工具和数据库免费,当前功能无额外固定运行成本,AI 调用成本由用户自行配置和控制 |
|
||||||
| 时间可行性 | 核心功能和主要编辑功能已经完成,后续扩展可按优先级继续迭代 |
|
| 时间可行性 | 核心功能和主要扩展功能已经完成,后续重点是测试和演示准备 |
|
||||||
| 操作可行性 | 面向学生和初学者,界面简单直观,危险操作有确认提示 |
|
| 操作可行性 | 面向学生和初学者,界面简单直观,危险操作有确认提示,AI 生成 SQL 不自动执行 |
|
||||||
|
|
||||||
### 总体结论
|
### 总体结论
|
||||||
|
|
||||||
HyperSql 的技术方案成熟,开发成本低,核心功能和主要编辑功能已经形成可运行版本,适合作为软件工程课程大作业项目继续完善。后续应优先补充数据库备份恢复功能,再根据时间实现 AI 辅助 SQL 生成。
|
HyperSql 的技术方案成熟,开发成本低,核心功能和主要扩展功能已经形成可运行版本,适合作为软件工程课程大作业项目继续完善和演示。当前项目已经具备数据库浏览、SQL 执行、数据编辑、结构编辑、备份恢复和 AI 辅助 SQL 生成等功能,整体可行性较高。
|
||||||
|
|||||||
@@ -37,6 +37,11 @@
|
|||||||
<artifactId>slf4j-simple</artifactId>
|
<artifactId>slf4j-simple</artifactId>
|
||||||
<version>2.0.17</version>
|
<version>2.0.17</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.18.3</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
+58
-30
@@ -11,10 +11,11 @@ HyperSql 是一个面向学生和 SQLite 初学者的轻量级数据库图形化
|
|||||||
3. 数据库表结构和表数据需要以更直观的方式展示。
|
3. 数据库表结构和表数据需要以更直观的方式展示。
|
||||||
4. 大表或较多查询结果需要分页显示,避免界面卡顿和阅读困难。
|
4. 大表或较多查询结果需要分页显示,避免界面卡顿和阅读困难。
|
||||||
5. 创建数据库、创建表、删除表等常见操作需要简洁清晰的图形界面。
|
5. 创建数据库、创建表、删除表等常见操作需要简洁清晰的图形界面。
|
||||||
6. 删除数据库文件、删除表和删除数据行等危险操作需要确认流程,降低误操作风险。
|
6. 删除数据库文件、删除表、删除数据行、恢复数据库等危险操作需要确认流程,降低误操作风险。
|
||||||
7. 表结构修改需要尊重 SQLite 原生能力,只提供安全可控的结构编辑入口。
|
7. 表结构修改需要尊重 SQLite 原生能力,只提供安全可控的结构编辑入口。
|
||||||
|
8. 用户希望通过 AI 根据当前数据库结构和自然语言需求辅助生成 SQL,但生成结果不应自动执行。
|
||||||
|
|
||||||
当前版本已经完成了数据库文件管理、表/视图浏览、表结构查看、表数据分页查看、SQL 执行、SQL 结果分页、创建表、删除表、数据行编辑和 SQLite 支持范围内的表结构编辑等核心功能。数据库备份恢复和 AI 辅助 SQL 生成作为后续扩展功能继续开发。
|
当前版本已经完成了数据库文件管理、表/视图浏览、表结构查看、表数据分页查看、SQL 执行、SQL 结果分页、创建表、删除表、数据行编辑、SQLite 支持范围内的表结构编辑、数据库备份恢复和 AI 辅助 SQL 生成等核心与扩展功能。
|
||||||
|
|
||||||
## 2. 用户需求分析
|
## 2. 用户需求分析
|
||||||
|
|
||||||
@@ -39,7 +40,11 @@ HyperSql 是一个面向学生和 SQLite 初学者的轻量级数据库图形化
|
|||||||
9. 用户通过图形界面新增、修改或删除表中的数据行。
|
9. 用户通过图形界面新增、修改或删除表中的数据行。
|
||||||
10. 用户在 SQLite 支持范围内重命名表、新增字段或重命名字段。
|
10. 用户在 SQLite 支持范围内重命名表、新增字段或重命名字段。
|
||||||
11. 用户刷新表列表,并希望保持当前选中的表不变。
|
11. 用户刷新表列表,并希望保持当前选中的表不变。
|
||||||
12. 后续版本中,用户可以备份恢复数据库,或使用 AI 辅助生成 SQL。
|
12. 用户将当前数据库备份为另一个文件。
|
||||||
|
13. 用户从备份文件恢复当前数据库,并在恢复前得到确认提示。
|
||||||
|
14. 用户在 UI 中配置 AI Provider、API Key、Base URL 和模型。
|
||||||
|
15. 用户输入自然语言需求,由 AI 根据当前数据库结构生成 SQL。
|
||||||
|
16. 用户检查 AI 生成的 SQL,然后手动点击执行。
|
||||||
|
|
||||||
## 3. 功能性需求
|
## 3. 功能性需求
|
||||||
|
|
||||||
@@ -56,8 +61,9 @@ HyperSql 的功能性需求主要包括以下模块:
|
|||||||
| 表结构安全编辑 | 支持重命名表、新增字段、重命名字段 | 已实现 |
|
| 表结构安全编辑 | 支持重命名表、新增字段、重命名字段 | 已实现 |
|
||||||
| 状态与错误提示 | 显示连接状态、执行结果和错误信息 | 已实现 |
|
| 状态与错误提示 | 显示连接状态、执行结果和错误信息 | 已实现 |
|
||||||
| 数据编辑 | 支持新增、修改、删除表数据行,并支持可识别单表 SQL 结果编辑 | 已实现 |
|
| 数据编辑 | 支持新增、修改、删除表数据行,并支持可识别单表 SQL 结果编辑 | 已实现 |
|
||||||
| 备份与恢复 | 支持数据库文件备份和从备份文件恢复 | 待实现 |
|
| 备份与恢复 | 支持数据库文件备份和从备份文件恢复 | 已实现 |
|
||||||
| AI SQL 生成 | 根据表结构和用户自然语言需求生成 SQL | 待实现 |
|
| AI SQL 生成 | 根据表结构和用户自然语言需求生成 SQL,支持 OpenAI 兼容接口和 Anthropic Claude | 已实现 |
|
||||||
|
| UI 优化 | 整理菜单、工具栏和侧边栏操作入口,降低界面拥挤程度 | 已实现 |
|
||||||
|
|
||||||
## 4. 核心功能需求说明
|
## 4. 核心功能需求说明
|
||||||
|
|
||||||
@@ -160,36 +166,53 @@ HyperSql 的功能性需求主要包括以下模块:
|
|||||||
|
|
||||||
### 5.2 数据库备份与恢复
|
### 5.2 数据库备份与恢复
|
||||||
|
|
||||||
- 系统后续应支持将当前数据库复制为备份文件。
|
- 系统应支持将当前数据库复制为备份文件。
|
||||||
- 系统后续应支持用户选择备份文件进行恢复。
|
- 系统应支持用户选择备份文件进行恢复。
|
||||||
- 系统应在恢复前提示用户确认,避免覆盖当前数据库。
|
- 系统应在恢复前提示用户确认,避免覆盖当前数据库。
|
||||||
|
- 系统应避免将当前数据库文件本身作为备份目标或恢复来源。
|
||||||
|
- 系统应在恢复前关闭当前数据库连接,恢复后重新打开数据库并刷新界面。
|
||||||
- 系统应显示备份或恢复的执行结果。
|
- 系统应显示备份或恢复的执行结果。
|
||||||
|
|
||||||
当前实现状态:待实现。
|
当前实现状态:已实现。
|
||||||
|
|
||||||
### 5.3 AI 辅助 SQL 生成
|
### 5.3 AI 辅助 SQL 生成
|
||||||
|
|
||||||
- 系统后续应读取当前数据库的表结构信息。
|
- 系统应读取当前数据库的表结构和视图结构信息。
|
||||||
- 系统后续应根据表结构自动生成 AI 提示词。
|
- 系统应根据表结构自动生成 AI 提示词。
|
||||||
- 用户可以输入自然语言需求。
|
- 用户可以输入自然语言需求。
|
||||||
|
- 系统支持 OpenAI 兼容接口和 Anthropic Claude。
|
||||||
|
- 用户应能在 UI 中设置 Provider、API Key、Base URL、模型和超时时间。
|
||||||
|
- API Key 只应保存在本次运行内存中,不应写入本地配置文件。
|
||||||
- 系统调用 AI API 生成 SQL 语句。
|
- 系统调用 AI API 生成 SQL 语句。
|
||||||
- 用户可以检查并确认生成的 SQL 后再执行。
|
- 用户可以检查并确认生成的 SQL 后再执行。
|
||||||
|
- AI 生成的 SQL 不应直接自动执行,应由用户确认后手动执行。
|
||||||
|
- 系统不应向 AI 发送表数据,只发送数据库结构和用户需求。
|
||||||
- 若 AI 调用失败,系统应显示错误提示。
|
- 若 AI 调用失败,系统应显示错误提示。
|
||||||
- AI 生成的 SQL 不应直接自动执行,应由用户确认后执行。
|
- 若生成 SQL 包含可能修改数据或结构的关键词,系统应提示用户谨慎检查。
|
||||||
|
|
||||||
当前实现状态:待实现。
|
当前实现状态:已实现。
|
||||||
|
|
||||||
|
### 5.4 界面整理优化
|
||||||
|
|
||||||
|
- 系统菜单应包含完整功能入口。
|
||||||
|
- 工具栏应保留高频操作,避免按钮过多导致界面拥挤。
|
||||||
|
- 表相关操作应放在侧边栏附近,方便用户针对当前表操作。
|
||||||
|
- 危险或低频操作应主要放在菜单中,减少误触。
|
||||||
|
|
||||||
|
当前实现状态:已实现。
|
||||||
|
|
||||||
## 6. 非功能性需求
|
## 6. 非功能性需求
|
||||||
|
|
||||||
| 需求类型 | 说明 | 当前体现 |
|
| 需求类型 | 说明 | 当前体现 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 易用性 | 界面清晰,操作流程简单,适合初学者使用 | 已采用菜单、工具栏、表格、创建表对话框、数据行编辑按钮和结构编辑按钮 |
|
| 易用性 | 界面清晰,操作流程简单,适合初学者使用 | 已采用菜单、工具栏、侧边栏、表格、创建表对话框、数据行编辑按钮、结构编辑按钮和 AI 生成入口 |
|
||||||
| 可靠性 | 数据库连接、SQL 执行、数据编辑和表操作失败时应有错误提示 | 已实现基础错误提示 |
|
| 可靠性 | 数据库连接、SQL 执行、数据编辑、表操作、备份恢复和 AI 调用失败时应有错误提示 | 已实现基础错误提示 |
|
||||||
| 安全性 | 删除数据库、删除表、删除数据行和结构变更等操作前应确认 | 已实现确认流程 |
|
| 安全性 | 删除数据库、删除表、删除数据行、恢复数据库和结构变更等操作前应确认;AI 生成 SQL 不自动执行 | 已实现确认流程和 AI 生成后手动执行机制 |
|
||||||
| 性能 | 对常见小型 SQLite 数据库能够快速打开和查询 | 表数据采用数据库侧分页,降低大表加载压力 |
|
| 性能 | 对常见小型 SQLite 数据库能够快速打开和查询 | 表数据采用数据库侧分页,降低大表加载压力 |
|
||||||
| 可维护性 | 采用模块化设计,便于后续扩展和维护 | 已划分数据库连接、元数据读取、SQL 执行、行数据服务、结构编辑服务、工具类和控制器 |
|
| 隐私性 | AI 功能不应发送表数据,API Key 不应持久化到本地文件 | AI 提示只包含表/视图结构和用户需求,API Key 仅保存在运行内存中 |
|
||||||
|
| 可维护性 | 采用模块化设计,便于后续扩展和维护 | 已划分数据库连接、元数据读取、SQL 执行、行数据服务、结构编辑服务、备份恢复逻辑、AI 客户端和工具类 |
|
||||||
| 兼容性 | 支持常见 SQLite 数据库文件,适配主流桌面系统 | 基于 JavaFX 和 SQLite JDBC,具备跨平台基础 |
|
| 兼容性 | 支持常见 SQLite 数据库文件,适配主流桌面系统 | 基于 JavaFX 和 SQLite JDBC,具备跨平台基础 |
|
||||||
| 可扩展性 | 后续可扩展备份恢复和 AI SQL 生成 | 已保留 SQL 执行、元数据读取、行数据服务和结构编辑服务基础能力 |
|
| 可扩展性 | 后续可扩展更多 AI Provider、SQL 历史记录和界面美化 | 已提供 AI Provider 适配层和独立 AI SQL 生成服务 |
|
||||||
|
|
||||||
## 7. 需求优先级
|
## 7. 需求优先级
|
||||||
|
|
||||||
@@ -202,8 +225,9 @@ HyperSql 的功能性需求主要包括以下模块:
|
|||||||
| 中 | 创建表、删除表、删除数据库文件 | 已实现 |
|
| 中 | 创建表、删除表、删除数据库文件 | 已实现 |
|
||||||
| 中 | 数据行新增、修改、删除 | 已实现 |
|
| 中 | 数据行新增、修改、删除 | 已实现 |
|
||||||
| 中 | SQLite 支持范围内的表结构编辑 | 已实现 |
|
| 中 | SQLite 支持范围内的表结构编辑 | 已实现 |
|
||||||
| 中 | 数据库备份与恢复 | 待实现 |
|
| 中 | 数据库备份与恢复 | 已实现 |
|
||||||
| 低 | AI 辅助 SQL 生成、SQL 历史记录、界面进一步美化 | 待实现 |
|
| 低 | AI 辅助 SQL 生成、界面进一步美化 | 已实现 |
|
||||||
|
| 低 | SQL 历史记录、更多数据库类型支持 | 后续可扩展 |
|
||||||
|
|
||||||
### 优先级说明
|
### 优先级说明
|
||||||
|
|
||||||
@@ -213,23 +237,24 @@ HyperSql 的功能性需求主要包括以下模块:
|
|||||||
|
|
||||||
2. **中优先级需求**
|
2. **中优先级需求**
|
||||||
- 提升软件实用性和数据安全性。
|
- 提升软件实用性和数据安全性。
|
||||||
- 当前已经完成创建表、删除表、删除数据库文件、数据行编辑和表结构安全编辑,后续应继续完成备份恢复。
|
- 当前已经完成创建表、删除表、删除数据库文件、数据行编辑、表结构安全编辑和备份恢复。
|
||||||
|
|
||||||
3. **低优先级需求**
|
3. **低优先级需求**
|
||||||
- 作为项目亮点和扩展功能。
|
- 作为项目亮点和扩展功能。
|
||||||
- 可根据开发进度调整实现程度。
|
- 当前已经完成 AI 辅助 SQL 生成和基础界面整理,后续可继续扩展 SQL 历史记录或更多界面美化。
|
||||||
|
|
||||||
## 8. 当前版本限制
|
## 8. 当前版本限制
|
||||||
|
|
||||||
当前 HyperSql 仍存在以下限制:
|
当前 HyperSql 仍存在以下限制:
|
||||||
|
|
||||||
1. 还没有实现数据库备份与恢复功能。
|
1. SQL 查询结果分页属于客户端分页,查询结果会先全部读取到内存中,不适合特别大的查询结果。
|
||||||
2. 还没有实现 AI 辅助 SQL 生成功能。
|
2. SQL 结果编辑只支持可识别的单表查询,JOIN、聚合、表达式等复杂结果保持只读。
|
||||||
3. SQL 查询结果分页属于客户端分页,查询结果会先全部读取到内存中,不适合特别大的查询结果。
|
3. 表结构编辑只支持 SQLite 原生安全支持的重命名表、新增字段和重命名字段,不支持需要重建表的复杂结构修改。
|
||||||
4. SQL 结果编辑只支持可识别的单表查询,JOIN、聚合、表达式等复杂结果保持只读。
|
4. 当前删除表功能只支持删除普通表,不支持删除视图。
|
||||||
5. 表结构编辑只支持 SQLite 原生安全支持的重命名表、新增字段和重命名字段,不支持需要重建表的复杂结构修改。
|
5. 当前主要面向 SQLite,不支持 MySQL、PostgreSQL 等远程数据库。
|
||||||
6. 当前删除表功能只支持删除普通表,不支持删除视图。
|
6. AI 生成 SQL 的质量取决于用户配置的模型和 API 服务,生成结果仍需要用户检查后再执行。
|
||||||
7. 当前主要面向 SQLite,不支持 MySQL、PostgreSQL 等远程数据库。
|
7. API Key 只在本次运行中保存,关闭软件后需要重新输入。
|
||||||
|
8. 当前未实现 SQL 历史记录功能。
|
||||||
|
|
||||||
## 9. 需求分析总结
|
## 9. 需求分析总结
|
||||||
|
|
||||||
@@ -246,6 +271,9 @@ HyperSql 的功能性需求主要包括以下模块:
|
|||||||
7. 删除普通表。
|
7. 删除普通表。
|
||||||
8. 数据行新增、修改和删除。
|
8. 数据行新增、修改和删除。
|
||||||
9. SQLite 支持范围内的表结构安全编辑。
|
9. SQLite 支持范围内的表结构安全编辑。
|
||||||
10. 状态提示、错误提示和危险操作确认。
|
10. 数据库备份和从备份恢复。
|
||||||
|
11. AI 辅助 SQL 生成。
|
||||||
|
12. 状态提示、错误提示和危险操作确认。
|
||||||
|
13. 菜单、工具栏和侧边栏入口整理。
|
||||||
|
|
||||||
后续开发应优先完成数据库备份恢复,使系统从“可浏览、可执行 SQL、可编辑数据、可管理表结构”进一步发展为“可完整管理 SQLite 数据库”的工具。在时间允许的情况下,再实现 AI 辅助 SQL 生成作为项目亮点。
|
当前 HyperSql 已经从“可浏览、可执行 SQL、可编辑数据、可管理表结构”进一步发展为“可较完整管理 SQLite 数据库,并提供 AI 辅助 SQL 生成”的轻量级数据库管理工具。后续可以围绕测试、演示体验、SQL 历史记录和更细致的界面美化继续迭代。
|
||||||
|
|||||||
+18
-10
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
增量迭代模型是指将软件系统划分为多个相对独立的功能增量,每个增量都经过需求分析、设计、编码、测试和反馈优化等过程,最终逐步形成完整的软件系统。
|
增量迭代模型是指将软件系统划分为多个相对独立的功能增量,每个增量都经过需求分析、设计、编码、测试和反馈优化等过程,最终逐步形成完整的软件系统。
|
||||||
|
|
||||||
HyperSql 是一个基于 Java 24、JavaFX 和 Maven 的轻量级 SQLite 图形化管理工具。项目开发过程中,先完成数据库连接、表结构浏览、表数据查看和 SQL 执行等核心功能,再逐步增加数据库文件管理、表管理、分页显示、数据行编辑和表结构安全编辑等增强功能,最后根据时间继续扩展备份恢复和 AI 辅助 SQL 生成。
|
HyperSql 是一个基于 Java 24、JavaFX 和 Maven 的轻量级 SQLite 图形化管理工具。项目开发过程中,先完成数据库连接、表结构浏览、表数据查看和 SQL 执行等核心功能,再逐步增加数据库文件管理、表管理、分页显示、数据行编辑、表结构安全编辑、备份恢复、界面优化和 AI 辅助 SQL 生成等增强功能。
|
||||||
|
|
||||||
## 2. 选择增量迭代模型的原因
|
## 2. 选择增量迭代模型的原因
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ HyperSql 选择增量迭代模型,主要有以下原因:
|
|||||||
|
|
||||||
3. **降低开发风险**
|
3. **降低开发风险**
|
||||||
- 先完成基础功能,再开发复杂功能,可以避免一开始实现过多内容导致项目失控。
|
- 先完成基础功能,再开发复杂功能,可以避免一开始实现过多内容导致项目失控。
|
||||||
|
- 对 SQLite 表结构编辑和 AI SQL 生成这类有安全边界的功能,可以在后续迭代中逐步控制范围。
|
||||||
|
|
||||||
4. **便于阶段性展示**
|
4. **便于阶段性展示**
|
||||||
- 每个迭代版本都能形成可运行的软件,方便课程汇报、阶段检查和最终演示。
|
- 每个迭代版本都能形成可运行的软件,方便课程汇报、阶段检查和最终演示。
|
||||||
@@ -38,12 +39,13 @@ HyperSql 选择增量迭代模型,主要有以下原因:
|
|||||||
| V1.3 | 分页显示优化 | 表数据按每页 100 行分页显示;SQL 查询结果区按每页 100 行分页显示 | 已完成 |
|
| V1.3 | 分页显示优化 | 表数据按每页 100 行分页显示;SQL 查询结果区按每页 100 行分页显示 | 已完成 |
|
||||||
| V1.4 | 数据库文件管理增强 | 新建 SQLite 数据库文件、删除当前数据库文件 | 已完成 |
|
| V1.4 | 数据库文件管理增强 | 新建 SQLite 数据库文件、删除当前数据库文件 | 已完成 |
|
||||||
| V1.5 | 表管理功能 | 通过简洁 UI 创建表;删除选中普通表;删除前进行确认 | 已完成 |
|
| V1.5 | 表管理功能 | 通过简洁 UI 创建表;删除选中普通表;删除前进行确认 | 已完成 |
|
||||||
| V1.6 | 交互与缺陷修复 | 修复表/视图选择时数据与结构不同步问题;刷新表列表时保留当前选中表 | 已完成 |
|
| V1.6 | 交互与缺陷修复 | 修复表/视图选择时数据与结构不同步问题;刷新表列表时保留当前选中表;SQL 结果区启动时保持空状态 | 已完成 |
|
||||||
| V1.7 | 数据行增删改查 | 在表数据界面新增、修改、删除数据行;在可识别单表 SQL 结果中支持编辑 | 已完成 |
|
| V1.7 | 数据行增删改查 | 在表数据界面新增、修改、删除数据行;在可识别单表 SQL 结果中支持编辑 | 已完成 |
|
||||||
| V1.8 | 表结构安全编辑 | 支持 SQLite 原生安全的重命名表、新增字段、重命名字段 | 已完成 |
|
| V1.8 | 表结构安全编辑 | 支持 SQLite 原生安全的重命名表、新增字段、重命名字段 | 已完成 |
|
||||||
| V1.9 | 备份与恢复 | 数据库文件备份、从备份恢复数据库 | 待实现 |
|
| V1.9 | 备份与恢复 | 数据库文件备份、从备份恢复数据库,恢复前进行确认并重新打开数据库 | 已完成 |
|
||||||
| V2.0 | AI 辅助 SQL 生成 | 根据表结构和用户自然语言需求调用 AI API 生成 SQL | 待实现 |
|
| V2.0 | 界面整理优化 | 整理菜单和工具栏,将高频操作保留在工具栏,将危险或低频操作放入菜单 | 已完成 |
|
||||||
| V2.1 | 测试与优化 | 完善异常处理、界面优化、系统测试和最终演示准备 | 持续进行 |
|
| V2.1 | AI 辅助 SQL 生成 | 根据表结构和用户自然语言需求调用 OpenAI 兼容接口或 Anthropic Claude 生成 SQL | 已完成 |
|
||||||
|
| V2.2 | 测试与优化 | 编译验证、启动验证、功能回归和最终演示准备 | 持续进行 |
|
||||||
|
|
||||||
## 4. 单次迭代流程
|
## 4. 单次迭代流程
|
||||||
|
|
||||||
@@ -69,15 +71,18 @@ HyperSql 选择增量迭代模型,主要有以下原因:
|
|||||||
|
|
||||||
1. **需求细化**
|
1. **需求细化**
|
||||||
- 明确本次迭代要完成的功能和边界。例如创建表功能要求有简洁易用的 UI,而不是只让用户手写 SQL。
|
- 明确本次迭代要完成的功能和边界。例如创建表功能要求有简洁易用的 UI,而不是只让用户手写 SQL。
|
||||||
|
- 对 AI SQL 生成明确了 API Key 只在本次运行中保存,同时支持 OpenAI 兼容接口和 Anthropic Claude。
|
||||||
|
|
||||||
2. **界面与功能设计**
|
2. **界面与功能设计**
|
||||||
- 设计 JavaFX 界面布局和功能交互流程。例如主界面采用左侧表/视图列表、右侧表数据/表结构/SQL 执行区域的布局。
|
- 设计 JavaFX 界面布局和功能交互流程。例如主界面采用左侧表/视图列表、右侧表数据/表结构/SQL 执行区域的布局。
|
||||||
|
- 对菜单和工具栏进行整理,保留常用操作入口,减少界面拥挤。
|
||||||
|
|
||||||
3. **编码实现**
|
3. **编码实现**
|
||||||
- 使用 Java 24、JavaFX、SQLite JDBC 和 Maven 完成功能开发。
|
- 使用 Java 24、JavaFX、SQLite JDBC 和 Maven 完成功能开发。
|
||||||
|
- 使用 JDK 自带 HttpClient 和 Jackson 实现 AI API 调用与 JSON 解析。
|
||||||
|
|
||||||
4. **功能测试**
|
4. **功能测试**
|
||||||
- 测试数据库连接、表结构读取、分页显示、SQL 执行、创建数据库、删除数据库、创建表和删除表等功能是否正确。
|
- 测试数据库连接、表结构读取、分页显示、SQL 执行、创建数据库、删除数据库、创建表、删除表、备份恢复、AI 设置和 AI SQL 生成等功能是否正确。
|
||||||
|
|
||||||
5. **问题修复**
|
5. **问题修复**
|
||||||
- 根据测试结果修复异常、界面错误和逻辑问题。例如修复刷新表列表后错误切换到第一张表的问题。
|
- 根据测试结果修复异常、界面错误和逻辑问题。例如修复刷新表列表后错误切换到第一张表的问题。
|
||||||
@@ -107,11 +112,14 @@ HyperSql 选择增量迭代模型,主要有以下原因:
|
|||||||
5. **逐步扩展编辑能力**
|
5. **逐步扩展编辑能力**
|
||||||
- 在只读浏览功能完成后,继续实现了数据行新增、修改、删除,并基于 SQLite 原生 `ALTER TABLE` 能力实现了重命名表、新增字段、重命名字段。
|
- 在只读浏览功能完成后,继续实现了数据行新增、修改、删除,并基于 SQLite 原生 `ALTER TABLE` 能力实现了重命名表、新增字段、重命名字段。
|
||||||
|
|
||||||
6. **保留后续扩展空间**
|
6. **完善数据安全能力**
|
||||||
- 数据库备份恢复和 AI SQL 生成尚未完成,后续可作为新的增量继续实现。
|
- 在数据库文件管理基础上增加备份和恢复功能,恢复前进行确认,并在恢复后重新打开数据库,降低数据丢失风险。
|
||||||
|
|
||||||
|
7. **增加智能辅助能力**
|
||||||
|
- 在 SQL 执行功能稳定后,增加 AI 辅助 SQL 生成。系统只发送数据库结构,不发送表数据;生成的 SQL 只填入输入框,不自动执行,由用户检查后手动运行。
|
||||||
|
|
||||||
## 6. 总结
|
## 6. 总结
|
||||||
|
|
||||||
HyperSql 采用增量迭代模型进行开发是合适的。当前项目已经完成了 SQLite 数据库管理工具的核心可运行版本,包括数据库文件管理、表/视图浏览、表结构查看、表数据分页查看、SQL 执行分页显示、创建表、删除表、数据行增删改查和 SQLite 支持范围内的表结构安全编辑等功能。
|
HyperSql 采用增量迭代模型进行开发是合适的。当前项目已经完成了 SQLite 数据库管理工具的核心可运行版本,包括数据库文件管理、表/视图浏览、表结构查看、表数据分页查看、SQL 执行分页显示、创建表、删除表、数据行增删改查、SQLite 支持范围内的表结构安全编辑、数据库备份恢复和 AI 辅助 SQL 生成等功能。
|
||||||
|
|
||||||
后续开发应继续沿用增量迭代方式,优先完善数据库备份恢复等实用功能,再根据课程进度实现 AI 辅助 SQL 生成等扩展功能。
|
后续开发可继续沿用增量迭代方式,重点进行功能测试、异常场景验证、界面细节优化和最终课程演示材料整理。
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.hypersql.ai;
|
||||||
|
|
||||||
|
public class AiGenerationException extends Exception {
|
||||||
|
public AiGenerationException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AiGenerationException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.hypersql.ai;
|
||||||
|
|
||||||
|
public enum AiProvider {
|
||||||
|
OPENAI_COMPATIBLE("OpenAI 兼容"),
|
||||||
|
ANTHROPIC("Anthropic Claude");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
AiProvider(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.hypersql.ai;
|
||||||
|
|
||||||
|
public record AiSettings(
|
||||||
|
AiProvider provider,
|
||||||
|
String apiKey,
|
||||||
|
String baseUrl,
|
||||||
|
String model,
|
||||||
|
int timeoutSeconds
|
||||||
|
) {
|
||||||
|
public AiSettings {
|
||||||
|
if (provider == null) {
|
||||||
|
throw new IllegalArgumentException("请选择 AI Provider。");
|
||||||
|
}
|
||||||
|
apiKey = normalize(apiKey);
|
||||||
|
baseUrl = normalize(baseUrl);
|
||||||
|
model = normalize(model);
|
||||||
|
if (timeoutSeconds <= 0) {
|
||||||
|
throw new IllegalArgumentException("超时时间必须大于 0 秒。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasApiKey() {
|
||||||
|
return apiKey != null && !apiKey.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalize(String value) {
|
||||||
|
return value == null ? "" : value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.hypersql.ai;
|
||||||
|
|
||||||
|
import com.hypersql.db.SchemaDescriptionService;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class AiSqlGenerationService {
|
||||||
|
private final SchemaDescriptionService schemaDescriptionService = new SchemaDescriptionService();
|
||||||
|
private final LlmClient openAiClient = new OpenAiCompatibleClient();
|
||||||
|
private final LlmClient anthropicClient = new AnthropicMessagesClient();
|
||||||
|
private final GeneratedSqlSanitizer sanitizer = new GeneratedSqlSanitizer();
|
||||||
|
|
||||||
|
public String generateSql(Connection connection, AiSettings settings, String naturalLanguagePrompt) throws AiGenerationException {
|
||||||
|
validate(settings, naturalLanguagePrompt);
|
||||||
|
try {
|
||||||
|
String schemaDescription = schemaDescriptionService.describe(connection);
|
||||||
|
String rawSql = client(settings.provider()).generateSql(systemPrompt(), userPrompt(schemaDescription, naturalLanguagePrompt), settings);
|
||||||
|
return sanitizer.sanitize(rawSql);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new AiGenerationException("读取数据库结构失败。", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean containsDestructiveKeyword(String sql) {
|
||||||
|
return sanitizer.containsDestructiveKeyword(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validate(AiSettings settings, String naturalLanguagePrompt) throws AiGenerationException {
|
||||||
|
if (settings == null || !settings.hasApiKey()) {
|
||||||
|
throw new AiGenerationException("请先在 AI 设置中输入 API Key。");
|
||||||
|
}
|
||||||
|
if (settings.baseUrl().isBlank()) {
|
||||||
|
throw new AiGenerationException("请先设置 AI Base URL。");
|
||||||
|
}
|
||||||
|
if (settings.model().isBlank()) {
|
||||||
|
throw new AiGenerationException("请先设置 AI Model。");
|
||||||
|
}
|
||||||
|
if (naturalLanguagePrompt == null || naturalLanguagePrompt.isBlank()) {
|
||||||
|
throw new AiGenerationException("请输入自然语言需求。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LlmClient client(AiProvider provider) {
|
||||||
|
return switch (provider) {
|
||||||
|
case OPENAI_COMPATIBLE -> openAiClient;
|
||||||
|
case ANTHROPIC -> anthropicClient;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String systemPrompt() {
|
||||||
|
return """
|
||||||
|
你是 SQLite SQL 生成助手。
|
||||||
|
只能根据提供的数据库结构生成 SQLite SQL。
|
||||||
|
只返回一条 SQL 语句。
|
||||||
|
不要使用 Markdown 代码块。
|
||||||
|
不要解释。
|
||||||
|
除非用户明确要求修改数据或结构,否则优先生成 SELECT 查询。
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String userPrompt(String schemaDescription, String naturalLanguagePrompt) {
|
||||||
|
return """
|
||||||
|
当前数据库结构如下:
|
||||||
|
%s
|
||||||
|
|
||||||
|
用户需求:
|
||||||
|
%s
|
||||||
|
|
||||||
|
请根据上述结构生成一条 SQLite SQL。生成结果会展示给用户检查,不会自动执行。
|
||||||
|
""".formatted(schemaDescription, naturalLanguagePrompt.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package com.hypersql.ai;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
public final class AnthropicMessagesClient implements LlmClient {
|
||||||
|
private static final int MAX_TOKENS = 1200;
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateSql(String systemPrompt, String userPrompt, AiSettings settings) throws AiGenerationException {
|
||||||
|
try {
|
||||||
|
String requestBody = buildRequestBody(systemPrompt, userPrompt, settings);
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(endpoint(settings.baseUrl()))
|
||||||
|
.timeout(Duration.ofSeconds(settings.timeoutSeconds()))
|
||||||
|
.header("x-api-key", settings.apiKey())
|
||||||
|
.header("anthropic-version", "2023-06-01")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(settings.timeoutSeconds()))
|
||||||
|
.build()
|
||||||
|
.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||||
|
throw new AiGenerationException("AI 调用失败,HTTP 状态码:" + response.statusCode());
|
||||||
|
}
|
||||||
|
return parseResponse(response.body());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AiGenerationException("AI 返回内容解析失败。", e);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new AiGenerationException("AI 调用已中断。", e);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new AiGenerationException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildRequestBody(String systemPrompt, String userPrompt, AiSettings settings) throws IOException {
|
||||||
|
ObjectNode root = objectMapper.createObjectNode();
|
||||||
|
root.put("model", settings.model());
|
||||||
|
root.put("max_tokens", MAX_TOKENS);
|
||||||
|
root.put("system", systemPrompt);
|
||||||
|
ArrayNode messages = root.putArray("messages");
|
||||||
|
messages.addObject()
|
||||||
|
.put("role", "user")
|
||||||
|
.put("content", userPrompt);
|
||||||
|
return objectMapper.writeValueAsString(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseResponse(String responseBody) throws IOException, AiGenerationException {
|
||||||
|
JsonNode root = objectMapper.readTree(responseBody);
|
||||||
|
JsonNode content = root.path("content");
|
||||||
|
StringBuilder text = new StringBuilder();
|
||||||
|
if (content.isArray()) {
|
||||||
|
for (JsonNode item : content) {
|
||||||
|
if ("text".equals(item.path("type").asText()) && item.path("text").isTextual()) {
|
||||||
|
text.append(item.path("text").asText()).append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String result = text.toString().trim();
|
||||||
|
if (result.isBlank()) {
|
||||||
|
throw new AiGenerationException("AI 没有返回可用的 SQL 内容。");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private URI endpoint(String baseUrl) {
|
||||||
|
String normalized = trimTrailingSlash(baseUrl);
|
||||||
|
if (normalized.endsWith("/v1")) {
|
||||||
|
normalized += "/messages";
|
||||||
|
} else if (!normalized.endsWith("/v1/messages")) {
|
||||||
|
normalized += "/v1/messages";
|
||||||
|
}
|
||||||
|
return URI.create(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimTrailingSlash(String value) {
|
||||||
|
String result = value == null ? "" : value.trim();
|
||||||
|
while (result.endsWith("/")) {
|
||||||
|
result = result.substring(0, result.length() - 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.hypersql.ai;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public final class GeneratedSqlSanitizer {
|
||||||
|
private static final Pattern FENCED_BLOCK = Pattern.compile("(?is)```(?:sql)?\\s*(.*?)\\s*```");
|
||||||
|
private static final Pattern SQL_START = Pattern.compile("(?is)\\b(SELECT|WITH|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|REPLACE|PRAGMA)\\b");
|
||||||
|
private static final Pattern DESTRUCTIVE_KEYWORD = Pattern.compile("(?is)\\b(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|REPLACE)\\b");
|
||||||
|
|
||||||
|
public String sanitize(String rawContent) throws AiGenerationException {
|
||||||
|
if (rawContent == null || rawContent.isBlank()) {
|
||||||
|
throw new AiGenerationException("AI 没有返回可用的 SQL 内容。");
|
||||||
|
}
|
||||||
|
|
||||||
|
String text = extractCodeBlock(rawContent.trim()).trim();
|
||||||
|
Matcher startMatcher = SQL_START.matcher(text);
|
||||||
|
if (startMatcher.find()) {
|
||||||
|
text = text.substring(startMatcher.start()).trim();
|
||||||
|
}
|
||||||
|
text = firstStatement(text).trim();
|
||||||
|
if (text.isBlank()) {
|
||||||
|
throw new AiGenerationException("AI 生成的 SQL 为空。");
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean containsDestructiveKeyword(String sql) {
|
||||||
|
if (sql == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return DESTRUCTIVE_KEYWORD.matcher(sql.toUpperCase(Locale.ROOT)).find();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractCodeBlock(String text) {
|
||||||
|
Matcher matcher = FENCED_BLOCK.matcher(text);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return matcher.group(1);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstStatement(String text) {
|
||||||
|
boolean inSingleQuote = false;
|
||||||
|
boolean inDoubleQuote = false;
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
char current = text.charAt(i);
|
||||||
|
if (current == '\'' && !inDoubleQuote) {
|
||||||
|
if (inSingleQuote && i + 1 < text.length() && text.charAt(i + 1) == '\'') {
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
inSingleQuote = !inSingleQuote;
|
||||||
|
}
|
||||||
|
} else if (current == '"' && !inSingleQuote) {
|
||||||
|
if (inDoubleQuote && i + 1 < text.length() && text.charAt(i + 1) == '"') {
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
inDoubleQuote = !inDoubleQuote;
|
||||||
|
}
|
||||||
|
} else if (current == ';' && !inSingleQuote && !inDoubleQuote) {
|
||||||
|
return text.substring(0, i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.hypersql.ai;
|
||||||
|
|
||||||
|
public interface LlmClient {
|
||||||
|
String generateSql(String systemPrompt, String userPrompt, AiSettings settings) throws AiGenerationException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.hypersql.ai;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
public final class OpenAiCompatibleClient implements LlmClient {
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generateSql(String systemPrompt, String userPrompt, AiSettings settings) throws AiGenerationException {
|
||||||
|
try {
|
||||||
|
String requestBody = buildRequestBody(systemPrompt, userPrompt, settings);
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(endpoint(settings.baseUrl()))
|
||||||
|
.timeout(Duration.ofSeconds(settings.timeoutSeconds()))
|
||||||
|
.header("Authorization", "Bearer " + settings.apiKey())
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(settings.timeoutSeconds()))
|
||||||
|
.build()
|
||||||
|
.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||||
|
throw new AiGenerationException("AI 调用失败,HTTP 状态码:" + response.statusCode());
|
||||||
|
}
|
||||||
|
return parseResponse(response.body());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AiGenerationException("AI 返回内容解析失败。", e);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new AiGenerationException("AI 调用已中断。", e);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new AiGenerationException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildRequestBody(String systemPrompt, String userPrompt, AiSettings settings) throws IOException {
|
||||||
|
ObjectNode root = objectMapper.createObjectNode();
|
||||||
|
root.put("model", settings.model());
|
||||||
|
ArrayNode messages = root.putArray("messages");
|
||||||
|
messages.addObject()
|
||||||
|
.put("role", "system")
|
||||||
|
.put("content", systemPrompt);
|
||||||
|
messages.addObject()
|
||||||
|
.put("role", "user")
|
||||||
|
.put("content", userPrompt);
|
||||||
|
return objectMapper.writeValueAsString(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseResponse(String responseBody) throws IOException, AiGenerationException {
|
||||||
|
JsonNode root = objectMapper.readTree(responseBody);
|
||||||
|
JsonNode content = root.path("choices").path(0).path("message").path("content");
|
||||||
|
if (!content.isTextual() || content.asText().isBlank()) {
|
||||||
|
throw new AiGenerationException("AI 没有返回可用的 SQL 内容。");
|
||||||
|
}
|
||||||
|
return content.asText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private URI endpoint(String baseUrl) {
|
||||||
|
String normalized = trimTrailingSlash(baseUrl);
|
||||||
|
if (!normalized.endsWith("/chat/completions")) {
|
||||||
|
normalized += "/chat/completions";
|
||||||
|
}
|
||||||
|
return URI.create(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimTrailingSlash(String value) {
|
||||||
|
String result = value == null ? "" : value.trim();
|
||||||
|
while (result.endsWith("/")) {
|
||||||
|
result = result.substring(0, result.length() - 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
package com.hypersql.controller;
|
package com.hypersql.controller;
|
||||||
|
|
||||||
|
import com.hypersql.ai.AiGenerationException;
|
||||||
|
import com.hypersql.ai.AiProvider;
|
||||||
|
import com.hypersql.ai.AiSettings;
|
||||||
|
import com.hypersql.ai.AiSqlGenerationService;
|
||||||
import com.hypersql.db.DatabaseConnectionManager;
|
import com.hypersql.db.DatabaseConnectionManager;
|
||||||
import com.hypersql.db.DatabaseMetadataService;
|
import com.hypersql.db.DatabaseMetadataService;
|
||||||
import com.hypersql.db.SqlExecutionService;
|
import com.hypersql.db.SqlExecutionService;
|
||||||
@@ -17,6 +21,7 @@ import javafx.beans.property.SimpleStringProperty;
|
|||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
@@ -26,6 +31,7 @@ import javafx.scene.control.ComboBox;
|
|||||||
import javafx.scene.control.Dialog;
|
import javafx.scene.control.Dialog;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.ListView;
|
import javafx.scene.control.ListView;
|
||||||
|
import javafx.scene.control.PasswordField;
|
||||||
import javafx.scene.control.TabPane;
|
import javafx.scene.control.TabPane;
|
||||||
import javafx.scene.control.TableColumn;
|
import javafx.scene.control.TableColumn;
|
||||||
import javafx.scene.control.TableView;
|
import javafx.scene.control.TableView;
|
||||||
@@ -43,8 +49,10 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -57,6 +65,7 @@ import java.util.stream.Collectors;
|
|||||||
public class MainController {
|
public class MainController {
|
||||||
private static final int TABLE_PAGE_SIZE = 100;
|
private static final int TABLE_PAGE_SIZE = 100;
|
||||||
private static final int SQL_PAGE_SIZE = 100;
|
private static final int SQL_PAGE_SIZE = 100;
|
||||||
|
private static final int DEFAULT_AI_TIMEOUT_SECONDS = 30;
|
||||||
private static final List<String> ALLOWED_COLUMN_TYPES = List.of(
|
private static final List<String> ALLOWED_COLUMN_TYPES = List.of(
|
||||||
"INTEGER", "TEXT", "REAL", "BLOB", "NUMERIC", "BOOLEAN", "DATE", "DATETIME"
|
"INTEGER", "TEXT", "REAL", "BLOB", "NUMERIC", "BOOLEAN", "DATE", "DATETIME"
|
||||||
);
|
);
|
||||||
@@ -134,12 +143,14 @@ public class MainController {
|
|||||||
private final SqlExecutionService sqlExecutionService = new SqlExecutionService();
|
private final SqlExecutionService sqlExecutionService = new SqlExecutionService();
|
||||||
private final RowDataService rowDataService = new RowDataService();
|
private final RowDataService rowDataService = new RowDataService();
|
||||||
private final SchemaEditService schemaEditService = new SchemaEditService();
|
private final SchemaEditService schemaEditService = new SchemaEditService();
|
||||||
|
private final AiSqlGenerationService aiSqlGenerationService = new AiSqlGenerationService();
|
||||||
private TableInfo currentTable;
|
private TableInfo currentTable;
|
||||||
private long currentTableTotalRows;
|
private long currentTableTotalRows;
|
||||||
private long currentTablePageIndex;
|
private long currentTablePageIndex;
|
||||||
private EditableQueryResult currentTableResult;
|
private EditableQueryResult currentTableResult;
|
||||||
private QueryResult currentSqlResult;
|
private QueryResult currentSqlResult;
|
||||||
private EditableQueryResult currentEditableSqlResult;
|
private EditableQueryResult currentEditableSqlResult;
|
||||||
|
private AiSettings aiSettings;
|
||||||
private String currentSqlText;
|
private String currentSqlText;
|
||||||
private long currentSqlPageIndex;
|
private long currentSqlPageIndex;
|
||||||
private boolean selectingTableProgrammatically;
|
private boolean selectingTableProgrammatically;
|
||||||
@@ -168,10 +179,7 @@ public class MainController {
|
|||||||
FileChooser fileChooser = new FileChooser();
|
FileChooser fileChooser = new FileChooser();
|
||||||
fileChooser.setTitle("新建 SQLite 数据库");
|
fileChooser.setTitle("新建 SQLite 数据库");
|
||||||
fileChooser.setInitialFileName("new-database.db");
|
fileChooser.setInitialFileName("new-database.db");
|
||||||
fileChooser.getExtensionFilters().addAll(
|
addSqliteExtensionFilters(fileChooser);
|
||||||
new FileChooser.ExtensionFilter("SQLite 数据库", "*.db", "*.sqlite", "*.sqlite3"),
|
|
||||||
new FileChooser.ExtensionFilter("所有文件", "*.*")
|
|
||||||
);
|
|
||||||
|
|
||||||
File selectedFile = fileChooser.showSaveDialog(currentWindow());
|
File selectedFile = fileChooser.showSaveDialog(currentWindow());
|
||||||
if (selectedFile == null) {
|
if (selectedFile == null) {
|
||||||
@@ -202,10 +210,7 @@ public class MainController {
|
|||||||
private void handleOpenDatabase() {
|
private void handleOpenDatabase() {
|
||||||
FileChooser fileChooser = new FileChooser();
|
FileChooser fileChooser = new FileChooser();
|
||||||
fileChooser.setTitle("打开 SQLite 数据库");
|
fileChooser.setTitle("打开 SQLite 数据库");
|
||||||
fileChooser.getExtensionFilters().addAll(
|
addSqliteExtensionFilters(fileChooser);
|
||||||
new FileChooser.ExtensionFilter("SQLite 数据库", "*.db", "*.sqlite", "*.sqlite3"),
|
|
||||||
new FileChooser.ExtensionFilter("所有文件", "*.*")
|
|
||||||
);
|
|
||||||
|
|
||||||
File selectedFile = fileChooser.showOpenDialog(currentWindow());
|
File selectedFile = fileChooser.showOpenDialog(currentWindow());
|
||||||
if (selectedFile == null) {
|
if (selectedFile == null) {
|
||||||
@@ -224,6 +229,102 @@ public class MainController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void handleBackupCurrentDatabase() {
|
||||||
|
Optional<Path> currentDatabasePath = currentDatabasePathOrAlert();
|
||||||
|
if (currentDatabasePath.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path sourcePath = currentDatabasePath.get();
|
||||||
|
FileChooser fileChooser = new FileChooser();
|
||||||
|
fileChooser.setTitle("备份 SQLite 数据库");
|
||||||
|
fileChooser.setInitialFileName(sourcePath.getFileName() + ".backup.db");
|
||||||
|
addSqliteExtensionFilters(fileChooser);
|
||||||
|
|
||||||
|
File selectedFile = fileChooser.showSaveDialog(currentWindow());
|
||||||
|
if (selectedFile == null) {
|
||||||
|
status("已取消备份数据库");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path backupPath = selectedFile.toPath().toAbsolutePath().normalize();
|
||||||
|
try {
|
||||||
|
if (isSamePathOrFile(sourcePath, backupPath)) {
|
||||||
|
DialogUtils.showError("备份数据库失败", "备份目标不能是当前数据库文件本身。");
|
||||||
|
status("备份数据库失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
checkpointCurrentDatabase();
|
||||||
|
Files.copy(sourcePath, backupPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
DialogUtils.showInfo("备份数据库成功", "已备份数据库:\n" + sourcePath + "\n\n备份文件:\n" + backupPath);
|
||||||
|
status("已备份数据库");
|
||||||
|
} catch (IOException | SQLException e) {
|
||||||
|
DialogUtils.showError("备份数据库失败", e.getMessage());
|
||||||
|
status("备份数据库失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void handleRestoreCurrentDatabase() {
|
||||||
|
Optional<Path> currentDatabasePath = currentDatabasePathOrAlert();
|
||||||
|
if (currentDatabasePath.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path targetPath = currentDatabasePath.get();
|
||||||
|
FileChooser fileChooser = new FileChooser();
|
||||||
|
fileChooser.setTitle("选择数据库备份文件");
|
||||||
|
addSqliteExtensionFilters(fileChooser);
|
||||||
|
|
||||||
|
File selectedFile = fileChooser.showOpenDialog(currentWindow());
|
||||||
|
if (selectedFile == null) {
|
||||||
|
status("已取消还原数据库");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path backupPath = selectedFile.toPath().toAbsolutePath().normalize();
|
||||||
|
try {
|
||||||
|
if (!Files.isRegularFile(backupPath) || !Files.isReadable(backupPath)) {
|
||||||
|
DialogUtils.showError("还原数据库失败", "备份文件不存在或不可读:\n" + backupPath);
|
||||||
|
status("还原数据库失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isSamePathOrFile(targetPath, backupPath)) {
|
||||||
|
DialogUtils.showError("还原数据库失败", "备份文件不能是当前数据库文件本身。");
|
||||||
|
status("还原数据库失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
DialogUtils.showError("还原数据库失败", e.getMessage());
|
||||||
|
status("还原数据库失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean confirmed = DialogUtils.confirm(
|
||||||
|
"确认还原数据库",
|
||||||
|
"将关闭当前连接,并用以下备份文件覆盖当前数据库:\n\n"
|
||||||
|
+ backupPath
|
||||||
|
+ "\n\n当前数据库:\n"
|
||||||
|
+ targetPath
|
||||||
|
+ "\n\n此操作不可撤销,是否继续?"
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
status("已取消还原数据库");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionManager.close();
|
||||||
|
try {
|
||||||
|
restoreFromBackup(backupPath, targetPath);
|
||||||
|
reopenDatabaseAndRefresh(targetPath);
|
||||||
|
DialogUtils.showInfo("还原数据库成功", "已从备份还原数据库:\n" + backupPath + "\n\n当前数据库:\n" + targetPath);
|
||||||
|
status("已还原数据库");
|
||||||
|
} catch (IOException | SQLException e) {
|
||||||
|
handleRestoreFailure(targetPath, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void handleDeleteCurrentDatabase() {
|
private void handleDeleteCurrentDatabase() {
|
||||||
Optional<Path> currentDatabasePath = connectionManager.getCurrentDatabasePath();
|
Optional<Path> currentDatabasePath = connectionManager.getCurrentDatabasePath();
|
||||||
@@ -469,6 +570,50 @@ public class MainController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void handleAiSettings() {
|
||||||
|
Optional<AiSettings> settings = showAiSettingsDialog();
|
||||||
|
if (settings.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
aiSettings = settings.get();
|
||||||
|
status("AI 设置已更新");
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private void handleGenerateSql() {
|
||||||
|
if (!ensureConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (aiSettings == null || !aiSettings.hasApiKey()) {
|
||||||
|
DialogUtils.showError("未设置 AI", "请先在“执行 > AI 设置”中输入 API Key。");
|
||||||
|
status("未设置 AI");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<String> prompt = showAiPromptDialog();
|
||||||
|
if (prompt.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AiSettings settingsSnapshot = aiSettings;
|
||||||
|
Connection connection = connection();
|
||||||
|
String naturalLanguagePrompt = prompt.get();
|
||||||
|
Task<String> task = new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected String call() throws AiGenerationException {
|
||||||
|
return aiSqlGenerationService.generateSql(connection, settingsSnapshot, naturalLanguagePrompt);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sqlMessageLabel.setText("AI 正在生成 SQL...");
|
||||||
|
status("AI 正在生成 SQL");
|
||||||
|
task.setOnSucceeded(_ -> applyGeneratedSql(task.getValue()));
|
||||||
|
task.setOnFailed(_ -> handleAiGenerationFailure(task.getException()));
|
||||||
|
Thread thread = new Thread(task, "hypersql-ai-sql-generation");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private void handleExecuteSql() {
|
private void handleExecuteSql() {
|
||||||
if (!ensureConnected()) {
|
if (!ensureConnected()) {
|
||||||
@@ -836,6 +981,139 @@ public class MainController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Optional<AiSettings> showAiSettingsDialog() {
|
||||||
|
Dialog<AiSettings> dialog = new Dialog<>();
|
||||||
|
dialog.setTitle("AI 设置");
|
||||||
|
dialog.initOwner(currentWindow());
|
||||||
|
dialog.getDialogPane().getButtonTypes().addAll(javafx.scene.control.ButtonType.OK, javafx.scene.control.ButtonType.CANCEL);
|
||||||
|
|
||||||
|
ComboBox<AiProvider> providerComboBox = new ComboBox<>(FXCollections.observableArrayList(AiProvider.values()));
|
||||||
|
PasswordField apiKeyField = new PasswordField();
|
||||||
|
TextField baseUrlField = new TextField();
|
||||||
|
TextField modelField = new TextField();
|
||||||
|
TextField timeoutField = new TextField(String.valueOf(DEFAULT_AI_TIMEOUT_SECONDS));
|
||||||
|
|
||||||
|
AiSettings currentSettings = aiSettings;
|
||||||
|
if (currentSettings == null) {
|
||||||
|
providerComboBox.getSelectionModel().select(AiProvider.OPENAI_COMPATIBLE);
|
||||||
|
baseUrlField.setText(defaultBaseUrl(AiProvider.OPENAI_COMPATIBLE));
|
||||||
|
modelField.setText("gpt-4o-mini");
|
||||||
|
} else {
|
||||||
|
providerComboBox.getSelectionModel().select(currentSettings.provider());
|
||||||
|
apiKeyField.setText(currentSettings.apiKey());
|
||||||
|
baseUrlField.setText(currentSettings.baseUrl());
|
||||||
|
modelField.setText(currentSettings.model());
|
||||||
|
timeoutField.setText(String.valueOf(currentSettings.timeoutSeconds()));
|
||||||
|
}
|
||||||
|
|
||||||
|
providerComboBox.valueProperty().addListener((_, _, provider) -> {
|
||||||
|
if (provider == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
baseUrlField.setText(defaultBaseUrl(provider));
|
||||||
|
modelField.setText(defaultModel(provider));
|
||||||
|
});
|
||||||
|
|
||||||
|
GridPane gridPane = new GridPane();
|
||||||
|
gridPane.setHgap(8);
|
||||||
|
gridPane.setVgap(8);
|
||||||
|
gridPane.add(new Label("Provider"), 0, 0);
|
||||||
|
gridPane.add(providerComboBox, 1, 0);
|
||||||
|
gridPane.add(new Label("API Key"), 0, 1);
|
||||||
|
gridPane.add(apiKeyField, 1, 1);
|
||||||
|
gridPane.add(new Label("Base URL"), 0, 2);
|
||||||
|
gridPane.add(baseUrlField, 1, 2);
|
||||||
|
gridPane.add(new Label("Model"), 0, 3);
|
||||||
|
gridPane.add(modelField, 1, 3);
|
||||||
|
gridPane.add(new Label("超时秒数"), 0, 4);
|
||||||
|
gridPane.add(timeoutField, 1, 4);
|
||||||
|
dialog.getDialogPane().setContent(gridPane);
|
||||||
|
|
||||||
|
dialog.setResultConverter(buttonType -> {
|
||||||
|
if (!javafx.scene.control.ButtonType.OK.equals(buttonType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
int timeoutSeconds = Integer.parseInt(timeoutField.getText().trim());
|
||||||
|
return new AiSettings(
|
||||||
|
providerComboBox.getValue(),
|
||||||
|
apiKeyField.getText(),
|
||||||
|
baseUrlField.getText(),
|
||||||
|
modelField.getText(),
|
||||||
|
timeoutSeconds
|
||||||
|
);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new IllegalArgumentException("超时时间必须是数字。");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
return dialog.showAndWait();
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
DialogUtils.showError("AI 设置无效", e.getMessage());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String> showAiPromptDialog() {
|
||||||
|
Dialog<String> dialog = new Dialog<>();
|
||||||
|
dialog.setTitle("AI 生成 SQL");
|
||||||
|
dialog.initOwner(currentWindow());
|
||||||
|
dialog.getDialogPane().getButtonTypes().addAll(javafx.scene.control.ButtonType.OK, javafx.scene.control.ButtonType.CANCEL);
|
||||||
|
|
||||||
|
TextArea promptArea = new TextArea();
|
||||||
|
promptArea.setPromptText("例如:查询每个用户的订单数量");
|
||||||
|
promptArea.setPrefRowCount(6);
|
||||||
|
promptArea.setWrapText(true);
|
||||||
|
dialog.getDialogPane().setContent(promptArea);
|
||||||
|
dialog.setResultConverter(buttonType -> {
|
||||||
|
if (!javafx.scene.control.ButtonType.OK.equals(buttonType)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return promptArea.getText() == null ? "" : promptArea.getText().trim();
|
||||||
|
});
|
||||||
|
Optional<String> result = dialog.showAndWait();
|
||||||
|
if (result.isPresent() && result.get().isBlank()) {
|
||||||
|
DialogUtils.showError("需求不能为空", "请输入希望 AI 生成的 SQL 需求。");
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyGeneratedSql(String generatedSql) {
|
||||||
|
sqlTextArea.setText(generatedSql);
|
||||||
|
mainTabPane.getSelectionModel().select(2);
|
||||||
|
String message = "AI 已生成 SQL,请检查后再手动执行。";
|
||||||
|
if (aiSqlGenerationService.containsDestructiveKeyword(generatedSql)) {
|
||||||
|
message += " 检测到可能修改数据或结构的关键词,请特别谨慎。";
|
||||||
|
}
|
||||||
|
sqlMessageLabel.setText(message);
|
||||||
|
status("AI 已生成 SQL");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleAiGenerationFailure(Throwable throwable) {
|
||||||
|
String message = throwable == null ? "AI 生成 SQL 失败。" : throwable.getMessage();
|
||||||
|
if (message == null || message.isBlank()) {
|
||||||
|
message = "AI 生成 SQL 失败。";
|
||||||
|
}
|
||||||
|
sqlMessageLabel.setText(message);
|
||||||
|
DialogUtils.showError("AI 生成 SQL 失败", message);
|
||||||
|
status("AI 生成 SQL 失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultBaseUrl(AiProvider provider) {
|
||||||
|
return switch (provider) {
|
||||||
|
case OPENAI_COMPATIBLE -> "https://api.openai.com/v1";
|
||||||
|
case ANTHROPIC -> "https://api.anthropic.com";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultModel(AiProvider provider) {
|
||||||
|
return switch (provider) {
|
||||||
|
case OPENAI_COMPATIBLE -> "gpt-4o-mini";
|
||||||
|
case ANTHROPIC -> "claude-opus-4-7";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private Optional<Map<String, Object>> showRowInputDialog(List<ColumnInfo> columns) {
|
private Optional<Map<String, Object>> showRowInputDialog(List<ColumnInfo> columns) {
|
||||||
Dialog<Map<String, Object>> dialog = new Dialog<>();
|
Dialog<Map<String, Object>> dialog = new Dialog<>();
|
||||||
dialog.setTitle("新增数据行");
|
dialog.setTitle("新增数据行");
|
||||||
@@ -1140,6 +1418,73 @@ public class MainController {
|
|||||||
return value == null ? "NULL" : String.valueOf(value);
|
return value == null ? "NULL" : String.valueOf(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Optional<Path> currentDatabasePathOrAlert() {
|
||||||
|
Optional<Path> currentDatabasePath = connectionManager.getCurrentDatabasePath();
|
||||||
|
if (currentDatabasePath.isPresent()) {
|
||||||
|
return currentDatabasePath;
|
||||||
|
}
|
||||||
|
DialogUtils.showError("未连接数据库", "请先打开或新建一个 SQLite 数据库文件。");
|
||||||
|
status("未连接数据库");
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addSqliteExtensionFilters(FileChooser fileChooser) {
|
||||||
|
fileChooser.getExtensionFilters().addAll(
|
||||||
|
new FileChooser.ExtensionFilter("SQLite 数据库", "*.db", "*.sqlite", "*.sqlite3"),
|
||||||
|
new FileChooser.ExtensionFilter("所有文件", "*.*")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSamePathOrFile(Path first, Path second) throws IOException {
|
||||||
|
Path normalizedFirst = first.toAbsolutePath().normalize();
|
||||||
|
Path normalizedSecond = second.toAbsolutePath().normalize();
|
||||||
|
if (normalizedFirst.equals(normalizedSecond)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return Files.exists(normalizedFirst)
|
||||||
|
&& Files.exists(normalizedSecond)
|
||||||
|
&& Files.isSameFile(normalizedFirst, normalizedSecond);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkpointCurrentDatabase() throws SQLException {
|
||||||
|
try (Statement statement = connection().createStatement()) {
|
||||||
|
statement.execute("PRAGMA wal_checkpoint(FULL)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restoreFromBackup(Path backupPath, Path targetPath) throws IOException {
|
||||||
|
Path parent = targetPath.getParent();
|
||||||
|
Path tempPath = Files.createTempFile(parent, targetPath.getFileName().toString(), ".restore.tmp");
|
||||||
|
try {
|
||||||
|
Files.copy(backupPath, tempPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
Files.move(tempPath, targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(tempPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reopenDatabaseAndRefresh(Path databasePath) throws SQLException {
|
||||||
|
connectionManager.open(databasePath);
|
||||||
|
databasePathLabel.setText(databasePath.toString());
|
||||||
|
refreshTables();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleRestoreFailure(Path targetPath, Exception cause) {
|
||||||
|
try {
|
||||||
|
if (Files.exists(targetPath)) {
|
||||||
|
reopenDatabaseAndRefresh(targetPath);
|
||||||
|
DialogUtils.showError("还原数据库失败", "还原失败,已重新打开当前数据库:\n" + cause.getMessage());
|
||||||
|
status("还原数据库失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
cause.addSuppressed(e);
|
||||||
|
}
|
||||||
|
updateDisconnectedUi();
|
||||||
|
DialogUtils.showError("还原数据库失败", "还原失败,且当前数据库无法重新打开:\n" + cause.getMessage());
|
||||||
|
status("还原数据库失败");
|
||||||
|
}
|
||||||
|
|
||||||
private boolean ensureConnected() {
|
private boolean ensureConnected() {
|
||||||
if (connectionManager.isConnected()) {
|
if (connectionManager.isConnected()) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package com.hypersql.db;
|
||||||
|
|
||||||
|
import com.hypersql.model.ColumnInfo;
|
||||||
|
import com.hypersql.model.TableInfo;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class SchemaDescriptionService {
|
||||||
|
private final DatabaseMetadataService metadataService = new DatabaseMetadataService();
|
||||||
|
|
||||||
|
public String describe(Connection connection) throws SQLException {
|
||||||
|
List<TableInfo> objects = metadataService.listTables(connection);
|
||||||
|
if (objects.isEmpty()) {
|
||||||
|
return "当前数据库没有用户表或视图。";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder description = new StringBuilder();
|
||||||
|
for (TableInfo object : objects) {
|
||||||
|
if ("view".equalsIgnoreCase(object.type())) {
|
||||||
|
appendView(connection, description, object);
|
||||||
|
} else {
|
||||||
|
appendTable(connection, description, object);
|
||||||
|
}
|
||||||
|
description.append('\n');
|
||||||
|
}
|
||||||
|
return description.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendTable(Connection connection, StringBuilder description, TableInfo table) throws SQLException {
|
||||||
|
description.append("表:").append(table.name()).append('\n');
|
||||||
|
List<ColumnInfo> columns = metadataService.listColumns(connection, table.name());
|
||||||
|
for (ColumnInfo column : columns) {
|
||||||
|
description.append(" - ")
|
||||||
|
.append(column.name())
|
||||||
|
.append(" ")
|
||||||
|
.append(column.type() == null || column.type().isBlank() ? "未声明类型" : column.type());
|
||||||
|
if (column.primaryKey()) {
|
||||||
|
description.append(" PRIMARY KEY");
|
||||||
|
}
|
||||||
|
if (column.notNull()) {
|
||||||
|
description.append(" NOT NULL");
|
||||||
|
}
|
||||||
|
if (column.defaultValue() != null) {
|
||||||
|
description.append(" DEFAULT ").append(column.defaultValue());
|
||||||
|
}
|
||||||
|
description.append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendView(Connection connection, StringBuilder description, TableInfo view) throws SQLException {
|
||||||
|
description.append("视图:").append(view.name()).append('\n');
|
||||||
|
String sql = viewDefinition(connection, view.name());
|
||||||
|
if (sql != null && !sql.isBlank()) {
|
||||||
|
description.append(" 定义:").append(sql).append('\n');
|
||||||
|
}
|
||||||
|
List<ColumnInfo> columns = metadataService.listColumns(connection, view.name());
|
||||||
|
for (ColumnInfo column : columns) {
|
||||||
|
description.append(" - ")
|
||||||
|
.append(column.name())
|
||||||
|
.append(" ")
|
||||||
|
.append(column.type() == null || column.type().isBlank() ? "未声明类型" : column.type())
|
||||||
|
.append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String viewDefinition(Connection connection, String viewName) throws SQLException {
|
||||||
|
String sql = """
|
||||||
|
SELECT sql
|
||||||
|
FROM sqlite_master
|
||||||
|
WHERE type = 'view'
|
||||||
|
AND name = ?
|
||||||
|
""";
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||||
|
statement.setString(1, viewName);
|
||||||
|
try (ResultSet resultSet = statement.executeQuery()) {
|
||||||
|
return resultSet.next() ? resultSet.getString("sql") : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
<?import javafx.scene.control.MenuBar?>
|
<?import javafx.scene.control.MenuBar?>
|
||||||
<?import javafx.scene.control.MenuItem?>
|
<?import javafx.scene.control.MenuItem?>
|
||||||
<?import javafx.scene.control.Separator?>
|
<?import javafx.scene.control.Separator?>
|
||||||
|
<?import javafx.scene.control.SeparatorMenuItem?>
|
||||||
<?import javafx.scene.control.SplitPane?>
|
<?import javafx.scene.control.SplitPane?>
|
||||||
<?import javafx.scene.control.Tab?>
|
<?import javafx.scene.control.Tab?>
|
||||||
<?import javafx.scene.control.TabPane?>
|
<?import javafx.scene.control.TabPane?>
|
||||||
@@ -26,27 +27,33 @@
|
|||||||
<Menu text="文件">
|
<Menu text="文件">
|
||||||
<MenuItem onAction="#handleCreateDatabase" text="新建数据库..." />
|
<MenuItem onAction="#handleCreateDatabase" text="新建数据库..." />
|
||||||
<MenuItem onAction="#handleOpenDatabase" text="打开数据库..." />
|
<MenuItem onAction="#handleOpenDatabase" text="打开数据库..." />
|
||||||
<MenuItem onAction="#handleDeleteCurrentDatabase" text="删除当前数据库文件..." />
|
|
||||||
<MenuItem onAction="#handleCloseDatabase" text="关闭数据库" />
|
<MenuItem onAction="#handleCloseDatabase" text="关闭数据库" />
|
||||||
|
<SeparatorMenuItem />
|
||||||
|
<MenuItem onAction="#handleBackupCurrentDatabase" text="备份当前数据库..." />
|
||||||
|
<MenuItem onAction="#handleRestoreCurrentDatabase" text="从备份还原当前数据库..." />
|
||||||
|
<SeparatorMenuItem />
|
||||||
|
<MenuItem onAction="#handleDeleteCurrentDatabase" text="删除当前数据库文件..." />
|
||||||
|
<SeparatorMenuItem />
|
||||||
<MenuItem onAction="#handleExit" text="退出" />
|
<MenuItem onAction="#handleExit" text="退出" />
|
||||||
</Menu>
|
</Menu>
|
||||||
<Menu text="表">
|
<Menu text="表">
|
||||||
<MenuItem onAction="#handleCreateTable" text="创建表..." />
|
<MenuItem onAction="#handleCreateTable" text="创建表..." />
|
||||||
<MenuItem onAction="#handleDeleteSelectedTable" text="删除选中表..." />
|
<MenuItem onAction="#handleDeleteSelectedTable" text="删除选中表..." />
|
||||||
|
<SeparatorMenuItem />
|
||||||
<MenuItem onAction="#handleRefreshTables" text="刷新表列表" />
|
<MenuItem onAction="#handleRefreshTables" text="刷新表列表" />
|
||||||
</Menu>
|
</Menu>
|
||||||
<Menu text="执行">
|
<Menu text="执行">
|
||||||
|
<MenuItem onAction="#handleAiSettings" text="AI 设置..." />
|
||||||
|
<MenuItem onAction="#handleGenerateSql" text="AI 生成 SQL..." />
|
||||||
|
<SeparatorMenuItem />
|
||||||
<MenuItem onAction="#handleExecuteSql" text="执行 SQL" />
|
<MenuItem onAction="#handleExecuteSql" text="执行 SQL" />
|
||||||
</Menu>
|
</Menu>
|
||||||
</MenuBar>
|
</MenuBar>
|
||||||
<ToolBar>
|
<ToolBar styleClass="app-toolbar">
|
||||||
<Button onAction="#handleCreateDatabase" text="新建数据库" />
|
<Button onAction="#handleCreateDatabase" text="新建数据库" />
|
||||||
<Button onAction="#handleOpenDatabase" text="打开数据库" />
|
<Button onAction="#handleOpenDatabase" text="打开数据库" />
|
||||||
<Button onAction="#handleDeleteCurrentDatabase" text="删除数据库" />
|
<Button onAction="#handleBackupCurrentDatabase" text="备份数据库" />
|
||||||
<Button onAction="#handleCloseDatabase" text="关闭数据库" />
|
|
||||||
<Separator />
|
<Separator />
|
||||||
<Button onAction="#handleCreateTable" text="创建表" />
|
|
||||||
<Button onAction="#handleDeleteSelectedTable" text="删除表" />
|
|
||||||
<Button onAction="#handleRefreshTables" text="刷新表列表" />
|
<Button onAction="#handleRefreshTables" text="刷新表列表" />
|
||||||
<Button onAction="#handleExecuteSql" text="执行 SQL" />
|
<Button onAction="#handleExecuteSql" text="执行 SQL" />
|
||||||
</ToolBar>
|
</ToolBar>
|
||||||
@@ -63,6 +70,11 @@
|
|||||||
<Label styleClass="section-title" text="当前数据库" />
|
<Label styleClass="section-title" text="当前数据库" />
|
||||||
<Label fx:id="databasePathLabel" maxWidth="Infinity" text="未连接数据库" wrapText="true" />
|
<Label fx:id="databasePathLabel" maxWidth="Infinity" text="未连接数据库" wrapText="true" />
|
||||||
<Label styleClass="section-title" text="表 / 视图列表" />
|
<Label styleClass="section-title" text="表 / 视图列表" />
|
||||||
|
<HBox spacing="6" styleClass="sidebar-actions">
|
||||||
|
<Button onAction="#handleCreateTable" text="创建" />
|
||||||
|
<Button onAction="#handleDeleteSelectedTable" text="删除" />
|
||||||
|
<Button onAction="#handleRefreshTables" text="刷新" />
|
||||||
|
</HBox>
|
||||||
<ListView fx:id="tableListView" VBox.vgrow="ALWAYS" />
|
<ListView fx:id="tableListView" VBox.vgrow="ALWAYS" />
|
||||||
</VBox>
|
</VBox>
|
||||||
<TabPane fx:id="mainTabPane">
|
<TabPane fx:id="mainTabPane">
|
||||||
@@ -118,6 +130,7 @@
|
|||||||
<Label styleClass="section-title" text="SQL 输入" />
|
<Label styleClass="section-title" text="SQL 输入" />
|
||||||
<TextArea fx:id="sqlTextArea" prefRowCount="7" promptText="请输入 SQL 语句" VBox.vgrow="NEVER" />
|
<TextArea fx:id="sqlTextArea" prefRowCount="7" promptText="请输入 SQL 语句" VBox.vgrow="NEVER" />
|
||||||
<HBox alignment="CENTER_LEFT" spacing="8">
|
<HBox alignment="CENTER_LEFT" spacing="8">
|
||||||
|
<Button onAction="#handleGenerateSql" text="AI 生成 SQL" />
|
||||||
<Button onAction="#handleExecuteSql" text="执行 SQL" />
|
<Button onAction="#handleExecuteSql" text="执行 SQL" />
|
||||||
<Label fx:id="sqlMessageLabel" text="等待执行 SQL" HBox.hgrow="ALWAYS" />
|
<Label fx:id="sqlMessageLabel" text="等待执行 SQL" HBox.hgrow="ALWAYS" />
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|||||||
@@ -7,6 +7,16 @@
|
|||||||
-fx-background-color: #f7f8fa;
|
-fx-background-color: #f7f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-toolbar {
|
||||||
|
-fx-background-color: #fafafa;
|
||||||
|
-fx-border-color: #dcdcdc transparent #dcdcdc transparent;
|
||||||
|
-fx-padding: 6 8 6 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-actions {
|
||||||
|
-fx-padding: 0 0 2 0;
|
||||||
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
-fx-font-weight: bold;
|
-fx-font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user