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:
2026-05-10 13:05:19 +08:00
commit 03a347ea31
20 changed files with 2232 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
target/
.claude/
*.class
*.jar
*.war
*.ear
.DS_Store
.idea/
.vscode/
*.iml
*.log
# Local SQLite test databases
*.db
*.sqlite
*.sqlite3
+175
View File
@@ -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 生成。
+62
View File
@@ -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>
+45
View File
@@ -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`:需求分析
+229
View File
@@ -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 生成作为项目亮点。
+113
View File
@@ -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>
+21
View File
@@ -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;
}