Initial HyperSql project
Add the JavaFX SQLite management application with project analysis documents and ignore local build/runtime files. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
target/
|
||||
.claude/
|
||||
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
*.iml
|
||||
|
||||
*.log
|
||||
|
||||
# Local SQLite test databases
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
@@ -0,0 +1,175 @@
|
||||
# HyperSql 可行性分析
|
||||
|
||||
## 1. 可行性分析概述
|
||||
|
||||
可行性分析用于判断 HyperSql 在当前技术条件、开发时间、经济成本和实际使用场景下是否能够顺利完成。
|
||||
|
||||
HyperSql 是一个基于 Java 24、JavaFX、Maven 和 SQLite 的轻量级数据库图形化管理工具,主要面向学生和 SQLite 初学者。目前项目已经完成了核心数据库浏览和管理功能,后续将继续扩展数据编辑、备份恢复和 AI 辅助 SQL 生成。
|
||||
|
||||
本项目从以下四个方面进行分析:
|
||||
|
||||
1. **技术可行性**
|
||||
2. **经济可行性**
|
||||
3. **时间可行性**
|
||||
4. **操作可行性**
|
||||
|
||||
## 2. 技术可行性
|
||||
|
||||
HyperSql 采用以下技术栈:
|
||||
|
||||
| 技术 | 作用 | 当前使用情况 |
|
||||
|---|---|---|
|
||||
| Java 24 | 主要开发语言 | 已用于项目主要代码开发 |
|
||||
| JavaFX | 构建图形化用户界面 | 已实现主界面、表格、菜单、工具栏和创建表对话框 |
|
||||
| Maven | 项目构建与依赖管理 | 已完成项目构建配置 |
|
||||
| SQLite | 本地轻量级数据库 | 作为系统管理对象 |
|
||||
| SQLite JDBC | Java 程序访问 SQLite 数据库 | 已实现数据库连接、元数据读取和 SQL 执行 |
|
||||
| AI API | 根据表结构和用户需求生成 SQL | 作为后续扩展功能,当前尚未实现 |
|
||||
|
||||
### 技术可行性说明
|
||||
|
||||
1. **Java 24 能满足桌面应用开发需求**
|
||||
- Java 语言成熟,适合编写结构清晰的桌面程序。
|
||||
- 当前项目已经通过 Maven 编译验证,说明技术环境可正常运行。
|
||||
|
||||
2. **JavaFX 能满足 GUI 需求**
|
||||
- JavaFX 提供菜单栏、按钮、标签、文本框、表格、分页按钮、弹窗和 FXML 等组件。
|
||||
- 当前项目已经实现左侧表/视图列表,右侧表数据、表结构和 SQL 执行区域,能够满足数据库管理工具的基本界面需求。
|
||||
|
||||
3. **SQLite 集成难度低**
|
||||
- SQLite 是文件型数据库,不需要单独部署数据库服务器,适合课程项目和本地数据库管理。
|
||||
- 当前项目已经支持打开、新建、关闭和删除 SQLite 数据库文件。
|
||||
|
||||
4. **SQLite JDBC 访问方式成熟**
|
||||
- 通过 JDBC 可以完成数据库连接、查询、执行 SQL、读取表结构等操作。
|
||||
- 当前项目已经实现表/视图列表读取、字段信息读取、表数据分页查询和 SQL 执行。
|
||||
|
||||
5. **表管理功能可以通过 SQL 和 JavaFX UI 实现**
|
||||
- 当前项目已经通过独立创建表对话框实现了表名输入、字段定义、字段类型选择、主键/非空设置、默认值填写和 SQL 预览。
|
||||
- 删除表功能已经加入确认流程,降低误操作风险。
|
||||
|
||||
6. **AI SQL 生成功能技术上可行,但需要控制范围**
|
||||
- 后续可以读取当前数据库表结构,结合用户自然语言描述构造提示词,然后调用 AI API 生成 SQL。
|
||||
- 该功能涉及 API Key 管理、网络请求、调用成本和生成结果校验,因此适合作为扩展功能。
|
||||
|
||||
### 技术可行性结论
|
||||
|
||||
HyperSql 所采用的核心技术已经在当前项目中得到验证。数据库连接、表结构读取、表数据分页显示、SQL 执行、数据库文件管理和表管理功能均已实现,因此项目核心功能在技术上可行。AI SQL 生成属于后续扩展,技术上可实现,但需要在安全性和调用成本方面进行控制。
|
||||
|
||||
## 3. 经济可行性
|
||||
|
||||
HyperSql 的开发成本较低,主要体现在以下方面:
|
||||
|
||||
1. **开发工具成本低**
|
||||
- Java、JavaFX、Maven、SQLite 和 SQLite JDBC 均可免费使用。
|
||||
- 可使用 IntelliJ IDEA Community、VS Code 等免费开发工具。
|
||||
|
||||
2. **运行环境成本低**
|
||||
- SQLite 不需要部署数据库服务器。
|
||||
- 软件运行在本地电脑上,不需要购买云服务器。
|
||||
|
||||
3. **项目开发成本低**
|
||||
- 项目主要成本是课程开发时间和学习成本。
|
||||
- 当前已完成的功能不涉及商业授权费用。
|
||||
|
||||
4. **AI API 成本可控**
|
||||
- AI SQL 生成尚未实现,可以作为后续扩展功能。
|
||||
- 若后续实现,可限制调用次数,或只在演示场景使用少量 API 调用,避免高额费用。
|
||||
|
||||
### 经济可行性结论
|
||||
|
||||
HyperSql 不需要额外硬件和商业软件投入,当前已实现功能均基于免费技术完成,整体开发与运行成本较低,经济上可行。
|
||||
|
||||
## 4. 时间可行性
|
||||
|
||||
HyperSql 采用增量迭代模型,可以按照功能优先级逐步完成,适合课程项目周期。
|
||||
|
||||
### 当前完成情况
|
||||
|
||||
| 阶段 | 主要任务 | 当前状态 |
|
||||
|---|---|---|
|
||||
| 需求与分析 | 明确功能范围,完成过程模型、可行性分析和需求分析 | 已完成 |
|
||||
| 项目搭建 | 搭建 Maven + JavaFX 项目结构,引入 SQLite JDBC | 已完成 |
|
||||
| 数据库连接 | 打开、新建、关闭 SQLite 数据库 | 已完成 |
|
||||
| 数据库浏览 | 显示表/视图列表,查看表结构 | 已完成 |
|
||||
| 数据查看 | 表数据分页显示,每页 100 行 | 已完成 |
|
||||
| SQL 执行 | 执行 SQL 并显示结果,SQL 查询结果分页 | 已完成 |
|
||||
| 数据库文件管理 | 删除当前数据库文件 | 已完成 |
|
||||
| 表管理 | 创建表 UI、删除表确认流程 | 已完成 |
|
||||
| 交互修复 | 修复选择表不同步、刷新表列表保留当前表 | 已完成 |
|
||||
| 数据行编辑 | 图形界面新增、修改、删除数据行 | 待实现 |
|
||||
| 备份恢复 | 数据库备份和恢复 | 待实现 |
|
||||
| AI SQL 生成 | 根据表结构和自然语言生成 SQL | 待实现 |
|
||||
| 测试优化 | 功能测试、界面优化、最终演示准备 | 持续进行 |
|
||||
|
||||
### 时间可行性说明
|
||||
|
||||
1. 当前项目已经完成数据库管理工具的核心可运行版本,可以满足基本演示要求。
|
||||
2. 后续开发可以优先完成数据行编辑和备份恢复,因为它们与数据库管理工具的实用性直接相关。
|
||||
3. AI SQL 生成可以作为亮点功能,根据剩余时间决定实现深度。
|
||||
4. 采用增量迭代方式后,即使扩展功能未全部完成,系统仍然具备可运行、可演示的核心功能。
|
||||
|
||||
### 时间可行性结论
|
||||
|
||||
从当前进展看,HyperSql 的核心功能已经完成,课程周期内完成一个可演示版本具有较高可行性。后续需要合理控制扩展功能范围,避免 AI 调用和复杂数据编辑占用过多开发时间。
|
||||
|
||||
## 5. 操作可行性
|
||||
|
||||
HyperSql 面向学生和 SQLite 初学者,软件操作流程较为简单。
|
||||
|
||||
### 当前主要操作流程
|
||||
|
||||
```text
|
||||
新建或打开 SQLite 数据库文件
|
||||
↓
|
||||
浏览左侧表/视图列表
|
||||
↓
|
||||
选择表或视图
|
||||
↓
|
||||
查看表结构和表数据
|
||||
↓
|
||||
使用分页按钮浏览更多数据
|
||||
↓
|
||||
在 SQL 执行区输入并执行 SQL
|
||||
↓
|
||||
根据需要创建表、删除表或刷新表列表
|
||||
```
|
||||
|
||||
### 操作可行性说明
|
||||
|
||||
1. **界面符合用户习惯**
|
||||
- 采用类似数据库管理工具的布局:左侧显示表/视图列表,右侧显示表数据、表结构和 SQL 执行区域。
|
||||
|
||||
2. **降低数据库操作门槛**
|
||||
- 用户可以通过图形界面打开数据库、浏览表结构、查看表数据、创建表和删除表,不必完全依赖命令行工具。
|
||||
|
||||
3. **分页降低操作压力**
|
||||
- 表数据和 SQL 查询结果都使用分页显示,每页 100 行,避免一次显示大量数据导致界面卡顿或不便查看。
|
||||
|
||||
4. **错误提示清晰**
|
||||
- 当未连接数据库、数据库连接失败、SQL 执行错误、创建表失败或删除表失败时,系统会给出提示信息。
|
||||
|
||||
5. **危险操作有确认流程**
|
||||
- 删除数据库文件和删除表都属于破坏性操作,系统在执行前会弹出确认提示。
|
||||
|
||||
6. **当前限制清晰**
|
||||
- 当前版本已经支持创建表和删除普通表,但尚未实现图形界面数据行编辑、数据库备份恢复和 AI SQL 生成。
|
||||
|
||||
### 操作可行性结论
|
||||
|
||||
HyperSql 当前功能界面直观,基本操作流程清晰,适合学生和 SQLite 初学者使用。随着后续数据编辑和备份恢复功能完善,软件的实用性会进一步提高。
|
||||
|
||||
## 6. 可行性分析总结
|
||||
|
||||
通过技术、经济、时间和操作四个方面的分析,可以得出以下结论:
|
||||
|
||||
| 分析方面 | 结论 |
|
||||
|---|---|
|
||||
| 技术可行性 | JavaFX、SQLite JDBC 和 Maven 已经支撑当前核心功能实现,AI API 可作为后续扩展 |
|
||||
| 经济可行性 | 开发工具和数据库免费,当前功能无额外运行成本 |
|
||||
| 时间可行性 | 核心功能已经完成,后续扩展可按优先级继续迭代 |
|
||||
| 操作可行性 | 面向学生和初学者,界面简单直观,危险操作有确认提示 |
|
||||
|
||||
### 总体结论
|
||||
|
||||
HyperSql 的技术方案成熟,开发成本低,核心功能已经形成可运行版本,适合作为软件工程课程大作业项目继续完善。后续应优先补充数据行编辑和备份恢复功能,再根据时间实现 AI 辅助 SQL 生成。
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.hypersql</groupId>
|
||||
<artifactId>hypersql</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>HyperSql</name>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>24</maven.compiler.source>
|
||||
<maven.compiler.target>24</maven.compiler.target>
|
||||
<javafx.version>24.0.1</javafx.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-controls</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-fxml</artifactId>
|
||||
<version>${javafx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.49.1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>2.0.17</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.0</version>
|
||||
<configuration>
|
||||
<release>24</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-maven-plugin</artifactId>
|
||||
<version>0.0.8</version>
|
||||
<configuration>
|
||||
<mainClass>com.hypersql.HyperSqlApplication</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,45 @@
|
||||
# HyperSql
|
||||
|
||||
HyperSql 是一个基于 Java 24、JavaFX 和 Maven 开发的轻量级 SQLite 图形化数据库管理工具,主要面向学生和 SQLite 初学者。
|
||||
|
||||
## 已实现功能
|
||||
|
||||
- 新建、打开、关闭 SQLite 数据库文件
|
||||
- 删除当前 SQLite 数据库文件
|
||||
- 浏览数据库表和视图列表
|
||||
- 查看表结构信息
|
||||
- 查看表数据,并按每页 100 行分页显示
|
||||
- 执行 SQL 语句
|
||||
- SQL 查询结果分页显示
|
||||
- 通过图形化界面创建表
|
||||
- 删除选中的普通表
|
||||
- 表列表刷新时保留当前选中表
|
||||
- 基础状态提示、错误提示和危险操作确认
|
||||
|
||||
## 待实现功能
|
||||
|
||||
- 图形化新增、修改、删除表数据行
|
||||
- 数据库备份与恢复
|
||||
- AI 辅助 SQL 生成
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Java 24
|
||||
- JavaFX 24
|
||||
- Maven
|
||||
- SQLite
|
||||
- SQLite JDBC
|
||||
|
||||
## 运行方式
|
||||
|
||||
确保本机已安装 Java 24 和 Maven。
|
||||
|
||||
```bash
|
||||
mvn javafx:run
|
||||
```
|
||||
|
||||
## 项目文档
|
||||
|
||||
- `software_process_model.md`:软件开发过程模型
|
||||
- `feasibility_analysis.md`:可行性分析
|
||||
- `requirements_analysis.md`:需求分析
|
||||
@@ -0,0 +1,229 @@
|
||||
# HyperSql 需求分析
|
||||
|
||||
## 1. 需求分析概述
|
||||
|
||||
需求分析用于明确 HyperSql 要解决什么问题、服务哪些用户,以及系统需要提供哪些功能。
|
||||
|
||||
HyperSql 是一个面向学生和 SQLite 初学者的轻量级数据库图形化管理工具,主要解决以下问题:
|
||||
|
||||
1. SQLite 数据库虽然轻量易用,但命令行操作对初学者不够直观。
|
||||
2. 初学者手写 SQL 时容易出错,需要图形界面辅助查看数据库结构和数据。
|
||||
3. 数据库表结构和表数据需要以更直观的方式展示。
|
||||
4. 大表或较多查询结果需要分页显示,避免界面卡顿和阅读困难。
|
||||
5. 创建数据库、创建表、删除表等常见操作需要简洁清晰的图形界面。
|
||||
6. 删除数据库文件和删除表等危险操作需要确认流程,降低误操作风险。
|
||||
|
||||
当前版本已经完成了数据库文件管理、表/视图浏览、表结构查看、表数据分页查看、SQL 执行、SQL 结果分页、创建表和删除表等核心功能。数据行编辑、数据库备份恢复和 AI 辅助 SQL 生成作为后续扩展功能继续开发。
|
||||
|
||||
## 2. 用户需求分析
|
||||
|
||||
### 目标用户
|
||||
|
||||
| 用户类型 | 说明 |
|
||||
|---|---|
|
||||
| 学生 | 学习数据库课程,需要查看和操作 SQLite 数据库 |
|
||||
| SQLite 初学者 | 不熟悉命令行和复杂 SQL,希望通过图形界面管理数据库 |
|
||||
| 开发者学习者 | 在小型项目中使用 SQLite,需要快速查看本地数据和表结构 |
|
||||
|
||||
### 典型使用场景
|
||||
|
||||
1. 用户新建一个 SQLite 数据库文件用于课程实验。
|
||||
2. 用户打开一个已有的本地 SQLite 数据库文件。
|
||||
3. 用户查看数据库中有哪些表或视图。
|
||||
4. 用户选择某张表,查看字段结构和表数据。
|
||||
5. 用户通过分页按钮浏览大表数据。
|
||||
6. 用户在 SQL 执行区输入 SQL 并查看执行结果。
|
||||
7. 用户通过图形界面创建新表。
|
||||
8. 用户删除不需要的普通表,并在删除前进行确认。
|
||||
9. 用户刷新表列表,并希望保持当前选中的表不变。
|
||||
10. 后续版本中,用户可以通过图形界面编辑数据、备份恢复数据库,或使用 AI 辅助生成 SQL。
|
||||
|
||||
## 3. 功能性需求
|
||||
|
||||
HyperSql 的功能性需求主要包括以下模块:
|
||||
|
||||
| 功能模块 | 需求说明 | 当前状态 |
|
||||
|---|---|---|
|
||||
| 数据库文件管理 | 新建、打开、关闭和删除本地 SQLite 数据库文件 | 已实现 |
|
||||
| 数据库结构浏览 | 显示数据库表/视图列表,查看字段名、字段类型、主键、默认值等信息 | 已实现 |
|
||||
| 表数据查看 | 在表格中展示指定数据表内容,并按每页 100 行分页 | 已实现 |
|
||||
| SQL 执行 | 支持用户输入 SQL 语句并执行,显示结果或错误 | 已实现 |
|
||||
| SQL 结果分页 | 查询结果按每页 100 行分页显示 | 已实现 |
|
||||
| 表管理 | 支持通过 UI 创建表,支持删除选中普通表 | 已实现 |
|
||||
| 状态与错误提示 | 显示连接状态、执行结果和错误信息 | 已实现 |
|
||||
| 数据编辑 | 支持新增、修改、删除表数据行 | 待实现 |
|
||||
| 备份与恢复 | 支持数据库文件备份和从备份文件恢复 | 待实现 |
|
||||
| AI SQL 生成 | 根据表结构和用户自然语言需求生成 SQL | 待实现 |
|
||||
|
||||
## 4. 核心功能需求说明
|
||||
|
||||
### 4.1 数据库文件管理
|
||||
|
||||
- 系统应支持用户选择本地 SQLite 数据库文件并建立连接。
|
||||
- 系统应支持用户新建 SQLite 数据库文件。
|
||||
- 系统应支持关闭当前数据库连接。
|
||||
- 系统应支持删除当前连接的数据库文件,删除前必须提示用户确认。
|
||||
- 系统应显示当前数据库文件路径。
|
||||
|
||||
当前实现状态:已实现。
|
||||
|
||||
### 4.2 数据库结构浏览
|
||||
|
||||
- 系统应显示当前数据库中的表和视图。
|
||||
- 系统应支持刷新表/视图列表。
|
||||
- 刷新表/视图列表时,应尽量保留当前选中的表或视图。
|
||||
- 系统应支持查看字段信息。
|
||||
- 字段信息包括字段名、字段类型、是否主键、是否非空、默认值等。
|
||||
- 用户切换左侧表/视图时,表数据和表结构应同步更新。
|
||||
|
||||
当前实现状态:已实现,并已修复选择表时数据和结构不同步的问题。
|
||||
|
||||
### 4.3 表数据查看
|
||||
|
||||
- 系统应支持点击表名后查看表数据。
|
||||
- 系统应以表格形式展示数据。
|
||||
- 系统应支持分页显示表数据。
|
||||
- 每页默认显示 100 行。
|
||||
- 当数据不足 100 行时,应只显示一页。
|
||||
- 用户应能够通过“上一页”和“下一页”按钮切换页码。
|
||||
- 系统应显示当前页码、总页数、总行数和当前显示范围。
|
||||
|
||||
当前实现状态:已实现。表数据分页采用数据库侧分页,通过 `LIMIT` 和 `OFFSET` 每次只查询当前页数据。
|
||||
|
||||
### 4.4 SQL 执行
|
||||
|
||||
- 系统应提供 SQL 输入区域。
|
||||
- 系统应支持执行查询、插入、更新、删除、建表、删表等 SQL 语句。
|
||||
- 系统应显示 SQL 执行结果或错误信息。
|
||||
- 对于查询结果,系统应以表格形式显示。
|
||||
- 查询结果应支持分页显示,每页默认 100 行。
|
||||
- 对于非查询 SQL,系统应显示影响行数或执行结果信息。
|
||||
- 执行可能改变数据库结构的 SQL 后,系统应刷新表/视图列表并尽量保留当前选择。
|
||||
|
||||
当前实现状态:已实现。SQL 结果分页采用客户端分页,即 SQL 查询结果先读取到内存中,再按页显示。
|
||||
|
||||
### 4.5 创建表
|
||||
|
||||
- 系统应提供图形化创建表界面,不能只依赖用户手写 `CREATE TABLE`。
|
||||
- 创建表界面应简洁易用。
|
||||
- 用户应能输入表名。
|
||||
- 用户应能添加、删除字段。
|
||||
- 用户应能设置字段名、字段类型、主键、非空和默认值。
|
||||
- 系统应提供 SQL 预览。
|
||||
- 系统应校验表名不能为空,且不能与已有表或视图重名。
|
||||
- 系统应校验字段名不能为空,字段名不能重复。
|
||||
- 系统应限制字段类型为常用 SQLite 类型。
|
||||
- 系统应限制默认值格式,避免明显非法或危险 SQL 片段。
|
||||
- 创建成功后,应刷新表列表并选中新创建的表。
|
||||
|
||||
当前实现状态:已实现。
|
||||
|
||||
### 4.6 删除表
|
||||
|
||||
- 系统应支持删除左侧选中的普通表。
|
||||
- 系统不应通过“删除表”功能删除视图。
|
||||
- 删除表前必须弹出确认提示。
|
||||
- 删除成功后,应刷新表列表。
|
||||
- 删除表失败时,应显示错误信息。
|
||||
|
||||
当前实现状态:已实现。
|
||||
|
||||
## 5. 扩展功能需求说明
|
||||
|
||||
### 5.1 数据行编辑
|
||||
|
||||
- 系统后续应支持在图形界面中新增表数据。
|
||||
- 系统后续应支持修改表格中的数据。
|
||||
- 系统后续应支持删除选中的数据行。
|
||||
- 对修改和删除操作,应提供必要的确认或错误提示。
|
||||
- 数据编辑应优先保证主键识别和 SQL 生成安全。
|
||||
|
||||
当前实现状态:待实现。
|
||||
|
||||
### 5.2 数据库备份与恢复
|
||||
|
||||
- 系统后续应支持将当前数据库复制为备份文件。
|
||||
- 系统后续应支持用户选择备份文件进行恢复。
|
||||
- 系统应在恢复前提示用户确认,避免覆盖当前数据库。
|
||||
- 系统应显示备份或恢复的执行结果。
|
||||
|
||||
当前实现状态:待实现。
|
||||
|
||||
### 5.3 AI 辅助 SQL 生成
|
||||
|
||||
- 系统后续应读取当前数据库的表结构信息。
|
||||
- 系统后续应根据表结构自动生成 AI 提示词。
|
||||
- 用户可以输入自然语言需求。
|
||||
- 系统调用 AI API 生成 SQL 语句。
|
||||
- 用户可以检查并确认生成的 SQL 后再执行。
|
||||
- 若 AI 调用失败,系统应显示错误提示。
|
||||
- AI 生成的 SQL 不应直接自动执行,应由用户确认后执行。
|
||||
|
||||
当前实现状态:待实现。
|
||||
|
||||
## 6. 非功能性需求
|
||||
|
||||
| 需求类型 | 说明 | 当前体现 |
|
||||
|---|---|---|
|
||||
| 易用性 | 界面清晰,操作流程简单,适合初学者使用 | 已采用菜单、工具栏、表格和创建表对话框 |
|
||||
| 可靠性 | 数据库连接、SQL 执行和表操作失败时应有错误提示 | 已实现基础错误提示 |
|
||||
| 安全性 | 删除数据库、删除表等危险操作前应确认 | 已实现确认流程 |
|
||||
| 性能 | 对常见小型 SQLite 数据库能够快速打开和查询 | 表数据采用数据库侧分页,降低大表加载压力 |
|
||||
| 可维护性 | 采用模块化设计,便于后续扩展和维护 | 已划分数据库连接、元数据读取、SQL 执行、工具类和控制器 |
|
||||
| 兼容性 | 支持常见 SQLite 数据库文件,适配主流桌面系统 | 基于 JavaFX 和 SQLite JDBC,具备跨平台基础 |
|
||||
| 可扩展性 | 后续可扩展数据编辑、备份恢复和 AI SQL 生成 | 已保留 SQL 执行和元数据读取基础能力 |
|
||||
|
||||
## 7. 需求优先级
|
||||
|
||||
为了保证项目按时完成,HyperSql 将需求分为三个优先级。
|
||||
|
||||
| 优先级 | 功能 | 当前状态 |
|
||||
|---|---|---|
|
||||
| 高 | 新建/打开/关闭 SQLite 数据库、浏览表结构、查看表数据、执行 SQL | 已实现 |
|
||||
| 高 | 表数据分页、SQL 查询结果分页、刷新表列表保留当前选择 | 已实现 |
|
||||
| 中 | 创建表、删除表、删除数据库文件 | 已实现 |
|
||||
| 中 | 数据行新增、修改、删除 | 待实现 |
|
||||
| 中 | 数据库备份与恢复 | 待实现 |
|
||||
| 低 | AI 辅助 SQL 生成、SQL 历史记录、界面进一步美化 | 待实现 |
|
||||
|
||||
### 优先级说明
|
||||
|
||||
1. **高优先级需求**
|
||||
- 是系统最基本的功能,决定软件是否能够正常演示。
|
||||
- 当前已经完成。
|
||||
|
||||
2. **中优先级需求**
|
||||
- 提升软件实用性和数据安全性。
|
||||
- 当前已经完成创建表、删除表和删除数据库文件,后续应继续完成数据行编辑和备份恢复。
|
||||
|
||||
3. **低优先级需求**
|
||||
- 作为项目亮点和扩展功能。
|
||||
- 可根据开发进度调整实现程度。
|
||||
|
||||
## 8. 当前版本限制
|
||||
|
||||
当前 HyperSql 仍存在以下限制:
|
||||
|
||||
1. 还不能通过图形界面直接新增、修改或删除表中的数据行。
|
||||
2. 还没有实现数据库备份与恢复功能。
|
||||
3. 还没有实现 AI 辅助 SQL 生成功能。
|
||||
4. SQL 查询结果分页属于客户端分页,查询结果会先全部读取到内存中,不适合特别大的查询结果。
|
||||
5. 当前删除表功能只支持删除普通表,不支持删除视图。
|
||||
6. 当前主要面向 SQLite,不支持 MySQL、PostgreSQL 等远程数据库。
|
||||
|
||||
## 9. 需求分析总结
|
||||
|
||||
通过需求分析可以看出,HyperSql 的主要目标是为学生和 SQLite 初学者提供一个简单、直观、易用的数据库图形化管理工具。
|
||||
|
||||
当前系统已经重点实现:
|
||||
|
||||
1. SQLite 数据库文件新建、打开、关闭和删除。
|
||||
2. 数据库表/视图列表展示。
|
||||
3. 表结构和表数据的可视化展示。
|
||||
4. 表数据分页显示。
|
||||
5. SQL 语句执行和 SQL 查询结果分页显示。
|
||||
6. 图形化创建表。
|
||||
7. 删除普通表。
|
||||
8. 状态提示、错误提示和危险操作确认。
|
||||
|
||||
后续开发应优先完成数据行增删改查和数据库备份恢复,使系统从“可浏览、可执行 SQL、可管理表结构”进一步发展为“可完整管理 SQLite 数据库”的工具。在时间允许的情况下,再实现 AI 辅助 SQL 生成作为项目亮点。
|
||||
@@ -0,0 +1,113 @@
|
||||
# HyperSql 软件开发过程模型
|
||||
|
||||
## 1. 软件开发过程模型选择
|
||||
|
||||
本项目 **HyperSql** 采用 **增量迭代模型** 进行开发。
|
||||
|
||||
增量迭代模型是指将软件系统划分为多个相对独立的功能增量,每个增量都经过需求分析、设计、编码、测试和反馈优化等过程,最终逐步形成完整的软件系统。
|
||||
|
||||
HyperSql 是一个基于 Java 24、JavaFX 和 Maven 的轻量级 SQLite 图形化管理工具。项目开发过程中,先完成数据库连接、表结构浏览、表数据查看和 SQL 执行等核心功能,再逐步增加数据库文件管理、表管理、分页显示等增强功能,最后根据时间继续扩展数据编辑、备份恢复和 AI 辅助 SQL 生成。
|
||||
|
||||
## 2. 选择增量迭代模型的原因
|
||||
|
||||
HyperSql 选择增量迭代模型,主要有以下原因:
|
||||
|
||||
1. **功能模块清晰**
|
||||
- HyperSql 可以划分为数据库文件管理、数据库连接、表/视图浏览、表结构查看、表数据分页查看、SQL 执行、表管理、数据编辑、备份恢复和 AI SQL 生成等模块。
|
||||
|
||||
2. **适合 GUI 软件开发**
|
||||
- JavaFX 图形界面需要不断调整布局和交互方式。采用迭代开发,可以在每个阶段完成可运行版本,并根据测试结果优化用户体验。
|
||||
|
||||
3. **降低开发风险**
|
||||
- 先完成基础功能,再开发复杂功能,可以避免一开始实现过多内容导致项目失控。
|
||||
|
||||
4. **便于阶段性展示**
|
||||
- 每个迭代版本都能形成可运行的软件,方便课程汇报、阶段检查和最终演示。
|
||||
|
||||
5. **适合课程项目周期**
|
||||
- 在有限时间内优先保证核心功能可用,再根据进度实现扩展功能。
|
||||
|
||||
## 3. HyperSql 当前迭代版本与进展
|
||||
|
||||
| 迭代版本 | 主要目标 | 主要功能 | 当前状态 |
|
||||
|---|---|---|---|
|
||||
| V0.1 | 项目准备与原型设计 | 明确需求、完成过程模型/可行性分析/需求分析、搭建 Maven + JavaFX 项目结构 | 已完成 |
|
||||
| V1.0 | 基础数据库管理 | 打开本地 SQLite 数据库、建立连接、显示数据库文件路径、关闭数据库连接 | 已完成 |
|
||||
| V1.1 | 数据库结构浏览 | 显示表/视图列表,查看字段名、类型、主键、默认值等信息 | 已完成 |
|
||||
| V1.2 | 表数据查看与 SQL 执行 | 查看表数据,输入并执行 SQL,显示查询结果或更新结果 | 已完成 |
|
||||
| V1.3 | 分页显示优化 | 表数据按每页 100 行分页显示;SQL 查询结果区按每页 100 行分页显示 | 已完成 |
|
||||
| V1.4 | 数据库文件管理增强 | 新建 SQLite 数据库文件、删除当前数据库文件 | 已完成 |
|
||||
| V1.5 | 表管理功能 | 通过简洁 UI 创建表;删除选中普通表;删除前进行确认 | 已完成 |
|
||||
| V1.6 | 交互与缺陷修复 | 修复表/视图选择时数据与结构不同步问题;刷新表列表时保留当前选中表 | 已完成 |
|
||||
| V1.7 | 数据行增删改查 | 在图形界面中新增、修改、删除表数据 | 待实现 |
|
||||
| V1.8 | 备份与恢复 | 数据库文件备份、从备份恢复数据库 | 待实现 |
|
||||
| V1.9 | AI 辅助 SQL 生成 | 根据表结构和用户自然语言需求调用 AI API 生成 SQL | 待实现 |
|
||||
| V2.0 | 测试与优化 | 完善异常处理、界面优化、系统测试和最终演示准备 | 持续进行 |
|
||||
|
||||
## 4. 单次迭代流程
|
||||
|
||||
每一个功能迭代都按照以下流程进行:
|
||||
|
||||
```text
|
||||
需求细化
|
||||
↓
|
||||
界面与功能设计
|
||||
↓
|
||||
编码实现
|
||||
↓
|
||||
功能测试
|
||||
↓
|
||||
问题修复
|
||||
↓
|
||||
版本集成
|
||||
↓
|
||||
小组评审与反馈
|
||||
```
|
||||
|
||||
### 具体说明
|
||||
|
||||
1. **需求细化**
|
||||
- 明确本次迭代要完成的功能和边界。例如创建表功能要求有简洁易用的 UI,而不是只让用户手写 SQL。
|
||||
|
||||
2. **界面与功能设计**
|
||||
- 设计 JavaFX 界面布局和功能交互流程。例如主界面采用左侧表/视图列表、右侧表数据/表结构/SQL 执行区域的布局。
|
||||
|
||||
3. **编码实现**
|
||||
- 使用 Java 24、JavaFX、SQLite JDBC 和 Maven 完成功能开发。
|
||||
|
||||
4. **功能测试**
|
||||
- 测试数据库连接、表结构读取、分页显示、SQL 执行、创建数据库、删除数据库、创建表和删除表等功能是否正确。
|
||||
|
||||
5. **问题修复**
|
||||
- 根据测试结果修复异常、界面错误和逻辑问题。例如修复刷新表列表后错误切换到第一张表的问题。
|
||||
|
||||
6. **版本集成**
|
||||
- 将本次迭代功能整合到主程序中,保证新功能不会影响已有功能。
|
||||
|
||||
7. **评审与反馈**
|
||||
- 检查功能完成情况,并决定下一次迭代内容。
|
||||
|
||||
## 5. 增量迭代模型在 HyperSql 中的体现
|
||||
|
||||
目前 HyperSql 的开发过程已经体现出增量迭代模型的特点:
|
||||
|
||||
1. **先完成核心功能**
|
||||
- 项目首先实现了 SQLite 数据库打开、表/视图列表显示、表结构查看、表数据查看和 SQL 执行,保证系统能够运行和演示。
|
||||
|
||||
2. **逐步增强可用性**
|
||||
- 在基础数据查看功能完成后,进一步加入了表数据分页和 SQL 结果分页,避免大结果集造成界面显示压力。
|
||||
|
||||
3. **根据实际测试补充功能**
|
||||
- 因为用户没有现成数据库文件,项目增加了新建数据库和删除数据库功能,方便测试和演示。
|
||||
|
||||
4. **通过反馈修复问题**
|
||||
- 在使用过程中发现表/视图选择和刷新逻辑存在体验问题后,及时进行了修复,保证表数据和表结构能够同步更新,并且刷新表列表时保留当前选择。
|
||||
|
||||
5. **保留后续扩展空间**
|
||||
- 数据行增删改查、数据库备份恢复和 AI SQL 生成尚未完成,后续可作为新的增量继续实现。
|
||||
|
||||
## 6. 总结
|
||||
|
||||
HyperSql 采用增量迭代模型进行开发是合适的。当前项目已经完成了 SQLite 数据库管理工具的核心可运行版本,包括数据库文件管理、表/视图浏览、表结构查看、表数据分页查看、SQL 执行分页显示、创建表和删除表等功能。
|
||||
|
||||
后续开发应继续沿用增量迭代方式,优先完善数据行增删改查和备份恢复等实用功能,再根据课程进度实现 AI 辅助 SQL 生成等扩展功能。
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.hypersql;
|
||||
|
||||
import com.hypersql.controller.MainController;
|
||||
import javafx.application.Application;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class HyperSqlApplication extends Application {
|
||||
@Override
|
||||
public void start(Stage stage) throws IOException {
|
||||
FXMLLoader loader = new FXMLLoader(HyperSqlApplication.class.getResource("/com/hypersql/main-view.fxml"));
|
||||
Scene scene = new Scene(loader.load(), 1180, 760);
|
||||
scene.getStylesheets().add(HyperSqlApplication.class.getResource("/com/hypersql/style.css").toExternalForm());
|
||||
|
||||
MainController controller = loader.getController();
|
||||
stage.setTitle("HyperSql");
|
||||
stage.setScene(scene);
|
||||
stage.setMinWidth(960);
|
||||
stage.setMinHeight(640);
|
||||
stage.setOnCloseRequest(_ -> controller.shutdown());
|
||||
stage.show();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
launch(args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
package com.hypersql.controller;
|
||||
|
||||
import com.hypersql.util.SqlUtils;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.cell.CheckBoxTableCell;
|
||||
import javafx.scene.control.cell.ComboBoxTableCell;
|
||||
import javafx.scene.control.cell.TextFieldTableCell;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class CreateTableDialogController {
|
||||
private static final List<String> ALLOWED_TYPES = List.of(
|
||||
"INTEGER", "TEXT", "REAL", "BLOB", "NUMERIC", "BOOLEAN", "DATE", "DATETIME"
|
||||
);
|
||||
private static final Pattern NUMBER_PATTERN = Pattern.compile("-?\\d+(\\.\\d+)?");
|
||||
private static final Pattern STRING_LITERAL_PATTERN = Pattern.compile("'([^']|'')*'");
|
||||
|
||||
@FXML
|
||||
private TextField tableNameField;
|
||||
|
||||
@FXML
|
||||
private TableView<ColumnDefinition> columnsTable;
|
||||
|
||||
@FXML
|
||||
private TableColumn<ColumnDefinition, String> columnNameColumn;
|
||||
|
||||
@FXML
|
||||
private TableColumn<ColumnDefinition, String> columnTypeColumn;
|
||||
|
||||
@FXML
|
||||
private TableColumn<ColumnDefinition, Boolean> primaryKeyColumn;
|
||||
|
||||
@FXML
|
||||
private TableColumn<ColumnDefinition, Boolean> notNullColumn;
|
||||
|
||||
@FXML
|
||||
private TableColumn<ColumnDefinition, String> defaultValueColumn;
|
||||
|
||||
@FXML
|
||||
private TextArea sqlPreviewTextArea;
|
||||
|
||||
@FXML
|
||||
private Label messageLabel;
|
||||
|
||||
private final Set<String> existingObjectNames = new HashSet<>();
|
||||
private boolean confirmed;
|
||||
private String createdTableName;
|
||||
private String generatedSql;
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
configureColumnsTable();
|
||||
columnsTable.setItems(FXCollections.observableArrayList(
|
||||
new ColumnDefinition("id", "INTEGER", true, true, ""),
|
||||
new ColumnDefinition("name", "TEXT", false, false, "")
|
||||
));
|
||||
tableNameField.textProperty().addListener((_, _, _) -> updateSqlPreview());
|
||||
updateSqlPreview();
|
||||
}
|
||||
|
||||
public void setExistingObjectNames(Collection<String> names) {
|
||||
existingObjectNames.clear();
|
||||
names.stream()
|
||||
.map(name -> name.trim().toLowerCase(Locale.ROOT))
|
||||
.forEach(existingObjectNames::add);
|
||||
}
|
||||
|
||||
public boolean isConfirmed() {
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
public String getCreatedTableName() {
|
||||
return createdTableName;
|
||||
}
|
||||
|
||||
public String getGeneratedSql() {
|
||||
return generatedSql;
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleAddColumn() {
|
||||
ColumnDefinition column = new ColumnDefinition("", "TEXT", false, false, "");
|
||||
columnsTable.getItems().add(column);
|
||||
columnsTable.getSelectionModel().select(column);
|
||||
columnsTable.scrollTo(column);
|
||||
updateSqlPreview();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleRemoveColumn() {
|
||||
ColumnDefinition selectedColumn = columnsTable.getSelectionModel().getSelectedItem();
|
||||
if (selectedColumn == null) {
|
||||
messageLabel.setText("请先选择要删除的列。");
|
||||
return;
|
||||
}
|
||||
if (columnsTable.getItems().size() <= 1) {
|
||||
messageLabel.setText("至少需要保留一列。");
|
||||
return;
|
||||
}
|
||||
columnsTable.getItems().remove(selectedColumn);
|
||||
updateSqlPreview();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleCreate() {
|
||||
List<String> errors = validateInput();
|
||||
if (!errors.isEmpty()) {
|
||||
messageLabel.setText(String.join("\n", errors));
|
||||
updateSqlPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
createdTableName = tableNameField.getText().trim();
|
||||
generatedSql = buildCreateTableSql();
|
||||
confirmed = true;
|
||||
closeWindow();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleCancel() {
|
||||
confirmed = false;
|
||||
closeWindow();
|
||||
}
|
||||
|
||||
private void configureColumnsTable() {
|
||||
columnsTable.setEditable(true);
|
||||
|
||||
columnNameColumn.setCellValueFactory(data -> data.getValue().nameProperty());
|
||||
columnNameColumn.setCellFactory(TextFieldTableCell.forTableColumn());
|
||||
columnNameColumn.setOnEditCommit(event -> {
|
||||
event.getRowValue().setName(event.getNewValue());
|
||||
updateSqlPreview();
|
||||
});
|
||||
|
||||
columnTypeColumn.setCellValueFactory(data -> data.getValue().typeProperty());
|
||||
columnTypeColumn.setCellFactory(ComboBoxTableCell.forTableColumn(ALLOWED_TYPES.toArray(String[]::new)));
|
||||
columnTypeColumn.setOnEditCommit(event -> {
|
||||
event.getRowValue().setType(event.getNewValue());
|
||||
updateSqlPreview();
|
||||
});
|
||||
|
||||
primaryKeyColumn.setCellValueFactory(data -> data.getValue().primaryKeyProperty());
|
||||
primaryKeyColumn.setCellFactory(CheckBoxTableCell.forTableColumn(primaryKeyColumn));
|
||||
primaryKeyColumn.setCellFactory(column -> new TableCell<>() {
|
||||
private final javafx.scene.control.CheckBox checkBox = new javafx.scene.control.CheckBox();
|
||||
|
||||
{
|
||||
checkBox.setOnAction(_ -> {
|
||||
ColumnDefinition row = getTableView().getItems().get(getIndex());
|
||||
if (checkBox.isSelected()) {
|
||||
for (ColumnDefinition item : getTableView().getItems()) {
|
||||
item.setPrimaryKey(item == row);
|
||||
}
|
||||
row.setNotNull(true);
|
||||
} else {
|
||||
row.setPrimaryKey(false);
|
||||
}
|
||||
getTableView().refresh();
|
||||
updateSqlPreview();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Boolean item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
checkBox.setSelected(Boolean.TRUE.equals(item));
|
||||
setGraphic(checkBox);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
notNullColumn.setCellValueFactory(data -> data.getValue().notNullProperty());
|
||||
notNullColumn.setCellFactory(CheckBoxTableCell.forTableColumn(notNullColumn));
|
||||
notNullColumn.setOnEditCommit(event -> {
|
||||
event.getRowValue().setNotNull(Boolean.TRUE.equals(event.getNewValue()));
|
||||
updateSqlPreview();
|
||||
});
|
||||
|
||||
defaultValueColumn.setCellValueFactory(data -> data.getValue().defaultValueProperty());
|
||||
defaultValueColumn.setCellFactory(TextFieldTableCell.forTableColumn(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fromString(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
}));
|
||||
defaultValueColumn.setOnEditCommit(event -> {
|
||||
event.getRowValue().setDefaultValue(event.getNewValue());
|
||||
updateSqlPreview();
|
||||
});
|
||||
}
|
||||
|
||||
private List<String> validateInput() {
|
||||
Set<String> columnNames = new HashSet<>();
|
||||
List<String> errors = new java.util.ArrayList<>();
|
||||
String tableName = tableNameField.getText() == null ? "" : tableNameField.getText().trim();
|
||||
|
||||
if (tableName.isEmpty()) {
|
||||
errors.add("请输入表名。");
|
||||
} else if (existingObjectNames.contains(tableName.toLowerCase(Locale.ROOT))) {
|
||||
errors.add("已存在同名表或视图,请更换表名。");
|
||||
}
|
||||
|
||||
if (columnsTable.getItems().isEmpty()) {
|
||||
errors.add("至少需要定义一列。");
|
||||
}
|
||||
|
||||
int primaryKeyCount = 0;
|
||||
for (ColumnDefinition column : columnsTable.getItems()) {
|
||||
String columnName = column.getName().trim();
|
||||
String type = column.getType().trim().toUpperCase(Locale.ROOT);
|
||||
String defaultValue = column.getDefaultValue().trim();
|
||||
|
||||
if (columnName.isEmpty()) {
|
||||
errors.add("列名不能为空。");
|
||||
} else if (!columnNames.add(columnName.toLowerCase(Locale.ROOT))) {
|
||||
errors.add("列名重复:" + columnName);
|
||||
}
|
||||
|
||||
if (!ALLOWED_TYPES.contains(type)) {
|
||||
errors.add("不支持的字段类型:" + column.getType());
|
||||
}
|
||||
|
||||
if (column.isPrimaryKey()) {
|
||||
primaryKeyCount++;
|
||||
}
|
||||
|
||||
if (!defaultValue.isEmpty() && !isSafeDefaultValue(defaultValue)) {
|
||||
errors.add("默认值不合法:" + defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (primaryKeyCount > 1) {
|
||||
errors.add("只能设置一个主键字段。");
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
private void updateSqlPreview() {
|
||||
try {
|
||||
if (validateInput().isEmpty()) {
|
||||
sqlPreviewTextArea.setText(buildCreateTableSql());
|
||||
messageLabel.setText("检查无误后点击创建。");
|
||||
} else {
|
||||
sqlPreviewTextArea.setText("请填写有效的表名和列定义。");
|
||||
}
|
||||
} catch (RuntimeException _) {
|
||||
sqlPreviewTextArea.setText("请填写有效的表名和列定义。");
|
||||
}
|
||||
}
|
||||
|
||||
private String buildCreateTableSql() {
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("CREATE TABLE ")
|
||||
.append(SqlUtils.quoteIdentifier(tableNameField.getText().trim()))
|
||||
.append(" (\n");
|
||||
|
||||
for (int i = 0; i < columnsTable.getItems().size(); i++) {
|
||||
ColumnDefinition column = columnsTable.getItems().get(i);
|
||||
sql.append(" ")
|
||||
.append(SqlUtils.quoteIdentifier(column.getName().trim()))
|
||||
.append(" ")
|
||||
.append(column.getType().trim().toUpperCase(Locale.ROOT));
|
||||
if (column.isPrimaryKey()) {
|
||||
sql.append(" PRIMARY KEY");
|
||||
}
|
||||
if (column.isNotNull() || column.isPrimaryKey()) {
|
||||
sql.append(" NOT NULL");
|
||||
}
|
||||
String defaultValue = column.getDefaultValue().trim();
|
||||
if (!defaultValue.isEmpty()) {
|
||||
sql.append(" DEFAULT ").append(defaultValue);
|
||||
}
|
||||
if (i < columnsTable.getItems().size() - 1) {
|
||||
sql.append(",");
|
||||
}
|
||||
sql.append("\n");
|
||||
}
|
||||
sql.append(")");
|
||||
return sql.toString();
|
||||
}
|
||||
|
||||
private boolean isSafeDefaultValue(String value) {
|
||||
String trimmed = value.trim();
|
||||
String upper = trimmed.toUpperCase(Locale.ROOT);
|
||||
if (trimmed.contains(";") || trimmed.contains("--") || trimmed.contains("/*") || trimmed.contains("*/")) {
|
||||
return false;
|
||||
}
|
||||
return upper.equals("NULL")
|
||||
|| upper.equals("CURRENT_TIME")
|
||||
|| upper.equals("CURRENT_DATE")
|
||||
|| upper.equals("CURRENT_TIMESTAMP")
|
||||
|| NUMBER_PATTERN.matcher(trimmed).matches()
|
||||
|| STRING_LITERAL_PATTERN.matcher(trimmed).matches();
|
||||
}
|
||||
|
||||
private void closeWindow() {
|
||||
((Stage) tableNameField.getScene().getWindow()).close();
|
||||
}
|
||||
|
||||
public static final class ColumnDefinition {
|
||||
private final StringProperty name = new SimpleStringProperty();
|
||||
private final StringProperty type = new SimpleStringProperty();
|
||||
private final BooleanProperty primaryKey = new SimpleBooleanProperty();
|
||||
private final BooleanProperty notNull = new SimpleBooleanProperty();
|
||||
private final StringProperty defaultValue = new SimpleStringProperty();
|
||||
|
||||
public ColumnDefinition(String name, String type, boolean primaryKey, boolean notNull, String defaultValue) {
|
||||
setName(name);
|
||||
setType(type);
|
||||
setPrimaryKey(primaryKey);
|
||||
setNotNull(notNull);
|
||||
setDefaultValue(defaultValue);
|
||||
}
|
||||
|
||||
public StringProperty nameProperty() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name.get() == null ? "" : name.get();
|
||||
}
|
||||
|
||||
public void setName(String value) {
|
||||
name.set(value == null ? "" : value);
|
||||
}
|
||||
|
||||
public StringProperty typeProperty() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type.get() == null ? "TEXT" : type.get();
|
||||
}
|
||||
|
||||
public void setType(String value) {
|
||||
type.set(value == null || value.isBlank() ? "TEXT" : value);
|
||||
}
|
||||
|
||||
public BooleanProperty primaryKeyProperty() {
|
||||
return primaryKey;
|
||||
}
|
||||
|
||||
public boolean isPrimaryKey() {
|
||||
return primaryKey.get();
|
||||
}
|
||||
|
||||
public void setPrimaryKey(boolean value) {
|
||||
primaryKey.set(value);
|
||||
}
|
||||
|
||||
public BooleanProperty notNullProperty() {
|
||||
return notNull;
|
||||
}
|
||||
|
||||
public boolean isNotNull() {
|
||||
return notNull.get();
|
||||
}
|
||||
|
||||
public void setNotNull(boolean value) {
|
||||
notNull.set(value);
|
||||
}
|
||||
|
||||
public StringProperty defaultValueProperty() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public String getDefaultValue() {
|
||||
return defaultValue.get() == null ? "" : defaultValue.get();
|
||||
}
|
||||
|
||||
public void setDefaultValue(String value) {
|
||||
defaultValue.set(value == null ? "" : value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,659 @@
|
||||
package com.hypersql.controller;
|
||||
|
||||
import com.hypersql.db.DatabaseConnectionManager;
|
||||
import com.hypersql.db.DatabaseMetadataService;
|
||||
import com.hypersql.db.SqlExecutionService;
|
||||
import com.hypersql.model.ColumnInfo;
|
||||
import com.hypersql.model.QueryResult;
|
||||
import com.hypersql.model.TableInfo;
|
||||
import com.hypersql.HyperSqlApplication;
|
||||
import com.hypersql.util.DialogUtils;
|
||||
import com.hypersql.util.SqlUtils;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MainController {
|
||||
private static final int TABLE_PAGE_SIZE = 100;
|
||||
private static final int SQL_PAGE_SIZE = 100;
|
||||
|
||||
@FXML
|
||||
private Label databasePathLabel;
|
||||
|
||||
@FXML
|
||||
private ListView<TableInfo> tableListView;
|
||||
|
||||
@FXML
|
||||
private TabPane mainTabPane;
|
||||
|
||||
@FXML
|
||||
private TableView<ObservableList<Object>> tableDataView;
|
||||
|
||||
@FXML
|
||||
private Button previousTablePageButton;
|
||||
|
||||
@FXML
|
||||
private Button nextTablePageButton;
|
||||
|
||||
@FXML
|
||||
private Label tablePageLabel;
|
||||
|
||||
@FXML
|
||||
private TableView<ColumnInfo> tableStructureView;
|
||||
|
||||
@FXML
|
||||
private TextArea sqlTextArea;
|
||||
|
||||
@FXML
|
||||
private Label sqlMessageLabel;
|
||||
|
||||
@FXML
|
||||
private TableView<ObservableList<Object>> sqlResultView;
|
||||
|
||||
@FXML
|
||||
private Button previousSqlPageButton;
|
||||
|
||||
@FXML
|
||||
private Button nextSqlPageButton;
|
||||
|
||||
@FXML
|
||||
private Label sqlPageLabel;
|
||||
|
||||
@FXML
|
||||
private Label statusLabel;
|
||||
|
||||
private final DatabaseConnectionManager connectionManager = new DatabaseConnectionManager();
|
||||
private final DatabaseMetadataService metadataService = new DatabaseMetadataService();
|
||||
private final SqlExecutionService sqlExecutionService = new SqlExecutionService();
|
||||
private TableInfo currentTable;
|
||||
private long currentTableTotalRows;
|
||||
private long currentTablePageIndex;
|
||||
private QueryResult currentSqlResult;
|
||||
private long currentSqlPageIndex;
|
||||
private boolean selectingTableProgrammatically;
|
||||
|
||||
@FXML
|
||||
private void initialize() {
|
||||
sqlTextArea.setText("SELECT * FROM sqlite_master;");
|
||||
configureStructureTable();
|
||||
tableListView.getSelectionModel().selectedItemProperty().addListener((_, _, selectedTable) -> {
|
||||
if (selectingTableProgrammatically) {
|
||||
return;
|
||||
}
|
||||
if (selectedTable == null) {
|
||||
clearSelectedTableUi();
|
||||
} else {
|
||||
loadSelectedTable(selectedTable);
|
||||
}
|
||||
});
|
||||
updateDisconnectedUi();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleCreateDatabase() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("新建 SQLite 数据库");
|
||||
fileChooser.setInitialFileName("new-database.db");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("SQLite 数据库", "*.db", "*.sqlite", "*.sqlite3"),
|
||||
new FileChooser.ExtensionFilter("所有文件", "*.*")
|
||||
);
|
||||
|
||||
File selectedFile = fileChooser.showSaveDialog(currentWindow());
|
||||
if (selectedFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path databasePath = selectedFile.toPath().toAbsolutePath().normalize();
|
||||
if (Files.exists(databasePath)) {
|
||||
DialogUtils.showError("新建数据库失败", "文件已存在,请使用打开数据库功能:\n" + databasePath);
|
||||
status("新建数据库失败");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
connectionManager.open(databasePath);
|
||||
databasePathLabel.setText(databasePath.toString());
|
||||
status("已新建数据库");
|
||||
refreshTables();
|
||||
DialogUtils.showInfo("新建数据库成功", "已创建并连接数据库:\n" + databasePath);
|
||||
} catch (SQLException e) {
|
||||
updateDisconnectedUi();
|
||||
DialogUtils.showError("新建数据库失败", e.getMessage());
|
||||
status("新建数据库失败");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleOpenDatabase() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("打开 SQLite 数据库");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("SQLite 数据库", "*.db", "*.sqlite", "*.sqlite3"),
|
||||
new FileChooser.ExtensionFilter("所有文件", "*.*")
|
||||
);
|
||||
|
||||
File selectedFile = fileChooser.showOpenDialog(currentWindow());
|
||||
if (selectedFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
connectionManager.open(selectedFile.toPath());
|
||||
databasePathLabel.setText(selectedFile.toPath().toAbsolutePath().normalize().toString());
|
||||
status("已连接数据库");
|
||||
refreshTables();
|
||||
} catch (SQLException e) {
|
||||
updateDisconnectedUi();
|
||||
DialogUtils.showError("打开数据库失败", e.getMessage());
|
||||
status("打开数据库失败");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleDeleteCurrentDatabase() {
|
||||
Optional<Path> currentDatabasePath = connectionManager.getCurrentDatabasePath();
|
||||
if (currentDatabasePath.isEmpty()) {
|
||||
DialogUtils.showError("未连接数据库", "请先打开或新建一个 SQLite 数据库文件。");
|
||||
status("未连接数据库");
|
||||
return;
|
||||
}
|
||||
|
||||
Path databasePath = currentDatabasePath.get();
|
||||
boolean confirmed = DialogUtils.confirm(
|
||||
"确认删除数据库文件",
|
||||
"将关闭连接并永久删除以下 SQLite 数据库文件:\n\n"
|
||||
+ databasePath
|
||||
+ "\n\n此操作不可撤销,是否继续?"
|
||||
);
|
||||
if (!confirmed) {
|
||||
status("已取消删除数据库");
|
||||
return;
|
||||
}
|
||||
|
||||
connectionManager.close();
|
||||
try {
|
||||
Files.delete(databasePath);
|
||||
updateDisconnectedUi();
|
||||
DialogUtils.showInfo("删除数据库成功", "已删除数据库文件:\n" + databasePath);
|
||||
status("已删除数据库");
|
||||
} catch (IOException e) {
|
||||
updateDisconnectedUi();
|
||||
DialogUtils.showError("删除数据库失败", "数据库连接已关闭,但文件删除失败:\n" + e.getMessage());
|
||||
status("删除数据库失败");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleCloseDatabase() {
|
||||
connectionManager.close();
|
||||
updateDisconnectedUi();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleRefreshTables() {
|
||||
if (!ensureConnected()) {
|
||||
return;
|
||||
}
|
||||
refreshTablesPreservingSelection();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleCreateTable() {
|
||||
if (!ensureConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
FXMLLoader loader = new FXMLLoader(HyperSqlApplication.class.getResource("/com/hypersql/create-table-dialog.fxml"));
|
||||
Scene scene = new Scene(loader.load(), 780, 560);
|
||||
scene.getStylesheets().add(HyperSqlApplication.class.getResource("/com/hypersql/style.css").toExternalForm());
|
||||
|
||||
CreateTableDialogController controller = loader.getController();
|
||||
Set<String> existingNames = tableListView.getItems().stream()
|
||||
.map(TableInfo::name)
|
||||
.collect(Collectors.toSet());
|
||||
controller.setExistingObjectNames(existingNames);
|
||||
|
||||
Stage dialog = new Stage();
|
||||
dialog.setTitle("创建表");
|
||||
dialog.initOwner(currentWindow());
|
||||
dialog.initModality(Modality.WINDOW_MODAL);
|
||||
dialog.setScene(scene);
|
||||
dialog.setMinWidth(720);
|
||||
dialog.setMinHeight(520);
|
||||
dialog.showAndWait();
|
||||
|
||||
if (!controller.isConfirmed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
sqlExecutionService.execute(connection(), controller.getGeneratedSql());
|
||||
refreshTablesSelecting(controller.getCreatedTableName());
|
||||
DialogUtils.showInfo("创建表成功", "已创建表:" + controller.getCreatedTableName());
|
||||
status("已创建表 " + controller.getCreatedTableName());
|
||||
} catch (IOException | SQLException | IllegalArgumentException e) {
|
||||
DialogUtils.showError("创建表失败", e.getMessage());
|
||||
status("创建表失败");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleDeleteSelectedTable() {
|
||||
if (!ensureConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
TableInfo selectedTable = tableListView.getSelectionModel().getSelectedItem();
|
||||
if (selectedTable == null) {
|
||||
DialogUtils.showError("未选择表", "请先在左侧选择要删除的表。");
|
||||
return;
|
||||
}
|
||||
if (!"table".equalsIgnoreCase(selectedTable.type())) {
|
||||
DialogUtils.showError("不能删除视图", "当前选中的是视图,不支持通过删除表功能删除。请在 SQL 执行页使用 DROP VIEW。");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean confirmed = DialogUtils.confirm(
|
||||
"确认删除表",
|
||||
"将永久删除表:\n\n"
|
||||
+ selectedTable.name()
|
||||
+ "\n\n此操作会删除表结构和全部数据,且不可撤销。是否继续?"
|
||||
);
|
||||
if (!confirmed) {
|
||||
status("已取消删除表");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String sql = "DROP TABLE " + SqlUtils.quoteIdentifier(selectedTable.name());
|
||||
sqlExecutionService.execute(connection(), sql);
|
||||
refreshTables();
|
||||
DialogUtils.showInfo("删除表成功", "已删除表:" + selectedTable.name());
|
||||
status("已删除表 " + selectedTable.name());
|
||||
} catch (SQLException | IllegalArgumentException e) {
|
||||
DialogUtils.showError("删除表失败", e.getMessage());
|
||||
status("删除表失败");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleExecuteSql() {
|
||||
if (!ensureConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String sql = sqlTextArea.getText();
|
||||
if (sql == null || sql.isBlank()) {
|
||||
DialogUtils.showError("SQL 不能为空", "请输入需要执行的 SQL 语句。");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
QueryResult result = sqlExecutionService.execute(connection(), sql);
|
||||
if (result.resultSet()) {
|
||||
currentSqlResult = result;
|
||||
currentSqlPageIndex = 0;
|
||||
renderCurrentSqlPage();
|
||||
sqlMessageLabel.setText(result.message());
|
||||
status("SQL 执行成功,返回 " + result.rows().size() + " 行");
|
||||
} else {
|
||||
clearTable(sqlResultView);
|
||||
resetSqlPaginationUi();
|
||||
sqlMessageLabel.setText(result.message());
|
||||
status("SQL 执行成功," + result.message());
|
||||
refreshTablesPreservingSelection();
|
||||
}
|
||||
mainTabPane.getSelectionModel().select(2);
|
||||
} catch (SQLException | IllegalArgumentException e) {
|
||||
sqlMessageLabel.setText(e.getMessage());
|
||||
DialogUtils.showError("SQL 执行失败", e.getMessage());
|
||||
status("SQL 执行失败");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handlePreviousTablePage() {
|
||||
if (currentTable == null || currentTablePageIndex <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
currentTablePageIndex--;
|
||||
loadCurrentTablePage();
|
||||
} catch (SQLException e) {
|
||||
DialogUtils.showError("加载上一页失败", e.getMessage());
|
||||
status("加载上一页失败");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleNextTablePage() {
|
||||
if (currentTable == null || currentTablePageIndex >= totalTablePages() - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
currentTablePageIndex++;
|
||||
loadCurrentTablePage();
|
||||
} catch (SQLException e) {
|
||||
DialogUtils.showError("加载下一页失败", e.getMessage());
|
||||
status("加载下一页失败");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handlePreviousSqlPage() {
|
||||
if (currentSqlResult == null || currentSqlPageIndex <= 0) {
|
||||
return;
|
||||
}
|
||||
currentSqlPageIndex--;
|
||||
renderCurrentSqlPage();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleNextSqlPage() {
|
||||
if (currentSqlResult == null || currentSqlPageIndex >= totalSqlPages() - 1) {
|
||||
return;
|
||||
}
|
||||
currentSqlPageIndex++;
|
||||
renderCurrentSqlPage();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void handleExit() {
|
||||
shutdown();
|
||||
currentWindow().hide();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
connectionManager.close();
|
||||
}
|
||||
|
||||
private void configureStructureTable() {
|
||||
TableColumn<ColumnInfo, String> nameColumn = new TableColumn<>("字段名");
|
||||
nameColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().name()));
|
||||
nameColumn.setPrefWidth(180);
|
||||
|
||||
TableColumn<ColumnInfo, String> typeColumn = new TableColumn<>("类型");
|
||||
typeColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().type()));
|
||||
typeColumn.setPrefWidth(140);
|
||||
|
||||
TableColumn<ColumnInfo, String> notNullColumn = new TableColumn<>("非空");
|
||||
notNullColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().notNull() ? "是" : "否"));
|
||||
notNullColumn.setPrefWidth(90);
|
||||
|
||||
TableColumn<ColumnInfo, String> primaryKeyColumn = new TableColumn<>("主键");
|
||||
primaryKeyColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().primaryKey() ? "是" : "否"));
|
||||
primaryKeyColumn.setPrefWidth(90);
|
||||
|
||||
TableColumn<ColumnInfo, String> defaultValueColumn = new TableColumn<>("默认值");
|
||||
defaultValueColumn.setCellValueFactory(data -> new SimpleStringProperty(displayValue(data.getValue().defaultValue())));
|
||||
defaultValueColumn.setPrefWidth(160);
|
||||
|
||||
tableStructureView.getColumns().setAll(nameColumn, typeColumn, notNullColumn, primaryKeyColumn, defaultValueColumn);
|
||||
}
|
||||
|
||||
private void refreshTables() {
|
||||
refreshTablesSelecting(null);
|
||||
}
|
||||
|
||||
private void refreshTablesPreservingSelection() {
|
||||
String selectedName = Optional.ofNullable(tableListView.getSelectionModel().getSelectedItem())
|
||||
.map(TableInfo::name)
|
||||
.orElse(null);
|
||||
refreshTablesSelecting(selectedName);
|
||||
}
|
||||
|
||||
private void refreshTablesSelecting(String tableName) {
|
||||
try {
|
||||
List<TableInfo> tables = metadataService.listTables(connection());
|
||||
tableListView.setItems(FXCollections.observableArrayList(tables));
|
||||
|
||||
if (tables.isEmpty()) {
|
||||
clearSelectedTableUi();
|
||||
status("当前数据库没有用户表或视图");
|
||||
return;
|
||||
}
|
||||
|
||||
TableInfo tableToLoad = tables.stream()
|
||||
.filter(table -> table.name().equals(tableName))
|
||||
.findFirst()
|
||||
.orElse(tables.getFirst());
|
||||
selectTable(tableToLoad);
|
||||
status("已加载 " + tables.size() + " 个表或视图");
|
||||
} catch (SQLException e) {
|
||||
DialogUtils.showError("刷新表列表失败", e.getMessage());
|
||||
status("刷新表列表失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void selectTable(TableInfo table) {
|
||||
selectingTableProgrammatically = true;
|
||||
try {
|
||||
tableListView.getSelectionModel().select(table);
|
||||
} finally {
|
||||
selectingTableProgrammatically = false;
|
||||
}
|
||||
loadSelectedTable(table);
|
||||
}
|
||||
|
||||
private void loadSelectedTable(TableInfo table) {
|
||||
clearSelectedTableUi();
|
||||
try {
|
||||
currentTable = table;
|
||||
currentTablePageIndex = 0;
|
||||
|
||||
List<ColumnInfo> columns = metadataService.listColumns(connection(), table.name());
|
||||
tableStructureView.setItems(FXCollections.observableArrayList(columns));
|
||||
|
||||
currentTableTotalRows = metadataService.countTableRows(connection(), table.name());
|
||||
loadCurrentTablePage();
|
||||
mainTabPane.getSelectionModel().select(0);
|
||||
} catch (SQLException e) {
|
||||
clearSelectedTableUi();
|
||||
DialogUtils.showError("加载表失败", e.getMessage());
|
||||
status("加载表失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCurrentTablePage() throws SQLException {
|
||||
if (currentTable == null) {
|
||||
resetTablePaginationUi();
|
||||
return;
|
||||
}
|
||||
|
||||
long offset = currentTablePageIndex * TABLE_PAGE_SIZE;
|
||||
QueryResult preview = metadataService.previewTable(connection(), currentTable.name(), TABLE_PAGE_SIZE, offset);
|
||||
renderQueryResult(tableDataView, preview);
|
||||
updateTablePaginationUi(preview.rows().size());
|
||||
}
|
||||
|
||||
private void renderQueryResult(TableView<ObservableList<Object>> tableView, QueryResult result) {
|
||||
renderRows(tableView, result.columnNames(), result.rows());
|
||||
}
|
||||
|
||||
private void renderRows(TableView<ObservableList<Object>> tableView, List<String> columnNames, List<List<Object>> resultRows) {
|
||||
tableView.getColumns().clear();
|
||||
tableView.getItems().clear();
|
||||
|
||||
for (int columnIndex = 0; columnIndex < columnNames.size(); columnIndex++) {
|
||||
final int index = columnIndex;
|
||||
TableColumn<ObservableList<Object>, String> column = new TableColumn<>(columnNames.get(columnIndex));
|
||||
column.setCellValueFactory(data -> {
|
||||
ObservableList<Object> row = data.getValue();
|
||||
Object value = index < row.size() ? row.get(index) : null;
|
||||
return new SimpleStringProperty(displayValue(value));
|
||||
});
|
||||
column.setPrefWidth(140);
|
||||
tableView.getColumns().add(column);
|
||||
}
|
||||
|
||||
ObservableList<ObservableList<Object>> rows = FXCollections.observableArrayList();
|
||||
for (List<Object> row : resultRows) {
|
||||
rows.add(FXCollections.observableArrayList(row));
|
||||
}
|
||||
tableView.setItems(rows);
|
||||
}
|
||||
|
||||
private void clearTable(TableView<ObservableList<Object>> tableView) {
|
||||
tableView.getColumns().clear();
|
||||
tableView.getItems().clear();
|
||||
}
|
||||
|
||||
private long totalTablePages() {
|
||||
if (currentTableTotalRows <= 0) {
|
||||
return 1;
|
||||
}
|
||||
return ((currentTableTotalRows - 1) / TABLE_PAGE_SIZE) + 1;
|
||||
}
|
||||
|
||||
private long totalSqlPages() {
|
||||
if (currentSqlResult == null || currentSqlResult.rows().isEmpty()) {
|
||||
return 1;
|
||||
}
|
||||
return ((currentSqlResult.rows().size() - 1L) / SQL_PAGE_SIZE) + 1;
|
||||
}
|
||||
|
||||
private void updateTablePaginationUi(int displayedRows) {
|
||||
long totalPages = totalTablePages();
|
||||
long currentPage = currentTablePageIndex + 1;
|
||||
previousTablePageButton.setDisable(currentTable == null || currentTablePageIndex <= 0);
|
||||
nextTablePageButton.setDisable(currentTable == null || currentTablePageIndex >= totalPages - 1);
|
||||
|
||||
String pageText;
|
||||
String statusText;
|
||||
if (currentTableTotalRows == 0) {
|
||||
pageText = "第 1 / 1 页,共 0 行";
|
||||
statusText = "表 " + currentTable.name() + ":无数据";
|
||||
} else {
|
||||
long startRow = currentTablePageIndex * TABLE_PAGE_SIZE + 1;
|
||||
long endRow = startRow + displayedRows - 1;
|
||||
pageText = "第 " + currentPage + " / " + totalPages + " 页,共 " + currentTableTotalRows + " 行,当前 " + startRow + "-" + endRow;
|
||||
statusText = "表 " + currentTable.name() + ":第 " + currentPage + " / " + totalPages + " 页,显示 " + startRow + "-" + endRow + " / " + currentTableTotalRows + " 行,每页 " + TABLE_PAGE_SIZE + " 行";
|
||||
}
|
||||
|
||||
tablePageLabel.setText(pageText);
|
||||
status(statusText);
|
||||
}
|
||||
|
||||
private void resetTablePaginationUi() {
|
||||
currentTable = null;
|
||||
currentTableTotalRows = 0;
|
||||
currentTablePageIndex = 0;
|
||||
tablePageLabel.setText("未选择表");
|
||||
previousTablePageButton.setDisable(true);
|
||||
nextTablePageButton.setDisable(true);
|
||||
}
|
||||
|
||||
private void clearSelectedTableUi() {
|
||||
clearTable(tableDataView);
|
||||
tableStructureView.getItems().clear();
|
||||
resetTablePaginationUi();
|
||||
}
|
||||
|
||||
private void renderCurrentSqlPage() {
|
||||
if (currentSqlResult == null) {
|
||||
clearTable(sqlResultView);
|
||||
resetSqlPaginationUi();
|
||||
return;
|
||||
}
|
||||
|
||||
List<List<Object>> allRows = currentSqlResult.rows();
|
||||
int totalRows = allRows.size();
|
||||
int fromIndex = (int) Math.min(currentSqlPageIndex * SQL_PAGE_SIZE, totalRows);
|
||||
int toIndex = Math.min(fromIndex + SQL_PAGE_SIZE, totalRows);
|
||||
List<List<Object>> pageRows = allRows.subList(fromIndex, toIndex);
|
||||
|
||||
renderRows(sqlResultView, currentSqlResult.columnNames(), pageRows);
|
||||
updateSqlPaginationUi(pageRows.size());
|
||||
}
|
||||
|
||||
private void updateSqlPaginationUi(int displayedRows) {
|
||||
long totalRows = currentSqlResult == null ? 0 : currentSqlResult.rows().size();
|
||||
long totalPages = totalSqlPages();
|
||||
long currentPage = currentSqlPageIndex + 1;
|
||||
previousSqlPageButton.setDisable(currentSqlResult == null || currentSqlPageIndex <= 0);
|
||||
nextSqlPageButton.setDisable(currentSqlResult == null || currentSqlPageIndex >= totalPages - 1);
|
||||
|
||||
if (totalRows == 0) {
|
||||
sqlPageLabel.setText("第 1 / 1 页,共 0 行");
|
||||
} else {
|
||||
long startRow = currentSqlPageIndex * SQL_PAGE_SIZE + 1;
|
||||
long endRow = startRow + displayedRows - 1;
|
||||
sqlPageLabel.setText("第 " + currentPage + " / " + totalPages + " 页,共 " + totalRows + " 行,当前 " + startRow + "-" + endRow);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetSqlPaginationUi() {
|
||||
currentSqlResult = null;
|
||||
currentSqlPageIndex = 0;
|
||||
sqlPageLabel.setText("未执行查询");
|
||||
previousSqlPageButton.setDisable(true);
|
||||
nextSqlPageButton.setDisable(true);
|
||||
}
|
||||
|
||||
private String displayValue(Object value) {
|
||||
return value == null ? "NULL" : String.valueOf(value);
|
||||
}
|
||||
|
||||
private boolean ensureConnected() {
|
||||
if (connectionManager.isConnected()) {
|
||||
return true;
|
||||
}
|
||||
DialogUtils.showError("未连接数据库", "请先打开一个 SQLite 数据库文件。");
|
||||
status("未连接数据库");
|
||||
return false;
|
||||
}
|
||||
|
||||
private Connection connection() {
|
||||
return connectionManager.getConnection();
|
||||
}
|
||||
|
||||
private void updateDisconnectedUi() {
|
||||
databasePathLabel.setText("未连接数据库");
|
||||
tableListView.setItems(FXCollections.observableArrayList());
|
||||
tableStructureView.getItems().clear();
|
||||
clearTable(tableDataView);
|
||||
resetTablePaginationUi();
|
||||
clearTable(sqlResultView);
|
||||
resetSqlPaginationUi();
|
||||
sqlMessageLabel.setText("等待执行 SQL");
|
||||
status("未连接数据库");
|
||||
}
|
||||
|
||||
private void status(String message) {
|
||||
statusLabel.setText(message);
|
||||
}
|
||||
|
||||
private Window currentWindow() {
|
||||
Node node = statusLabel;
|
||||
return node.getScene().getWindow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.hypersql.db;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class DatabaseConnectionManager {
|
||||
private Connection connection;
|
||||
private Path currentDatabasePath;
|
||||
|
||||
public void open(Path databasePath) throws SQLException {
|
||||
close();
|
||||
Path absolutePath = databasePath.toAbsolutePath().normalize();
|
||||
connection = DriverManager.getConnection("jdbc:sqlite:" + absolutePath);
|
||||
currentDatabasePath = absolutePath;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (connection != null) {
|
||||
try {
|
||||
connection.close();
|
||||
} catch (SQLException ignored) {
|
||||
} finally {
|
||||
connection = null;
|
||||
currentDatabasePath = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
try {
|
||||
return connection != null && !connection.isClosed();
|
||||
} catch (SQLException _) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Connection getConnection() {
|
||||
if (!isConnected()) {
|
||||
throw new IllegalStateException("当前未连接数据库");
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
public Optional<Path> getCurrentDatabasePath() {
|
||||
return Optional.ofNullable(currentDatabasePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.hypersql.db;
|
||||
|
||||
import com.hypersql.model.ColumnInfo;
|
||||
import com.hypersql.model.QueryResult;
|
||||
import com.hypersql.model.TableInfo;
|
||||
import com.hypersql.util.SqlUtils;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class DatabaseMetadataService {
|
||||
public List<TableInfo> listTables(Connection connection) throws SQLException {
|
||||
String sql = """
|
||||
SELECT name, type
|
||||
FROM sqlite_master
|
||||
WHERE type IN ('table', 'view')
|
||||
AND name NOT LIKE 'sqlite_%'
|
||||
ORDER BY type, name
|
||||
""";
|
||||
try (PreparedStatement statement = connection.prepareStatement(sql);
|
||||
ResultSet resultSet = statement.executeQuery()) {
|
||||
List<TableInfo> tables = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
tables.add(new TableInfo(resultSet.getString("name"), resultSet.getString("type")));
|
||||
}
|
||||
return tables;
|
||||
}
|
||||
}
|
||||
|
||||
public List<ColumnInfo> listColumns(Connection connection, String tableName) throws SQLException {
|
||||
String sql = "PRAGMA table_info(" + SqlUtils.quoteIdentifier(tableName) + ")";
|
||||
try (Statement statement = connection.createStatement();
|
||||
ResultSet resultSet = statement.executeQuery(sql)) {
|
||||
List<ColumnInfo> columns = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
columns.add(new ColumnInfo(
|
||||
resultSet.getString("name"),
|
||||
resultSet.getString("type"),
|
||||
resultSet.getInt("notnull") == 1,
|
||||
resultSet.getInt("pk") > 0,
|
||||
resultSet.getString("dflt_value")
|
||||
));
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
}
|
||||
|
||||
public long countTableRows(Connection connection, String tableName) throws SQLException {
|
||||
String sql = "SELECT COUNT(*) FROM " + SqlUtils.quoteIdentifier(tableName);
|
||||
try (Statement statement = connection.createStatement();
|
||||
ResultSet resultSet = statement.executeQuery(sql)) {
|
||||
return resultSet.next() ? resultSet.getLong(1) : 0L;
|
||||
}
|
||||
}
|
||||
|
||||
public QueryResult previewTable(Connection connection, String tableName, int limit) throws SQLException {
|
||||
return previewTable(connection, tableName, limit, 0L);
|
||||
}
|
||||
|
||||
public QueryResult previewTable(Connection connection, String tableName, int limit, long offset) throws SQLException {
|
||||
int safeLimit = Math.max(limit, 1);
|
||||
long safeOffset = Math.max(offset, 0L);
|
||||
String sql = "SELECT * FROM " + SqlUtils.quoteIdentifier(tableName)
|
||||
+ " LIMIT " + safeLimit
|
||||
+ " OFFSET " + safeOffset;
|
||||
return executeQuery(connection, sql);
|
||||
}
|
||||
|
||||
private QueryResult executeQuery(Connection connection, String sql) throws SQLException {
|
||||
try (Statement statement = connection.createStatement();
|
||||
ResultSet resultSet = statement.executeQuery(sql)) {
|
||||
int columnCount = resultSet.getMetaData().getColumnCount();
|
||||
List<String> columnNames = new ArrayList<>();
|
||||
for (int i = 1; i <= columnCount; i++) {
|
||||
columnNames.add(resultSet.getMetaData().getColumnName(i));
|
||||
}
|
||||
|
||||
List<List<Object>> rows = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
List<Object> row = new ArrayList<>();
|
||||
for (int i = 1; i <= columnCount; i++) {
|
||||
row.add(resultSet.getObject(i));
|
||||
}
|
||||
rows.add(row);
|
||||
}
|
||||
return QueryResult.resultSet(columnNames, rows);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.hypersql.db;
|
||||
|
||||
import com.hypersql.model.QueryResult;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class SqlExecutionService {
|
||||
public QueryResult execute(Connection connection, String sql) throws SQLException {
|
||||
if (sql == null || sql.isBlank()) {
|
||||
throw new IllegalArgumentException("SQL 语句不能为空");
|
||||
}
|
||||
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
boolean hasResultSet = statement.execute(sql.trim());
|
||||
if (hasResultSet) {
|
||||
try (ResultSet resultSet = statement.getResultSet()) {
|
||||
return readResultSet(resultSet);
|
||||
}
|
||||
}
|
||||
return QueryResult.updateCount(statement.getUpdateCount());
|
||||
}
|
||||
}
|
||||
|
||||
private QueryResult readResultSet(ResultSet resultSet) throws SQLException {
|
||||
ResultSetMetaData metaData = resultSet.getMetaData();
|
||||
int columnCount = metaData.getColumnCount();
|
||||
List<String> columnNames = new ArrayList<>();
|
||||
for (int i = 1; i <= columnCount; i++) {
|
||||
columnNames.add(metaData.getColumnName(i));
|
||||
}
|
||||
|
||||
List<List<Object>> rows = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
List<Object> row = new ArrayList<>();
|
||||
for (int i = 1; i <= columnCount; i++) {
|
||||
row.add(resultSet.getObject(i));
|
||||
}
|
||||
rows.add(row);
|
||||
}
|
||||
return QueryResult.resultSet(columnNames, rows);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.hypersql.model;
|
||||
|
||||
public record ColumnInfo(
|
||||
String name,
|
||||
String type,
|
||||
boolean notNull,
|
||||
boolean primaryKey,
|
||||
String defaultValue
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.hypersql.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public record QueryResult(
|
||||
boolean resultSet,
|
||||
List<String> columnNames,
|
||||
List<List<Object>> rows,
|
||||
int updateCount,
|
||||
String message
|
||||
) {
|
||||
public static QueryResult resultSet(List<String> columnNames, List<List<Object>> rows) {
|
||||
return new QueryResult(true, List.copyOf(columnNames), copyRows(rows), -1, "返回 " + rows.size() + " 行");
|
||||
}
|
||||
|
||||
public static QueryResult updateCount(int updateCount) {
|
||||
return new QueryResult(false, List.of(), List.of(), updateCount, "影响 " + Math.max(updateCount, 0) + " 行");
|
||||
}
|
||||
|
||||
private static List<List<Object>> copyRows(List<List<Object>> rows) {
|
||||
return rows.stream()
|
||||
.map(row -> Collections.unmodifiableList(new ArrayList<>(row)))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.hypersql.model;
|
||||
|
||||
public record TableInfo(String name, String type) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.hypersql.util;
|
||||
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
|
||||
public final class DialogUtils {
|
||||
private DialogUtils() {
|
||||
}
|
||||
|
||||
public static void showError(String title, String message) {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
public static void showInfo(String title, String message) {
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
alert.showAndWait();
|
||||
}
|
||||
|
||||
public static boolean confirm(String title, String message) {
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText(message);
|
||||
return alert.showAndWait()
|
||||
.filter(ButtonType.OK::equals)
|
||||
.isPresent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.hypersql.util;
|
||||
|
||||
public final class SqlUtils {
|
||||
private SqlUtils() {
|
||||
}
|
||||
|
||||
public static String quoteIdentifier(String identifier) {
|
||||
if (identifier == null || identifier.isBlank()) {
|
||||
throw new IllegalArgumentException("标识符不能为空");
|
||||
}
|
||||
return "\"" + identifier.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.TableColumn?>
|
||||
<?import javafx.scene.control.TableView?>
|
||||
<?import javafx.scene.control.TextArea?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.layout.BorderPane?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<BorderPane xmlns="http://javafx.com/javafx/24.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.hypersql.controller.CreateTableDialogController">
|
||||
<center>
|
||||
<VBox spacing="10">
|
||||
<padding>
|
||||
<Insets bottom="12" left="12" right="12" top="12" />
|
||||
</padding>
|
||||
<Label styleClass="section-title" text="表名" />
|
||||
<TextField fx:id="tableNameField" promptText="例如:users" />
|
||||
|
||||
<Label styleClass="section-title" text="列定义" />
|
||||
<TableView fx:id="columnsTable" editable="true" prefHeight="220">
|
||||
<columns>
|
||||
<TableColumn fx:id="columnNameColumn" prefWidth="150" text="列名" />
|
||||
<TableColumn fx:id="columnTypeColumn" prefWidth="120" text="类型" />
|
||||
<TableColumn fx:id="primaryKeyColumn" prefWidth="70" text="主键" />
|
||||
<TableColumn fx:id="notNullColumn" prefWidth="70" text="非空" />
|
||||
<TableColumn fx:id="defaultValueColumn" prefWidth="180" text="默认值" />
|
||||
</columns>
|
||||
</TableView>
|
||||
|
||||
<HBox spacing="8">
|
||||
<Button onAction="#handleAddColumn" text="添加列" />
|
||||
<Button onAction="#handleRemoveColumn" text="删除选中列" />
|
||||
<Label text="文本默认值请写成 'unknown',当前时间可写 CURRENT_TIMESTAMP" />
|
||||
</HBox>
|
||||
|
||||
<Label styleClass="section-title" text="SQL 预览" />
|
||||
<TextArea fx:id="sqlPreviewTextArea" editable="false" prefRowCount="5" wrapText="true" />
|
||||
<Label fx:id="messageLabel" text="填写表名和列定义后点击创建。" wrapText="true" />
|
||||
</VBox>
|
||||
</center>
|
||||
<bottom>
|
||||
<HBox alignment="CENTER_RIGHT" spacing="8">
|
||||
<padding>
|
||||
<Insets bottom="12" left="12" right="12" top="0" />
|
||||
</padding>
|
||||
<Button onAction="#handleCancel" text="取消" />
|
||||
<Button defaultButton="true" onAction="#handleCreate" text="创建" />
|
||||
</HBox>
|
||||
</bottom>
|
||||
</BorderPane>
|
||||
@@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
<?import javafx.scene.control.Menu?>
|
||||
<?import javafx.scene.control.MenuBar?>
|
||||
<?import javafx.scene.control.MenuItem?>
|
||||
<?import javafx.scene.control.Separator?>
|
||||
<?import javafx.scene.control.SplitPane?>
|
||||
<?import javafx.scene.control.Tab?>
|
||||
<?import javafx.scene.control.TabPane?>
|
||||
<?import javafx.scene.control.TableView?>
|
||||
<?import javafx.scene.control.TextArea?>
|
||||
<?import javafx.scene.control.ToolBar?>
|
||||
<?import javafx.scene.layout.BorderPane?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.Priority?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<BorderPane xmlns="http://javafx.com/javafx/24.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.hypersql.controller.MainController">
|
||||
<top>
|
||||
<VBox>
|
||||
<MenuBar>
|
||||
<Menu text="文件">
|
||||
<MenuItem onAction="#handleCreateDatabase" text="新建数据库..." />
|
||||
<MenuItem onAction="#handleOpenDatabase" text="打开数据库..." />
|
||||
<MenuItem onAction="#handleDeleteCurrentDatabase" text="删除当前数据库文件..." />
|
||||
<MenuItem onAction="#handleCloseDatabase" text="关闭数据库" />
|
||||
<MenuItem onAction="#handleExit" text="退出" />
|
||||
</Menu>
|
||||
<Menu text="表">
|
||||
<MenuItem onAction="#handleCreateTable" text="创建表..." />
|
||||
<MenuItem onAction="#handleDeleteSelectedTable" text="删除选中表..." />
|
||||
<MenuItem onAction="#handleRefreshTables" text="刷新表列表" />
|
||||
</Menu>
|
||||
<Menu text="执行">
|
||||
<MenuItem onAction="#handleExecuteSql" text="执行 SQL" />
|
||||
</Menu>
|
||||
</MenuBar>
|
||||
<ToolBar>
|
||||
<Button onAction="#handleCreateDatabase" text="新建数据库" />
|
||||
<Button onAction="#handleOpenDatabase" text="打开数据库" />
|
||||
<Button onAction="#handleDeleteCurrentDatabase" text="删除数据库" />
|
||||
<Button onAction="#handleCloseDatabase" text="关闭数据库" />
|
||||
<Separator />
|
||||
<Button onAction="#handleCreateTable" text="创建表" />
|
||||
<Button onAction="#handleDeleteSelectedTable" text="删除表" />
|
||||
<Button onAction="#handleRefreshTables" text="刷新表列表" />
|
||||
<Button onAction="#handleExecuteSql" text="执行 SQL" />
|
||||
</ToolBar>
|
||||
</VBox>
|
||||
</top>
|
||||
|
||||
<center>
|
||||
<SplitPane dividerPositions="0.26">
|
||||
<items>
|
||||
<VBox spacing="8" styleClass="sidebar">
|
||||
<padding>
|
||||
<Insets bottom="10" left="10" right="10" top="10" />
|
||||
</padding>
|
||||
<Label styleClass="section-title" text="当前数据库" />
|
||||
<Label fx:id="databasePathLabel" maxWidth="Infinity" text="未连接数据库" wrapText="true" />
|
||||
<Label styleClass="section-title" text="表 / 视图列表" />
|
||||
<ListView fx:id="tableListView" VBox.vgrow="ALWAYS" />
|
||||
</VBox>
|
||||
<TabPane fx:id="mainTabPane">
|
||||
<tabs>
|
||||
<Tab closable="false" text="表数据">
|
||||
<content>
|
||||
<BorderPane>
|
||||
<center>
|
||||
<TableView fx:id="tableDataView" />
|
||||
</center>
|
||||
<bottom>
|
||||
<HBox alignment="CENTER_LEFT" spacing="8" styleClass="pagination-bar">
|
||||
<padding>
|
||||
<Insets bottom="8" left="8" right="8" top="8" />
|
||||
</padding>
|
||||
<Button fx:id="previousTablePageButton" onAction="#handlePreviousTablePage" text="上一页" />
|
||||
<Button fx:id="nextTablePageButton" onAction="#handleNextTablePage" text="下一页" />
|
||||
<Label fx:id="tablePageLabel" text="未选择表" />
|
||||
</HBox>
|
||||
</bottom>
|
||||
</BorderPane>
|
||||
</content>
|
||||
</Tab>
|
||||
<Tab closable="false" text="表结构">
|
||||
<content>
|
||||
<TableView fx:id="tableStructureView" />
|
||||
</content>
|
||||
</Tab>
|
||||
<Tab closable="false" text="SQL 执行">
|
||||
<content>
|
||||
<VBox spacing="8">
|
||||
<padding>
|
||||
<Insets bottom="10" left="10" right="10" top="10" />
|
||||
</padding>
|
||||
<Label styleClass="section-title" text="SQL 输入" />
|
||||
<TextArea fx:id="sqlTextArea" prefRowCount="7" promptText="请输入 SQL 语句" VBox.vgrow="NEVER" />
|
||||
<HBox alignment="CENTER_LEFT" spacing="8">
|
||||
<Button onAction="#handleExecuteSql" text="执行 SQL" />
|
||||
<Label fx:id="sqlMessageLabel" text="等待执行 SQL" HBox.hgrow="ALWAYS" />
|
||||
</HBox>
|
||||
<Label styleClass="section-title" text="SQL 结果" />
|
||||
<BorderPane VBox.vgrow="ALWAYS">
|
||||
<center>
|
||||
<TableView fx:id="sqlResultView" />
|
||||
</center>
|
||||
<bottom>
|
||||
<HBox alignment="CENTER_LEFT" spacing="8" styleClass="pagination-bar">
|
||||
<padding>
|
||||
<Insets bottom="8" left="8" right="8" top="8" />
|
||||
</padding>
|
||||
<Button fx:id="previousSqlPageButton" onAction="#handlePreviousSqlPage" text="上一页" />
|
||||
<Button fx:id="nextSqlPageButton" onAction="#handleNextSqlPage" text="下一页" />
|
||||
<Label fx:id="sqlPageLabel" text="未执行查询" />
|
||||
</HBox>
|
||||
</bottom>
|
||||
</BorderPane>
|
||||
</VBox>
|
||||
</content>
|
||||
</Tab>
|
||||
</tabs>
|
||||
</TabPane>
|
||||
</items>
|
||||
</SplitPane>
|
||||
</center>
|
||||
|
||||
<bottom>
|
||||
<HBox styleClass="status-bar">
|
||||
<padding>
|
||||
<Insets bottom="6" left="10" right="10" top="6" />
|
||||
</padding>
|
||||
<Label fx:id="statusLabel" text="未连接数据库" />
|
||||
</HBox>
|
||||
</bottom>
|
||||
</BorderPane>
|
||||
@@ -0,0 +1,21 @@
|
||||
.root {
|
||||
-fx-font-family: "Arial", "PingFang SC", sans-serif;
|
||||
-fx-font-size: 13px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
-fx-background-color: #f7f8fa;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
-fx-background-color: #f0f0f0;
|
||||
-fx-border-color: #d0d0d0 transparent transparent transparent;
|
||||
}
|
||||
|
||||
.text-area {
|
||||
-fx-font-family: "JetBrains Mono", "Menlo", "Consolas", monospace;
|
||||
}
|
||||
Reference in New Issue
Block a user