commit 03a347ea31fa166d0dbd16618dd3977871c47906 Author: Gary Gan Date: Sun May 10 13:05:19 2026 +0800 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5bbb13a --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +target/ +.claude/ + +*.class +*.jar +*.war +*.ear + +.DS_Store +.idea/ +.vscode/ +*.iml + +*.log + +# Local SQLite test databases +*.db +*.sqlite +*.sqlite3 diff --git a/feasibility_analysis.md b/feasibility_analysis.md new file mode 100644 index 0000000..abcf5d7 --- /dev/null +++ b/feasibility_analysis.md @@ -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 生成。 \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3520dee --- /dev/null +++ b/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + com.hypersql + hypersql + 1.0.0 + HyperSql + + + UTF-8 + 24 + 24 + 24.0.1 + + + + + org.openjfx + javafx-controls + ${javafx.version} + + + org.openjfx + javafx-fxml + ${javafx.version} + + + org.xerial + sqlite-jdbc + 3.49.1.0 + + + org.slf4j + slf4j-simple + 2.0.17 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + 24 + + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + com.hypersql.HyperSqlApplication + + + + + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..7561cb2 --- /dev/null +++ b/readme.md @@ -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`:需求分析 diff --git a/requirements_analysis.md b/requirements_analysis.md new file mode 100644 index 0000000..57f5ea7 --- /dev/null +++ b/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 生成作为项目亮点。 \ No newline at end of file diff --git a/software_process_model.md b/software_process_model.md new file mode 100644 index 0000000..5512f61 --- /dev/null +++ b/software_process_model.md @@ -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 生成等扩展功能。 \ No newline at end of file diff --git a/src/main/java/com/hypersql/HyperSqlApplication.java b/src/main/java/com/hypersql/HyperSqlApplication.java new file mode 100644 index 0000000..3be66a4 --- /dev/null +++ b/src/main/java/com/hypersql/HyperSqlApplication.java @@ -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); + } +} diff --git a/src/main/java/com/hypersql/controller/CreateTableDialogController.java b/src/main/java/com/hypersql/controller/CreateTableDialogController.java new file mode 100644 index 0000000..45a47a9 --- /dev/null +++ b/src/main/java/com/hypersql/controller/CreateTableDialogController.java @@ -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 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 columnsTable; + + @FXML + private TableColumn columnNameColumn; + + @FXML + private TableColumn columnTypeColumn; + + @FXML + private TableColumn primaryKeyColumn; + + @FXML + private TableColumn notNullColumn; + + @FXML + private TableColumn defaultValueColumn; + + @FXML + private TextArea sqlPreviewTextArea; + + @FXML + private Label messageLabel; + + private final Set 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 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 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 validateInput() { + Set columnNames = new HashSet<>(); + List 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); + } + } +} diff --git a/src/main/java/com/hypersql/controller/MainController.java b/src/main/java/com/hypersql/controller/MainController.java new file mode 100644 index 0000000..7a4fa46 --- /dev/null +++ b/src/main/java/com/hypersql/controller/MainController.java @@ -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 tableListView; + + @FXML + private TabPane mainTabPane; + + @FXML + private TableView> tableDataView; + + @FXML + private Button previousTablePageButton; + + @FXML + private Button nextTablePageButton; + + @FXML + private Label tablePageLabel; + + @FXML + private TableView tableStructureView; + + @FXML + private TextArea sqlTextArea; + + @FXML + private Label sqlMessageLabel; + + @FXML + private TableView> 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 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 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 nameColumn = new TableColumn<>("字段名"); + nameColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().name())); + nameColumn.setPrefWidth(180); + + TableColumn typeColumn = new TableColumn<>("类型"); + typeColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().type())); + typeColumn.setPrefWidth(140); + + TableColumn notNullColumn = new TableColumn<>("非空"); + notNullColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().notNull() ? "是" : "否")); + notNullColumn.setPrefWidth(90); + + TableColumn primaryKeyColumn = new TableColumn<>("主键"); + primaryKeyColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().primaryKey() ? "是" : "否")); + primaryKeyColumn.setPrefWidth(90); + + TableColumn 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 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 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> tableView, QueryResult result) { + renderRows(tableView, result.columnNames(), result.rows()); + } + + private void renderRows(TableView> tableView, List columnNames, List> resultRows) { + tableView.getColumns().clear(); + tableView.getItems().clear(); + + for (int columnIndex = 0; columnIndex < columnNames.size(); columnIndex++) { + final int index = columnIndex; + TableColumn, String> column = new TableColumn<>(columnNames.get(columnIndex)); + column.setCellValueFactory(data -> { + ObservableList 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> rows = FXCollections.observableArrayList(); + for (List row : resultRows) { + rows.add(FXCollections.observableArrayList(row)); + } + tableView.setItems(rows); + } + + private void clearTable(TableView> 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> 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> 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(); + } +} diff --git a/src/main/java/com/hypersql/db/DatabaseConnectionManager.java b/src/main/java/com/hypersql/db/DatabaseConnectionManager.java new file mode 100644 index 0000000..e6af6c2 --- /dev/null +++ b/src/main/java/com/hypersql/db/DatabaseConnectionManager.java @@ -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 getCurrentDatabasePath() { + return Optional.ofNullable(currentDatabasePath); + } +} diff --git a/src/main/java/com/hypersql/db/DatabaseMetadataService.java b/src/main/java/com/hypersql/db/DatabaseMetadataService.java new file mode 100644 index 0000000..6dfd3be --- /dev/null +++ b/src/main/java/com/hypersql/db/DatabaseMetadataService.java @@ -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 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 tables = new ArrayList<>(); + while (resultSet.next()) { + tables.add(new TableInfo(resultSet.getString("name"), resultSet.getString("type"))); + } + return tables; + } + } + + public List 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 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 columnNames = new ArrayList<>(); + for (int i = 1; i <= columnCount; i++) { + columnNames.add(resultSet.getMetaData().getColumnName(i)); + } + + List> rows = new ArrayList<>(); + while (resultSet.next()) { + List row = new ArrayList<>(); + for (int i = 1; i <= columnCount; i++) { + row.add(resultSet.getObject(i)); + } + rows.add(row); + } + return QueryResult.resultSet(columnNames, rows); + } + } +} diff --git a/src/main/java/com/hypersql/db/SqlExecutionService.java b/src/main/java/com/hypersql/db/SqlExecutionService.java new file mode 100644 index 0000000..47688ed --- /dev/null +++ b/src/main/java/com/hypersql/db/SqlExecutionService.java @@ -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 columnNames = new ArrayList<>(); + for (int i = 1; i <= columnCount; i++) { + columnNames.add(metaData.getColumnName(i)); + } + + List> rows = new ArrayList<>(); + while (resultSet.next()) { + List row = new ArrayList<>(); + for (int i = 1; i <= columnCount; i++) { + row.add(resultSet.getObject(i)); + } + rows.add(row); + } + return QueryResult.resultSet(columnNames, rows); + } +} diff --git a/src/main/java/com/hypersql/model/ColumnInfo.java b/src/main/java/com/hypersql/model/ColumnInfo.java new file mode 100644 index 0000000..4d93a3f --- /dev/null +++ b/src/main/java/com/hypersql/model/ColumnInfo.java @@ -0,0 +1,10 @@ +package com.hypersql.model; + +public record ColumnInfo( + String name, + String type, + boolean notNull, + boolean primaryKey, + String defaultValue +) { +} diff --git a/src/main/java/com/hypersql/model/QueryResult.java b/src/main/java/com/hypersql/model/QueryResult.java new file mode 100644 index 0000000..a43aac4 --- /dev/null +++ b/src/main/java/com/hypersql/model/QueryResult.java @@ -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 columnNames, + List> rows, + int updateCount, + String message +) { + public static QueryResult resultSet(List columnNames, List> 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> copyRows(List> rows) { + return rows.stream() + .map(row -> Collections.unmodifiableList(new ArrayList<>(row))) + .toList(); + } +} diff --git a/src/main/java/com/hypersql/model/TableInfo.java b/src/main/java/com/hypersql/model/TableInfo.java new file mode 100644 index 0000000..1975bf6 --- /dev/null +++ b/src/main/java/com/hypersql/model/TableInfo.java @@ -0,0 +1,8 @@ +package com.hypersql.model; + +public record TableInfo(String name, String type) { + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/com/hypersql/util/DialogUtils.java b/src/main/java/com/hypersql/util/DialogUtils.java new file mode 100644 index 0000000..c44bc5f --- /dev/null +++ b/src/main/java/com/hypersql/util/DialogUtils.java @@ -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(); + } +} diff --git a/src/main/java/com/hypersql/util/SqlUtils.java b/src/main/java/com/hypersql/util/SqlUtils.java new file mode 100644 index 0000000..0a6a3c9 --- /dev/null +++ b/src/main/java/com/hypersql/util/SqlUtils.java @@ -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("\"", "\"\"") + "\""; + } +} diff --git a/src/main/resources/com/hypersql/create-table-dialog.fxml b/src/main/resources/com/hypersql/create-table-dialog.fxml new file mode 100644 index 0000000..447f6f7 --- /dev/null +++ b/src/main/resources/com/hypersql/create-table-dialog.fxml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + +
+ + + + +